You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

151 lines
4.6 KiB

/**
* API Authorization Helpers
* Reusable authorization functions for API routes
*/
import { error } from '@sveltejs/kit';
import type { NostrEvent } from '../types/nostr.js';
import { verifyNIP98Auth } from '../services/nostr/nip98-auth.js';
import { maintainerService } from '../services/service-registry.js';
import { fileManager } from '../services/service-registry.js';
import type { RepoContext, RequestContext, RepoRequestContext } from './api-context.js';
import { handleValidationError, handleAuthError, handleAuthorizationError, handleNotFoundError } from './error-handler.js';
/**
* Check if user has access to a repository (privacy check)
*
* @param repoContext - Repository context
* @param requestContext - Request context with user pubkey
* @param operation - Operation name for error context
* @returns void if access allowed, throws error if denied
*/
export async function requireRepoAccess(
repoContext: RepoContext,
requestContext: RequestContext,
operation?: string
): Promise<void> {
const canView = await maintainerService.canView(
requestContext.userPubkeyHex || null,
repoContext.repoOwnerPubkey,
repoContext.repo
);
if (!canView) {
throw handleAuthorizationError(
'This repository is private. Only owners and maintainers can view it.',
{ operation, npub: repoContext.npub, repo: repoContext.repo }
);
}
}
/**
* Check if repository exists
*
* @param repoContext - Repository context
* @param operation - Operation name for error context
* @returns void if exists, throws error if not found
*/
export function requireRepoExists(
repoContext: RepoContext,
operation?: string
): void {
if (!fileManager.repoExists(repoContext.npub, repoContext.repo)) {
throw handleNotFoundError(
'Repository not found',
{ operation, npub: repoContext.npub, repo: repoContext.repo }
);
}
}
/**
* Check if user is a maintainer of the repository
*
* @param repoContext - Repository context
* @param requestContext - Request context with user pubkey
* @param operation - Operation name for error context
* @returns void if maintainer, throws error if not
*/
export async function requireMaintainer(
repoContext: RepoContext,
requestContext: RequestContext,
operation?: string
): Promise<void> {
if (!requestContext.userPubkeyHex) {
throw handleAuthError(
'Authentication required. Please provide userPubkey.',
{ operation, npub: repoContext.npub, repo: repoContext.repo }
);
}
const isMaintainer = await maintainerService.isMaintainer(
requestContext.userPubkeyHex,
repoContext.repoOwnerPubkey,
repoContext.repo
);
if (!isMaintainer) {
throw handleAuthorizationError(
'Only repository maintainers can perform this action. Please submit a pull request instead.',
{ operation, npub: repoContext.npub, repo: repoContext.repo }
);
}
}
/**
* Verify NIP-98 authentication from request headers
*
* @param request - Request object
* @param url - URL object
* @param method - HTTP method
* @param operation - Operation name for error context
* @returns NIP-98 event if valid, throws error if invalid
*/
export function requireNIP98Auth(
request: Request,
url: URL,
method: string,
operation?: string
): NostrEvent {
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Nostr ')) {
throw handleAuthError(
'NIP-98 authentication required',
{ operation }
);
}
// Build absolute request URL for NIP-98 validation
const protocol = request.headers.get('x-forwarded-proto') ||
(url.protocol === 'https:' ? 'https' : 'http');
const host = request.headers.get('host') || url.host;
const requestUrl = `${protocol}://${host}${url.pathname}${url.search}`;
const authResult = verifyNIP98Auth(authHeader, requestUrl, method);
if (!authResult.valid || !authResult.event) {
throw handleAuthError(
authResult.error || 'Invalid NIP-98 authentication',
{ operation }
);
}
return authResult.event;
}
/**
* Combined check: repository exists and user has access
*
* @param repoContext - Repository context
* @param requestContext - Request context with user pubkey
* @param operation - Operation name for error context
* @returns void if all checks pass, throws error if any fail
*/
export async function requireRepoAccessWithExists(
repoContext: RepoContext,
requestContext: RequestContext,
operation?: string
): Promise<void> {
requireRepoExists(repoContext, operation);
await requireRepoAccess(repoContext, requestContext, operation);
}