From 4f2fd74309e7dbd4d754bbcf4d35788f6883c36d Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 17 Feb 2026 20:38:23 +0100 Subject: [PATCH] refactor --- src/lib/services/service-registry.ts | 158 ++++++++++ src/lib/utils/api-auth.ts | 151 +++++++++ src/lib/utils/api-context.ts | 162 ++++++++++ src/lib/utils/api-handlers.ts | 289 ++++++++++++++++++ .../[repo]/branch-protection/+server.ts | 90 ++---- .../repos/[npub]/[repo]/branches/+server.ts | 93 ++---- .../repos/[npub]/[repo]/commits/+server.ts | 48 +-- .../api/repos/[npub]/[repo]/diff/+server.ts | 47 +-- .../repos/[npub]/[repo]/download/+server.ts | 73 ++--- .../repos/[npub]/[repo]/highlights/+server.ts | 93 +++--- .../api/repos/[npub]/[repo]/issues/+server.ts | 85 ++---- .../[npub]/[repo]/maintainers/+server.ts | 48 +-- .../api/repos/[npub]/[repo]/prs/+server.ts | 84 ++--- .../api/repos/[npub]/[repo]/raw/+server.ts | 59 +--- .../api/repos/[npub]/[repo]/readme/+server.ts | 57 +--- .../repos/[npub]/[repo]/settings/+server.ts | 122 +++----- .../api/repos/[npub]/[repo]/tags/+server.ts | 102 ++----- .../repos/[npub]/[repo]/transfer/+server.ts | 111 +++---- .../api/repos/[npub]/[repo]/tree/+server.ts | 59 +--- .../api/repos/[npub]/[repo]/verify/+server.ts | 66 ++-- 20 files changed, 1125 insertions(+), 872 deletions(-) create mode 100644 src/lib/services/service-registry.ts create mode 100644 src/lib/utils/api-auth.ts create mode 100644 src/lib/utils/api-context.ts create mode 100644 src/lib/utils/api-handlers.ts diff --git a/src/lib/services/service-registry.ts b/src/lib/services/service-registry.ts new file mode 100644 index 0000000..b5490aa --- /dev/null +++ b/src/lib/services/service-registry.ts @@ -0,0 +1,158 @@ +/** + * Service Registry + * Provides singleton instances of commonly used services + * Reduces memory usage and ensures consistent service configuration across API routes + */ + +import { FileManager } from './git/file-manager.js'; +import { RepoManager } from './git/repo-manager.js'; +import { MaintainerService } from './nostr/maintainer-service.js'; +import { NostrClient } from './nostr/nostr-client.js'; +import { OwnershipTransferService } from './nostr/ownership-transfer-service.js'; +import { BranchProtectionService } from './nostr/branch-protection-service.js'; +import { IssuesService } from './nostr/issues-service.js'; +import { ForkCountService } from './nostr/fork-count-service.js'; +import { PRsService } from './nostr/prs-service.js'; +import { HighlightsService } from './nostr/highlights-service.js'; +import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS } from '../config.js'; + +// Get repo root from environment or use default +const repoRoot = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT + ? process.env.GIT_REPO_ROOT + : '/repos'; + +// Lazy initialization - services are created on first access +let _fileManager: FileManager | null = null; +let _repoManager: RepoManager | null = null; +let _maintainerService: MaintainerService | null = null; +let _nostrClient: NostrClient | null = null; +let _nostrSearchClient: NostrClient | null = null; +let _ownershipTransferService: OwnershipTransferService | null = null; +let _branchProtectionService: BranchProtectionService | null = null; +let _issuesService: IssuesService | null = null; +let _forkCountService: ForkCountService | null = null; +let _prsService: PRsService | null = null; +let _highlightsService: HighlightsService | null = null; + +/** + * Get singleton FileManager instance + */ +export function getFileManager(): FileManager { + if (!_fileManager) { + _fileManager = new FileManager(repoRoot); + } + return _fileManager; +} + +/** + * Get singleton RepoManager instance + */ +export function getRepoManager(): RepoManager { + if (!_repoManager) { + _repoManager = new RepoManager(repoRoot); + } + return _repoManager; +} + +/** + * Get singleton MaintainerService instance + */ +export function getMaintainerService(): MaintainerService { + if (!_maintainerService) { + _maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); + } + return _maintainerService; +} + +/** + * Get singleton NostrClient instance (default relays) + */ +export function getNostrClient(): NostrClient { + if (!_nostrClient) { + _nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); + } + return _nostrClient; +} + +/** + * Get singleton NostrClient instance (search relays) + */ +export function getNostrSearchClient(): NostrClient { + if (!_nostrSearchClient) { + _nostrSearchClient = new NostrClient(DEFAULT_NOSTR_SEARCH_RELAYS); + } + return _nostrSearchClient; +} + +/** + * Get singleton OwnershipTransferService instance + */ +export function getOwnershipTransferService(): OwnershipTransferService { + if (!_ownershipTransferService) { + _ownershipTransferService = new OwnershipTransferService(DEFAULT_NOSTR_RELAYS); + } + return _ownershipTransferService; +} + +/** + * Get singleton BranchProtectionService instance + */ +export function getBranchProtectionService(): BranchProtectionService { + if (!_branchProtectionService) { + _branchProtectionService = new BranchProtectionService(DEFAULT_NOSTR_RELAYS); + } + return _branchProtectionService; +} + +/** + * Get singleton IssuesService instance + */ +export function getIssuesService(): IssuesService { + if (!_issuesService) { + _issuesService = new IssuesService(DEFAULT_NOSTR_RELAYS); + } + return _issuesService; +} + +/** + * Get singleton ForkCountService instance + */ +export function getForkCountService(): ForkCountService { + if (!_forkCountService) { + _forkCountService = new ForkCountService(DEFAULT_NOSTR_RELAYS); + } + return _forkCountService; +} + +/** + * Get singleton PRsService instance + */ +export function getPRsService(): PRsService { + if (!_prsService) { + _prsService = new PRsService(DEFAULT_NOSTR_RELAYS); + } + return _prsService; +} + +/** + * Get singleton HighlightsService instance + */ +export function getHighlightsService(): HighlightsService { + if (!_highlightsService) { + _highlightsService = new HighlightsService(DEFAULT_NOSTR_RELAYS); + } + return _highlightsService; +} + +// Convenience exports for direct access (common pattern) +export const fileManager = getFileManager(); +export const repoManager = getRepoManager(); +export const maintainerService = getMaintainerService(); +export const nostrClient = getNostrClient(); +export const nostrSearchClient = getNostrSearchClient(); +export const ownershipTransferService = getOwnershipTransferService(); +export const branchProtectionService = getBranchProtectionService(); +export const issuesService = getIssuesService(); +export const forkCountService = getForkCountService(); +export const prsService = getPRsService(); +export const highlightsService = getHighlightsService(); diff --git a/src/lib/utils/api-auth.ts b/src/lib/utils/api-auth.ts new file mode 100644 index 0000000..77e61e9 --- /dev/null +++ b/src/lib/utils/api-auth.ts @@ -0,0 +1,151 @@ +/** + * 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 { + 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 { + 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 { + requireRepoExists(repoContext, operation); + await requireRepoAccess(repoContext, requestContext, operation); +} diff --git a/src/lib/utils/api-context.ts b/src/lib/utils/api-context.ts new file mode 100644 index 0000000..e76a216 --- /dev/null +++ b/src/lib/utils/api-context.ts @@ -0,0 +1,162 @@ +/** + * API Context Utilities + * Extract and validate common request context from SvelteKit requests + */ + +import type { RequestEvent } from '@sveltejs/kit'; +import { requireNpubHex, decodeNpubToHex } from './npub-utils.js'; + +// Re-export RequestEvent for convenience +export type { RequestEvent }; + +/** + * Extracted request context + */ +export interface RequestContext { + userPubkey: string | null; + userPubkeyHex: string | null; + clientIp: string; + ref?: string; + path?: string; + branch?: string; + limit?: number; + [key: string]: unknown; +} + +/** + * Repository context with validated parameters + */ +export interface RepoContext { + npub: string; + repo: string; + repoOwnerPubkey: string; +} + +/** + * Combined context for repository operations + */ +export interface RepoRequestContext extends RequestContext, RepoContext {} + +/** + * Extract common request context from a SvelteKit request + * + * @param event - SvelteKit request event + * @param url - URL object (can be extracted from event.url) + * @returns Extracted request context + */ +export function extractRequestContext( + event: RequestEvent, + url?: URL +): RequestContext { + const requestUrl = url || event.url; + + // Extract user pubkey from query params or headers + const userPubkey = requestUrl.searchParams.get('userPubkey') || + event.request.headers.get('x-user-pubkey') || + null; + + // Convert to hex if needed + const userPubkeyHex = userPubkey ? (decodeNpubToHex(userPubkey) || userPubkey) : null; + + // Extract client IP + let clientIp: string; + try { + clientIp = event.getClientAddress(); + } catch { + // Fallback for internal Vite dev server requests or when client address can't be determined + clientIp = event.request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + event.request.headers.get('x-real-ip') || + '127.0.0.1'; + } + + // Extract common query parameters + const ref = requestUrl.searchParams.get('ref') || undefined; + const path = requestUrl.searchParams.get('path') || undefined; + const branch = requestUrl.searchParams.get('branch') || undefined; + const limit = requestUrl.searchParams.get('limit') + ? parseInt(requestUrl.searchParams.get('limit')!, 10) + : undefined; + + return { + userPubkey, + userPubkeyHex, + clientIp, + ref, + path, + branch, + limit + }; +} + +/** + * Validate and extract repository context from route parameters + * + * @param params - Route parameters (from SvelteKit) + * @returns Validated repository context + * @throws Error if validation fails + */ +export function validateRepoParams(params: { npub?: string; repo?: string }): RepoContext { + const { npub, repo } = params; + + if (!npub || !repo) { + throw new Error('Missing npub or repo parameter'); + } + + // Validate and convert npub to pubkey + let repoOwnerPubkey: string; + try { + repoOwnerPubkey = requireNpubHex(npub); + } catch { + throw new Error(`Invalid npub format: ${npub}`); + } + + return { + npub, + repo, + repoOwnerPubkey + }; +} + +/** + * Get combined repository and request context + * Combines parameter validation with request context extraction + * + * @param event - SvelteKit request event + * @param params - Route parameters + * @returns Combined repository and request context + */ +export function getRepoContext( + event: RequestEvent, + params: { npub?: string; repo?: string } +): RepoRequestContext { + const requestContext = extractRequestContext(event); + const repoContext = validateRepoParams(params); + + return { + ...requestContext, + ...repoContext + }; +} + +/** + * Extract user pubkey from request (convenience function) + */ +export function getUserPubkey(event: RequestEvent): string | null { + const url = event.url; + return url.searchParams.get('userPubkey') || + event.request.headers.get('x-user-pubkey') || + null; +} + +/** + * Extract client IP from request (convenience function) + */ +export function getClientIp(event: RequestEvent): string { + try { + return event.getClientAddress(); + } catch { + return event.request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + event.request.headers.get('x-real-ip') || + '127.0.0.1'; + } +} diff --git a/src/lib/utils/api-handlers.ts b/src/lib/utils/api-handlers.ts new file mode 100644 index 0000000..d6399b7 --- /dev/null +++ b/src/lib/utils/api-handlers.ts @@ -0,0 +1,289 @@ +/** + * API Handler Wrappers + * Higher-order functions to wrap SvelteKit request handlers with common logic + */ + +import type { RequestHandler } from '@sveltejs/kit'; +import type { RequestEvent } from '@sveltejs/kit'; +import { handleApiError, handleValidationError, type ErrorContext } from './error-handler.js'; +import { getRepoContext, extractRequestContext, validateRepoParams, type RepoContext, type RequestContext, type RepoRequestContext } from './api-context.js'; +import { requireRepoAccess, requireRepoExists, requireMaintainer, requireNIP98Auth, requireRepoAccessWithExists } from './api-auth.js'; +import { auditLogger } from '../services/security/audit-logger.js'; + +/** + * Handler function that receives validated context + */ +export type RepoHandler = (context: { + repoContext: RepoContext; + requestContext: RequestContext; + event: RequestEvent; +}) => Promise; + +/** + * Handler function with full repo request context + */ +export type RepoRequestHandler = (context: { + repoRequestContext: RepoRequestContext; + event: RequestEvent; +}) => Promise; + +/** + * Options for handler wrappers + */ +export interface HandlerOptions { + operation?: string; + requireAuth?: boolean; + requireMaintainer?: boolean; + requireNIP98?: boolean; + requireRepoExists?: boolean; + requireRepoAccess?: boolean; + auditLog?: boolean; +} + +/** + * Wrap a handler with repository parameter validation + * Validates npub/repo params and converts npub to pubkey + * + * @param handler - Handler function that receives validated context + * @param options - Handler options + * @returns Wrapped SvelteKit RequestHandler + */ +export function withRepoValidation( + handler: RepoHandler, + options: HandlerOptions = {} +): RequestHandler { + return async (event) => { + const { params } = event; + const operation = options.operation || 'unknown'; + + try { + // Validate repo parameters + const repoContext = validateRepoParams(params); + + // Extract request context + const requestContext = extractRequestContext(event); + + // Check if repo exists (if required) + if (options.requireRepoExists !== false) { + requireRepoExists(repoContext, operation); + } + + // Check repository access (if required) + if (options.requireRepoAccess !== false) { + await requireRepoAccess(repoContext, requestContext, operation); + } + + // Check if user is maintainer (if required) + if (options.requireMaintainer) { + await requireMaintainer(repoContext, requestContext, operation); + } + + // Check NIP-98 auth (if required) + if (options.requireNIP98) { + requireNIP98Auth(event.request, event.url, event.request.method, operation); + } + + // Audit logging (if enabled) + if (options.auditLog) { + auditLogger.log({ + user: requestContext.userPubkeyHex || undefined, + ip: requestContext.clientIp, + action: `api.${operation}`, + resource: `${repoContext.npub}/${repoContext.repo}`, + result: 'success' + }); + } + + // Call the handler with validated context + return await handler({ + repoContext, + requestContext, + event + }); + } catch (err) { + // If it's already a Response (from error handlers), return it + if (err instanceof Response) { + return err; + } + + // If it's a SvelteKit HttpError (from error() function), re-throw it + // SvelteKit errors have a status property and body property + if (err && typeof err === 'object' && 'status' in err && 'body' in err) { + throw err; + } + + // Otherwise, wrap in standard error handler + return handleApiError( + err, + { operation, npub: params.npub, repo: params.repo }, + `Failed to process ${operation}` + ); + } + }; +} + +/** + * Create a simple GET handler for repository operations + * Automatically handles validation, access checks, and error handling + * + * @param handler - Handler function that receives repo request context + * @param options - Handler options + * @returns GET RequestHandler + */ +export function createRepoGetHandler( + handler: (context: RepoRequestContext, event: RequestEvent) => Promise, + options: HandlerOptions = {} +): RequestHandler { + return withRepoValidation(async ({ repoContext, requestContext, event }) => { + const repoRequestContext: RepoRequestContext = { + ...repoContext, + ...requestContext + }; + + return await handler(repoRequestContext, event); + }, { + requireRepoExists: true, + requireRepoAccess: true, + ...options + }); +} + +/** + * Create a POST handler with maintainer requirement + * + * @param handler - Handler function that receives repo request context + * @param options - Handler options + * @returns POST RequestHandler + */ +export function createRepoPostHandler( + handler: (context: RepoRequestContext, event: RequestEvent) => Promise, + options: HandlerOptions = {} +): RequestHandler { + return withRepoValidation(async ({ repoContext, requestContext, event }) => { + const repoRequestContext: RepoRequestContext = { + ...repoContext, + ...requestContext + }; + + return await handler(repoRequestContext, event); + }, { + requireRepoExists: true, + requireMaintainer: true, + ...options + }); +} + +/** + * Wrap handler with audit logging + * + * @param handler - Handler function + * @param operation - Operation name for audit log + * @returns Wrapped handler with audit logging + */ +export function withAuditLogging( + handler: RequestHandler, + operation: string +): RequestHandler { + return async (event) => { + const { params } = event; + const requestContext = extractRequestContext(event); + + try { + const response = await handler(event); + + // Log successful operation + if (params.npub && params.repo) { + auditLogger.log({ + user: requestContext.userPubkeyHex || undefined, + ip: requestContext.clientIp, + action: `api.${operation}`, + resource: `${params.npub}/${params.repo}`, + result: 'success', + metadata: { status: response.status } + }); + } + + return response; + } catch (err) { + // Log failed operation + if (params.npub && params.repo) { + auditLogger.log({ + user: requestContext.userPubkeyHex || undefined, + ip: requestContext.clientIp, + action: `api.${operation}`, + resource: `${params.npub}/${params.repo}`, + result: 'failure', + error: err instanceof Error ? err.message : String(err) + }); + } + + throw err; + } + }; +} + +/** + * Create a handler factory for common repository API patterns + * + * @param config - Handler configuration + * @returns RequestHandler factory function + */ +export function createRepoHandler(config: { + get?: (context: RepoRequestContext, event: RequestEvent) => Promise; + post?: (context: RepoRequestContext, event: RequestEvent) => Promise; + put?: (context: RepoRequestContext, event: RequestEvent) => Promise; + delete?: (context: RepoRequestContext, event: RequestEvent) => Promise; + options?: HandlerOptions; +}): { + GET?: RequestHandler; + POST?: RequestHandler; + PUT?: RequestHandler; + DELETE?: RequestHandler; +} { + const handlerOptions = config.options || {}; + + const result: { + GET?: RequestHandler; + POST?: RequestHandler; + PUT?: RequestHandler; + DELETE?: RequestHandler; + } = {}; + + if (config.get) { + result.GET = createRepoGetHandler(config.get, handlerOptions); + } + + if (config.post) { + result.POST = createRepoPostHandler(config.post, handlerOptions); + } + + if (config.put) { + result.PUT = withRepoValidation(async ({ repoContext, requestContext, event }) => { + const repoRequestContext: RepoRequestContext = { + ...repoContext, + ...requestContext + }; + return await config.put!(repoRequestContext, event); + }, { + requireRepoExists: true, + requireMaintainer: true, + ...handlerOptions + }); + } + + if (config.delete) { + result.DELETE = withRepoValidation(async ({ repoContext, requestContext, event }) => { + const repoRequestContext: RepoRequestContext = { + ...repoContext, + ...requestContext + }; + return await config.delete!(repoRequestContext, event); + }, { + requireRepoExists: true, + requireMaintainer: true, + ...handlerOptions + }); + } + + return result; +} diff --git a/src/routes/api/repos/[npub]/[repo]/branch-protection/+server.ts b/src/routes/api/repos/[npub]/[repo]/branch-protection/+server.ts index 03c422f..e3ae43b 100644 --- a/src/routes/api/repos/[npub]/[repo]/branch-protection/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/branch-protection/+server.ts @@ -4,88 +4,47 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit'; -import { BranchProtectionService } from '$lib/services/nostr/branch-protection-service.js'; -import { DEFAULT_NOSTR_RELAYS, combineRelays } from '$lib/config.js'; +import { branchProtectionService, ownershipTransferService, nostrClient } from '$lib/services/service-registry.js'; +import { combineRelays } from '$lib/config.js'; import { getUserRelays } from '$lib/services/nostr/user-relays.js'; -import { OwnershipTransferService } from '$lib/services/nostr/ownership-transfer-service.js'; import { signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; -import { NostrClient } from '$lib/services/nostr/nostr-client.js'; -import { nip19 } from 'nostr-tools'; import type { BranchProtectionRule } from '$lib/services/nostr/branch-protection-service.js'; -import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleAuthError, handleAuthorizationError } from '$lib/utils/error-handler.js'; - -const branchProtectionService = new BranchProtectionService(DEFAULT_NOSTR_RELAYS); -const ownershipTransferService = new OwnershipTransferService(DEFAULT_NOSTR_RELAYS); -const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); +import { createRepoGetHandler, createRepoPostHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; +import { handleApiError, handleValidationError, handleAuthorizationError } from '$lib/utils/error-handler.js'; /** * GET - Get branch protection rules */ -export const GET: RequestHandler = async ({ params }: { params: { npub?: string; repo?: string } }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getBranchProtection' }); - } - - try { - // Decode npub to get pubkey - let ownerPubkey: string; - try { - ownerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getBranchProtection', npub }); - } - - const config = await branchProtectionService.getBranchProtection(ownerPubkey, repo); +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const config = await branchProtectionService.getBranchProtection(context.repoOwnerPubkey, context.repo); if (!config) { return json({ rules: [] }); } return json(config); - } catch (err) { - return handleApiError(err, { operation: 'getBranchProtection', npub, repo }, 'Failed to get branch protection'); - } -}; + }, + { operation: 'getBranchProtection', requireRepoAccess: false } // Branch protection rules are public +); /** * POST - Update branch protection rules */ -export const POST: RequestHandler = async ({ params, request }: { params: { npub?: string; repo?: string }; request: Request }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'updateBranchProtection' }); - } - - try { - const body = await request.json(); - const { userPubkey, rules } = body; - - if (!userPubkey) { - return handleAuthError('Authentication required', { operation: 'updateBranchProtection', npub, repo }); - } +export const POST: RequestHandler = createRepoPostHandler( + async (context: RepoRequestContext, event: RequestEvent) => { + const body = await event.request.json(); + const { rules } = body; if (!Array.isArray(rules)) { - return handleValidationError('Rules must be an array', { operation: 'updateBranchProtection', npub, repo }); + return handleValidationError('Rules must be an array', { operation: 'updateBranchProtection', npub: context.npub, repo: context.repo }); } - // Decode npub to get pubkey - let ownerPubkey: string; - try { - ownerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'updateBranchProtection', npub }); - } - - const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey; - // Check if user is owner - const currentOwner = await ownershipTransferService.getCurrentOwner(ownerPubkey, repo); - if (userPubkeyHex !== currentOwner) { - return handleAuthorizationError('Only the repository owner can update branch protection', { operation: 'updateBranchProtection', npub, repo }); + const currentOwner = await ownershipTransferService.getCurrentOwner(context.repoOwnerPubkey, context.repo); + if (context.userPubkeyHex !== currentOwner) { + return handleAuthorizationError('Only the repository owner can update branch protection', { operation: 'updateBranchProtection', npub: context.npub, repo: context.repo }); } // Validate rules @@ -101,7 +60,7 @@ export const POST: RequestHandler = async ({ params, request }: { params: { npub // Create protection event const protectionEvent = branchProtectionService.createProtectionEvent( currentOwner, - repo, + context.repo, validatedRules ); @@ -114,11 +73,10 @@ export const POST: RequestHandler = async ({ params, request }: { params: { npub const result = await nostrClient.publishEvent(signedEvent, combinedRelays); if (result.success.length === 0) { - return error(500, 'Failed to publish branch protection rules to relays'); + throw handleApiError(new Error('Failed to publish branch protection rules to relays'), { operation: 'updateBranchProtection', npub: context.npub, repo: context.repo }, 'Failed to publish branch protection rules to relays'); } return json({ success: true, event: signedEvent, rules: validatedRules }); - } catch (err) { - return handleApiError(err, { operation: 'updateBranchProtection', npub, repo }, 'Failed to update branch protection'); - } -}; + }, + { operation: 'updateBranchProtection', requireMaintainer: false } // Override to check owner instead +); diff --git a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts index 2735d31..d17dd9b 100644 --- a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts @@ -2,84 +2,33 @@ * API endpoint for getting and creating repository branches */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; // @ts-ignore - SvelteKit generates this type import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; -import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthError, handleAuthorizationError } from '$lib/utils/error-handler.js'; - -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); -const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); - -export const GET: RequestHandler = async ({ params }: { params: { npub?: string; repo?: string } }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getBranches' }); - } - - try { - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'getBranches', npub, repo }); - } - - const branches = await fileManager.getBranches(npub, repo); +import { fileManager } from '$lib/services/service-registry.js'; +import { createRepoGetHandler, createRepoPostHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; +import { handleValidationError } from '$lib/utils/error-handler.js'; + +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const branches = await fileManager.getBranches(context.npub, context.repo); return json(branches); - } catch (err) { - return handleApiError(err, { operation: 'getBranches', npub, repo }, 'Failed to get branches'); - } -}; - -export const POST: RequestHandler = async ({ params, request }: { params: { npub?: string; repo?: string }; request: Request }) => { - const { npub, repo } = params; + }, + { operation: 'getBranches', requireRepoAccess: false } // Branches are public info +); - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'createBranch' }); - } - - let branchName: string | undefined; - let fromBranch: string | undefined; - let userPubkey: string | undefined; - try { - const body = await request.json(); - ({ branchName, fromBranch, userPubkey } = body); +export const POST: RequestHandler = createRepoPostHandler( + async (context: RepoRequestContext, event: RequestEvent) => { + const body = await event.request.json(); + const { branchName, fromBranch } = body; if (!branchName) { - return handleValidationError('Missing branchName parameter', { operation: 'createBranch', npub, repo }); - } - - if (!userPubkey) { - return handleAuthError('Authentication required. Please provide userPubkey.', { operation: 'createBranch', npub, repo }); - } - - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'createBranch', npub, repo }); - } - - // Check if user is a maintainer - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'createBranch', npub }); - } - - // Convert userPubkey to hex if needed - const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey; - - const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo); - if (!isMaintainer) { - return handleAuthorizationError('Only repository maintainers can create branches. Please submit a pull request instead.', { operation: 'createBranch', npub, repo }); + throw handleValidationError('Missing branchName parameter', { operation: 'createBranch', npub: context.npub, repo: context.repo }); } - await fileManager.createBranch(npub, repo, branchName, fromBranch || 'main'); + await fileManager.createBranch(context.npub, context.repo, branchName, fromBranch || 'main'); return json({ success: true, message: 'Branch created successfully' }); - } catch (err) { - return handleApiError(err, { operation: 'createBranch', npub, repo, branchName }, 'Failed to create branch'); - } -}; + }, + { operation: 'createBranch' } +); diff --git a/src/routes/api/repos/[npub]/[repo]/commits/+server.ts b/src/routes/api/repos/[npub]/[repo]/commits/+server.ts index 37de6ff..a43cd2a 100644 --- a/src/routes/api/repos/[npub]/[repo]/commits/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/commits/+server.ts @@ -2,40 +2,20 @@ * API endpoint for getting commit history */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; +import { fileManager } from '$lib/services/service-registry.js'; +import { createRepoGetHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext } from '$lib/utils/api-context.js'; -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); - -export const GET: RequestHandler = async ({ params, url, request }) => { - const { npub, repo } = params; - const branch = url.searchParams.get('branch') || 'main'; - const limit = parseInt(url.searchParams.get('limit') || '50', 10); - const path = url.searchParams.get('path') || undefined; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getCommits' }); - } - - try { - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'getCommits', npub, repo }); - } - - // Check repository privacy - const { checkRepoAccess } = await import('$lib/utils/repo-privacy.js'); - const access = await checkRepoAccess(npub, repo, userPubkey || null); - if (!access.allowed) { - return handleAuthorizationError(access.error || 'Access denied', { operation: 'getCommits', npub, repo }); - } - - const commits = await fileManager.getCommitHistory(npub, repo, branch, limit, path); +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const branch = context.branch || 'main'; + const limit = context.limit || 50; + const path = context.path; + + const commits = await fileManager.getCommitHistory(context.npub, context.repo, branch, limit, path); return json(commits); - } catch (err) { - return handleApiError(err, { operation: 'getCommits', npub, repo, branch }, 'Failed to get commit history'); - } -}; + }, + { operation: 'getCommits' } +); diff --git a/src/routes/api/repos/[npub]/[repo]/diff/+server.ts b/src/routes/api/repos/[npub]/[repo]/diff/+server.ts index 8aecbf0..bf3c21d 100644 --- a/src/routes/api/repos/[npub]/[repo]/diff/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/diff/+server.ts @@ -2,40 +2,25 @@ * API endpoint for getting diffs */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; +import { fileManager } from '$lib/services/service-registry.js'; +import { createRepoGetHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; +import { handleValidationError } from '$lib/utils/error-handler.js'; -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext, event: RequestEvent) => { + const fromRef = event.url.searchParams.get('from'); + const toRef = event.url.searchParams.get('to') || 'HEAD'; + const filePath = event.url.searchParams.get('path') || undefined; -export const GET: RequestHandler = async ({ params, url, request }) => { - const { npub, repo } = params; - const fromRef = url.searchParams.get('from'); - const toRef = url.searchParams.get('to') || 'HEAD'; - const filePath = url.searchParams.get('path') || undefined; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo || !fromRef) { - return handleValidationError('Missing npub, repo, or from parameter', { operation: 'getDiff' }); - } - - try { - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'getDiff', npub, repo }); - } - - // Check repository privacy - const { checkRepoAccess } = await import('$lib/utils/repo-privacy.js'); - const access = await checkRepoAccess(npub, repo, userPubkey || null); - if (!access.allowed) { - return handleAuthorizationError(access.error || 'Access denied', { operation: 'getDiff', npub, repo }); + if (!fromRef) { + throw handleValidationError('Missing from parameter', { operation: 'getDiff', npub: context.npub, repo: context.repo }); } - const diffs = await fileManager.getDiff(npub, repo, fromRef, toRef, filePath); + const diffs = await fileManager.getDiff(context.npub, context.repo, fromRef, toRef, filePath); return json(diffs); - } catch (err) { - return handleApiError(err, { operation: 'getDiff', npub, repo, fromRef, toRef }, 'Failed to get diff'); - } -}; + }, + { operation: 'getDiff' } +); diff --git a/src/routes/api/repos/[npub]/[repo]/download/+server.ts b/src/routes/api/repos/[npub]/[repo]/download/+server.ts index c6589e1..06f3e2e 100644 --- a/src/routes/api/repos/[npub]/[repo]/download/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/download/+server.ts @@ -2,85 +2,61 @@ * API endpoint for downloading repository as ZIP */ -import { error } from '@sveltejs/kit'; +import { error, json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; -import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { requireNpubHex } from '$lib/utils/npub-utils.js'; +import { fileManager } from '$lib/services/service-registry.js'; +import { createRepoGetHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; import { spawn } from 'child_process'; import { mkdir, rm, readFile } from 'fs/promises'; -import { existsSync } from 'fs'; import { join, resolve } from 'path'; import logger from '$lib/services/logger.js'; import { isValidBranchName, sanitizeError } from '$lib/utils/security.js'; import simpleGit from 'simple-git'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); -const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); - -export const GET: RequestHandler = async ({ params, url, request }) => { - const { npub, repo } = params; - const ref = url.searchParams.get('ref') || 'HEAD'; - const format = url.searchParams.get('format') || 'zip'; // zip or tar.gz - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo) { - return error(400, 'Missing npub or repo parameter'); - } - - try { - if (!fileManager.repoExists(npub, repo)) { - return error(404, 'Repository not found'); - } +import { handleApiError } from '$lib/utils/error-handler.js'; - // Check repository privacy - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return error(400, 'Invalid npub format'); - } +const repoRoot = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT + ? process.env.GIT_REPO_ROOT + : '/repos'; - const canView = await maintainerService.canView(userPubkey || null, repoOwnerPubkey, repo); - if (!canView) { - return error(403, 'This repository is private. Only owners and maintainers can view it.'); - } +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext, event: RequestEvent) => { + const ref = event.url.searchParams.get('ref') || 'HEAD'; + const format = event.url.searchParams.get('format') || 'zip'; // zip or tar.gz // Security: Validate ref to prevent command injection if (ref !== 'HEAD' && !isValidBranchName(ref)) { - return error(400, 'Invalid ref format'); + throw error(400, 'Invalid ref format'); } // Security: Validate format if (format !== 'zip' && format !== 'tar.gz') { - return error(400, 'Invalid format. Must be "zip" or "tar.gz"'); + throw error(400, 'Invalid format. Must be "zip" or "tar.gz"'); } - const repoPath = join(repoRoot, npub, `${repo}.git`); + const repoPath = join(repoRoot, context.npub, `${context.repo}.git`); // Security: Ensure resolved path is within repoRoot const resolvedRepoPath = resolve(repoPath).replace(/\\/g, '/'); const resolvedRoot = resolve(repoRoot).replace(/\\/g, '/'); if (!resolvedRepoPath.startsWith(resolvedRoot + '/')) { - return error(403, 'Invalid repository path'); + throw error(403, 'Invalid repository path'); } const tempDir = join(repoRoot, '..', 'temp-downloads'); - const workDir = join(tempDir, `${npub}-${repo}-${Date.now()}`); + const workDir = join(tempDir, `${context.npub}-${context.repo}-${Date.now()}`); // Security: Ensure workDir is within tempDir const resolvedWorkDir = resolve(workDir).replace(/\\/g, '/'); const resolvedTempDir = resolve(tempDir).replace(/\\/g, '/'); if (!resolvedWorkDir.startsWith(resolvedTempDir + '/')) { - return error(500, 'Invalid work directory path'); + throw error(500, 'Invalid work directory path'); } - const archiveName = `${repo}-${ref}.${format === 'tar.gz' ? 'tar.gz' : 'zip'}`; + const archiveName = `${context.repo}-${ref}.${format === 'tar.gz' ? 'tar.gz' : 'zip'}`; const archivePath = join(tempDir, archiveName); // Security: Ensure archive path is within tempDir const resolvedArchivePath = resolve(archivePath).replace(/\\/g, '/'); if (!resolvedArchivePath.startsWith(resolvedTempDir + '/')) { - return error(500, 'Invalid archive path'); + throw error(500, 'Invalid archive path'); } try { @@ -158,10 +134,9 @@ export const GET: RequestHandler = async ({ params, url, request }) => { await rm(workDir, { recursive: true, force: true }).catch(() => {}); await rm(archivePath, { force: true }).catch(() => {}); const sanitizedError = sanitizeError(archiveError); - logger.error({ error: sanitizedError, npub, repo, ref, format }, 'Error creating archive'); + logger.error({ error: sanitizedError, npub: context.npub, repo: context.repo, ref, format }, 'Error creating archive'); throw archiveError; } - } catch (err) { - return handleApiError(err, { operation: 'download', npub, repo, ref, format }, 'Failed to create repository archive'); - } -}; + }, + { operation: 'download' } +); diff --git a/src/routes/api/repos/[npub]/[repo]/highlights/+server.ts b/src/routes/api/repos/[npub]/[repo]/highlights/+server.ts index 15bfe3d..9415563 100644 --- a/src/routes/api/repos/[npub]/[repo]/highlights/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/highlights/+server.ts @@ -4,44 +4,27 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { HighlightsService } from '$lib/services/nostr/highlights-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js'; +import { highlightsService, nostrClient } from '$lib/services/service-registry.js'; +import { DEFAULT_NOSTR_RELAYS, combineRelays } from '$lib/config.js'; +import { getUserRelays } from '$lib/services/nostr/user-relays.js'; import { verifyEvent } from 'nostr-tools'; import type { NostrEvent } from '$lib/types/nostr.js'; -import { combineRelays } from '$lib/config.js'; -import { getUserRelays } from '$lib/services/nostr/user-relays.js'; -import { NostrClient } from '$lib/services/nostr/nostr-client.js'; +import { createRepoGetHandler, withRepoValidation } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; import { handleApiError, handleValidationError } from '$lib/utils/error-handler.js'; - -const highlightsService = new HighlightsService(DEFAULT_NOSTR_RELAYS); -const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); +import { decodeNpubToHex } from '$lib/utils/npub-utils.js'; /** * GET - Get highlights for a pull request * Query params: prId, prAuthor */ -export const GET: RequestHandler = async ({ params, url }) => { - const { npub, repo } = params; - const prId = url.searchParams.get('prId'); - const prAuthor = url.searchParams.get('prAuthor'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getHighlights' }); - } +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext, event: RequestEvent) => { + const prId = event.url.searchParams.get('prId'); + const prAuthor = event.url.searchParams.get('prAuthor'); - if (!prId || !prAuthor) { - return handleValidationError('Missing prId or prAuthor parameter', { operation: 'getHighlights', npub, repo }); - } - - try { - // Decode npub to get pubkey - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getHighlights', npub }); + if (!prId || !prAuthor) { + return handleValidationError('Missing prId or prAuthor parameter', { operation: 'getHighlights', npub: context.npub, repo: context.repo }); } // Decode prAuthor if it's an npub @@ -51,8 +34,8 @@ export const GET: RequestHandler = async ({ params, url }) => { const highlights = await highlightsService.getHighlightsForPR( prId, prAuthorPubkey, - repoOwnerPubkey, - repo + context.repoOwnerPubkey, + context.repo ); // Also get top-level comments on the PR @@ -62,55 +45,47 @@ export const GET: RequestHandler = async ({ params, url }) => { highlights, comments: prComments }); - } catch (err) { - return handleApiError(err, { operation: 'getHighlights', npub, repo, prId }, 'Failed to fetch highlights'); - } -}; + }, + { operation: 'getHighlights', requireRepoAccess: false } // Highlights are public +); /** * POST - Create a highlight or comment * Body: { type: 'highlight' | 'comment', event, userPubkey } */ -export const POST: RequestHandler = async ({ params, request }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'createHighlight' }); - } - - try { - const body = await request.json(); - const { type, event, userPubkey } = body; +export const POST: RequestHandler = withRepoValidation( + async ({ repoContext, requestContext, event }) => { + const body = await event.request.json(); + const { type, event: highlightEvent, userPubkey } = body; - if (!type || !event || !userPubkey) { - return handleValidationError('Missing type, event, or userPubkey in request body', { operation: 'createHighlight', npub, repo }); + if (!type || !highlightEvent || !userPubkey) { + throw handleValidationError('Missing type, event, or userPubkey in request body', { operation: 'createHighlight', npub: repoContext.npub, repo: repoContext.repo }); } if (type !== 'highlight' && type !== 'comment') { - return handleValidationError('Type must be "highlight" or "comment"', { operation: 'createHighlight', npub, repo }); + throw handleValidationError('Type must be "highlight" or "comment"', { operation: 'createHighlight', npub: repoContext.npub, repo: repoContext.repo }); } // Verify the event is properly signed - if (!event.sig || !event.id) { - return handleValidationError('Invalid event: missing signature or ID', { operation: 'createHighlight', npub, repo }); + if (!highlightEvent.sig || !highlightEvent.id) { + throw handleValidationError('Invalid event: missing signature or ID', { operation: 'createHighlight', npub: repoContext.npub, repo: repoContext.repo }); } - if (!verifyEvent(event)) { - return handleValidationError('Invalid event signature', { operation: 'createHighlight', npub, repo }); + if (!verifyEvent(highlightEvent)) { + throw handleValidationError('Invalid event signature', { operation: 'createHighlight', npub: repoContext.npub, repo: repoContext.repo }); } // Get user's relays and publish const { outbox } = await getUserRelays(userPubkey, nostrClient); const combinedRelays = combineRelays(outbox); - const result = await highlightsService['nostrClient'].publishEvent(event as NostrEvent, combinedRelays); + const result = await nostrClient.publishEvent(highlightEvent as NostrEvent, combinedRelays); if (result.failed.length > 0 && result.success.length === 0) { - return error(500, 'Failed to publish to all relays'); + throw handleApiError(new Error('Failed to publish to all relays'), { operation: 'createHighlight', npub: repoContext.npub, repo: repoContext.repo }, 'Failed to publish to all relays'); } - return json({ success: true, event, published: result }); - } catch (err) { - return handleApiError(err, { operation: 'createHighlight', npub, repo }, 'Failed to create highlight/comment'); - } -}; + return json({ success: true, event: highlightEvent, published: result }); + }, + { operation: 'createHighlight', requireRepoAccess: false } // Highlights can be created by anyone +); diff --git a/src/routes/api/repos/[npub]/[repo]/issues/+server.ts b/src/routes/api/repos/[npub]/[repo]/issues/+server.ts index 7c75f9f..b34c360 100644 --- a/src/routes/api/repos/[npub]/[repo]/issues/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/issues/+server.ts @@ -2,79 +2,44 @@ * API endpoint for Issues (NIP-34 kind 1621) */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { IssuesService } from '$lib/services/nostr/issues-service.js'; +import { issuesService, nostrClient } from '$lib/services/service-registry.js'; +import { createRepoGetHandler, withRepoValidation } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; +import { handleValidationError, handleApiError } from '$lib/utils/error-handler.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; -export const GET: RequestHandler = async ({ params, url, request }) => { - const { npub, repo } = params; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getIssues' }); - } - - try { - // Convert npub to pubkey - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getIssues', npub }); - } - - // Check repository privacy - const { checkRepoAccess } = await import('$lib/utils/repo-privacy.js'); - const access = await checkRepoAccess(npub, repo, userPubkey || null); - if (!access.allowed) { - return handleAuthorizationError(access.error || 'Access denied', { operation: 'getIssues', npub, repo }); - } - - const issuesService = new IssuesService(DEFAULT_NOSTR_RELAYS); - const issues = await issuesService.getIssues(repoOwnerPubkey, repo); - +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const issues = await issuesService.getIssues(context.repoOwnerPubkey, context.repo); return json(issues); - } catch (err) { - return handleApiError(err, { operation: 'getIssues', npub, repo }, 'Failed to fetch issues'); - } -}; - -export const POST: RequestHandler = async ({ params, request }) => { - // For creating issues, we accept a pre-signed event from the client - // since NIP-07 signing must happen client-side - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'createIssue' }); - } + }, + { operation: 'getIssues' } +); - try { - const body = await request.json(); - const { event } = body; +export const POST: RequestHandler = withRepoValidation( + async ({ repoContext, requestContext, event }) => { + const body = await event.request.json(); + const { event: issueEvent } = body; - if (!event) { - return handleValidationError('Missing event in request body', { operation: 'createIssue', npub, repo }); + if (!issueEvent) { + throw handleValidationError('Missing event in request body', { operation: 'createIssue', npub: repoContext.npub, repo: repoContext.repo }); } // Verify the event is properly signed (basic check) - if (!event.sig || !event.id) { - return handleValidationError('Invalid event: missing signature or ID', { operation: 'createIssue', npub, repo }); + if (!issueEvent.sig || !issueEvent.id) { + throw handleValidationError('Invalid event: missing signature or ID', { operation: 'createIssue', npub: repoContext.npub, repo: repoContext.repo }); } // Publish the event to relays - const issuesService = new IssuesService(DEFAULT_NOSTR_RELAYS); - const result = await issuesService['nostrClient'].publishEvent(event, DEFAULT_NOSTR_RELAYS); + const result = await nostrClient.publishEvent(issueEvent, DEFAULT_NOSTR_RELAYS); if (result.failed.length > 0 && result.success.length === 0) { - return handleApiError(new Error('Failed to publish issue to all relays'), { operation: 'createIssue', npub, repo }, 'Failed to publish issue to all relays'); + throw handleApiError(new Error('Failed to publish issue to all relays'), { operation: 'createIssue', npub: repoContext.npub, repo: repoContext.repo }, 'Failed to publish issue to all relays'); } - return json({ success: true, event, published: result }); - } catch (err) { - return handleApiError(err, { operation: 'createIssue', npub, repo }, 'Failed to create issue'); - } -}; + return json({ success: true, event: issueEvent, published: result }); + }, + { operation: 'createIssue', requireRepoAccess: false } // Issues can be created by anyone with access +); diff --git a/src/routes/api/repos/[npub]/[repo]/maintainers/+server.ts b/src/routes/api/repos/[npub]/[repo]/maintainers/+server.ts index 8410b7a..defcaa4 100644 --- a/src/routes/api/repos/[npub]/[repo]/maintainers/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/maintainers/+server.ts @@ -2,51 +2,29 @@ * API endpoint for checking maintainer status */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; // @ts-ignore - SvelteKit generates this type import type { RequestHandler } from './$types'; -import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError } from '$lib/utils/error-handler.js'; +import { maintainerService } from '$lib/services/service-registry.js'; +import { createRepoGetHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext } from '$lib/utils/api-context.js'; -const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); - -export const GET: RequestHandler = async ({ params, url }: { params: { npub?: string; repo?: string }; url: URL }) => { - const { npub, repo } = params; - const userPubkey = url.searchParams.get('userPubkey'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getMaintainers' }); - } - - try { - // Convert npub to pubkey - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getMaintainers', npub }); - } - - const { maintainers, owner } = await maintainerService.getMaintainers(repoOwnerPubkey, repo); +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const { maintainers, owner } = await maintainerService.getMaintainers(context.repoOwnerPubkey, context.repo); // If userPubkey provided, check if they're a maintainer - if (userPubkey) { - const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey; - - const isMaintainer = maintainers.includes(userPubkeyHex); + if (context.userPubkeyHex) { + const isMaintainer = maintainers.includes(context.userPubkeyHex); return json({ maintainers, owner, isMaintainer, - userPubkey: userPubkeyHex + userPubkey: context.userPubkeyHex }); } return json({ maintainers, owner }); - } catch (err) { - return handleApiError(err, { operation: 'getMaintainers', npub, repo }, 'Failed to check maintainers'); - } -}; + }, + { operation: 'getMaintainers', requireRepoAccess: false } // Maintainer list is public info +); diff --git a/src/routes/api/repos/[npub]/[repo]/prs/+server.ts b/src/routes/api/repos/[npub]/[repo]/prs/+server.ts index cf8b391..417a26e 100644 --- a/src/routes/api/repos/[npub]/[repo]/prs/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/prs/+server.ts @@ -2,79 +2,45 @@ * API endpoint for Pull Requests (NIP-34 kind 1618) */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; // @ts-ignore - SvelteKit generates this type import type { RequestHandler } from './$types'; -import { PRsService } from '$lib/services/nostr/prs-service.js'; +import { prsService, nostrClient } from '$lib/services/service-registry.js'; +import { createRepoGetHandler, withRepoValidation } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; +import { handleValidationError, handleApiError } from '$lib/utils/error-handler.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; -export const GET: RequestHandler = async ({ params, url, request }: { params: { npub?: string; repo?: string }; url: URL; request: Request }) => { - const { npub, repo } = params; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getPRs' }); - } - - try { - // Convert npub to pubkey - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getPRs', npub }); - } - - // Check repository privacy - const { checkRepoAccess } = await import('$lib/utils/repo-privacy.js'); - const access = await checkRepoAccess(npub, repo, userPubkey || null); - if (!access.allowed) { - return handleAuthorizationError(access.error || 'Access denied', { operation: 'getPRs', npub, repo }); - } - - const prsService = new PRsService(DEFAULT_NOSTR_RELAYS); - const prs = await prsService.getPullRequests(repoOwnerPubkey, repo); - +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const prs = await prsService.getPullRequests(context.repoOwnerPubkey, context.repo); return json(prs); - } catch (err) { - return handleApiError(err, { operation: 'getPRs', npub, repo }, 'Failed to fetch pull requests'); - } -}; - -export const POST: RequestHandler = async ({ params, request }: { params: { npub?: string; repo?: string }; request: Request }) => { - // For creating PRs, we accept a pre-signed event from the client - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'createPR' }); - } + }, + { operation: 'getPRs' } +); - try { - const body = await request.json(); - const { event } = body; +export const POST: RequestHandler = withRepoValidation( + async ({ repoContext, requestContext, event }) => { + const body = await event.request.json(); + const { event: prEvent } = body; - if (!event) { - return handleValidationError('Missing event in request body', { operation: 'createPR', npub, repo }); + if (!prEvent) { + throw handleValidationError('Missing event in request body', { operation: 'createPR', npub: repoContext.npub, repo: repoContext.repo }); } // Verify the event is properly signed - if (!event.sig || !event.id) { - return handleValidationError('Invalid event: missing signature or ID', { operation: 'createPR', npub, repo }); + if (!prEvent.sig || !prEvent.id) { + throw handleValidationError('Invalid event: missing signature or ID', { operation: 'createPR', npub: repoContext.npub, repo: repoContext.repo }); } // Publish the event to relays - const prsService = new PRsService(DEFAULT_NOSTR_RELAYS); - const result = await prsService['nostrClient'].publishEvent(event, DEFAULT_NOSTR_RELAYS); + const result = await nostrClient.publishEvent(prEvent, DEFAULT_NOSTR_RELAYS); if (result.failed.length > 0 && result.success.length === 0) { - return handleApiError(new Error('Failed to publish pull request to all relays'), { operation: 'createPR', npub, repo }, 'Failed to publish pull request to all relays'); + throw handleApiError(new Error('Failed to publish pull request to all relays'), { operation: 'createPR', npub: repoContext.npub, repo: repoContext.repo }, 'Failed to publish pull request to all relays'); } - return json({ success: true, event, published: result }); - } catch (err) { - return handleApiError(err, { operation: 'createPR', npub, repo }, 'Failed to create pull request'); - } -}; + return json({ success: true, event: prEvent, published: result }); + }, + { operation: 'createPR', requireRepoAccess: false } // PRs can be created by anyone with access +); diff --git a/src/routes/api/repos/[npub]/[repo]/raw/+server.ts b/src/routes/api/repos/[npub]/[repo]/raw/+server.ts index 459c51b..93c4ab7 100644 --- a/src/routes/api/repos/[npub]/[repo]/raw/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/raw/+server.ts @@ -2,49 +2,23 @@ * API endpoint for raw file access */ -import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; -import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; - -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); -const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); - -export const GET: RequestHandler = async ({ params, url, request }) => { - const { npub, repo } = params; - const filePath = url.searchParams.get('path'); - const ref = url.searchParams.get('ref') || 'HEAD'; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo || !filePath) { - return handleValidationError('Missing npub, repo, or path parameter', { operation: 'getRawFile' }); - } - - try { - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'getRawFile', npub, repo }); - } - - // Check repository privacy - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getRawFile', npub }); - } - - const canView = await maintainerService.canView(userPubkey || null, repoOwnerPubkey, repo); - if (!canView) { - return handleAuthorizationError('This repository is private. Only owners and maintainers can view it.', { operation: 'getRawFile', npub, repo }); +import { fileManager } from '$lib/services/service-registry.js'; +import { createRepoGetHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; +import { handleValidationError } from '$lib/utils/error-handler.js'; + +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext, event: RequestEvent) => { + const filePath = context.path || event.url.searchParams.get('path'); + const ref = context.ref || 'HEAD'; + + if (!filePath) { + throw handleValidationError('Missing path parameter', { operation: 'getRawFile', npub: context.npub, repo: context.repo }); } // Get file content - const fileData = await fileManager.getFileContent(npub, repo, filePath, ref); + const fileData = await fileManager.getFileContent(context.npub, context.repo, filePath, ref); // Determine content type based on file extension const ext = filePath.split('.').pop()?.toLowerCase(); @@ -78,7 +52,6 @@ export const GET: RequestHandler = async ({ params, url, request }) => { 'Cache-Control': 'public, max-age=3600' } }); - } catch (err) { - return handleApiError(err, { operation: 'getRawFile', npub, repo, filePath }, 'Failed to get raw file'); - } -}; + }, + { operation: 'getRawFile' } +); diff --git a/src/routes/api/repos/[npub]/[repo]/readme/+server.ts b/src/routes/api/repos/[npub]/[repo]/readme/+server.ts index 541695f..dd07f6b 100644 --- a/src/routes/api/repos/[npub]/[repo]/readme/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/readme/+server.ts @@ -2,18 +2,11 @@ * API endpoint for getting README content */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; -import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; - -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); -const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); +import { fileManager } from '$lib/services/service-registry.js'; +import { createRepoGetHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext } from '$lib/utils/api-context.js'; const README_PATTERNS = [ 'README.md', @@ -26,33 +19,10 @@ const README_PATTERNS = [ 'readme' ]; -export const GET: RequestHandler = async ({ params, url, request }) => { - const { npub, repo } = params; - const ref = url.searchParams.get('ref') || 'HEAD'; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getReadme' }); - } - - try { - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'getReadme', npub, repo }); - } - - // Check repository privacy - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getReadme', npub }); - } - - const canView = await maintainerService.canView(userPubkey || null, repoOwnerPubkey, repo); - if (!canView) { - return handleAuthorizationError('This repository is private. Only owners and maintainers can view it.', { operation: 'getReadme', npub, repo }); - } - +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const ref = context.ref || 'HEAD'; + // Try to find README file let readmeContent: string | null = null; let readmePath: string | null = null; @@ -60,14 +30,14 @@ export const GET: RequestHandler = async ({ params, url, request }) => { for (const pattern of README_PATTERNS) { try { // Try root directory first - const content = await fileManager.getFileContent(npub, repo, pattern, ref); + const content = await fileManager.getFileContent(context.npub, context.repo, pattern, ref); readmeContent = content.content; readmePath = pattern; break; } catch { // Try in root directory with different paths try { - const content = await fileManager.getFileContent(npub, repo, `/${pattern}`, ref); + const content = await fileManager.getFileContent(context.npub, context.repo, `/${pattern}`, ref); readmeContent = content.content; readmePath = `/${pattern}`; break; @@ -87,7 +57,6 @@ export const GET: RequestHandler = async ({ params, url, request }) => { path: readmePath, isMarkdown: readmePath?.toLowerCase().endsWith('.md') || readmePath?.toLowerCase().endsWith('.markdown') }); - } catch (err) { - return handleApiError(err, { operation: 'getReadme', npub, repo }, 'Failed to get README'); - } -}; + }, + { operation: 'getReadme' } +); diff --git a/src/routes/api/repos/[npub]/[repo]/settings/+server.ts b/src/routes/api/repos/[npub]/[repo]/settings/+server.ts index b889ff0..052ca08 100644 --- a/src/routes/api/repos/[npub]/[repo]/settings/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/settings/+server.ts @@ -4,52 +4,28 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { NostrClient } from '$lib/services/nostr/nostr-client.js'; -import { DEFAULT_NOSTR_RELAYS, combineRelays, getGitUrl } from '$lib/config.js'; +import { nostrClient, maintainerService, ownershipTransferService } from '$lib/services/service-registry.js'; +import { DEFAULT_NOSTR_RELAYS, combineRelays } from '$lib/config.js'; import { getUserRelays } from '$lib/services/nostr/user-relays.js'; import { KIND } from '$lib/types/nostr.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js'; import { signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; -import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; -import logger from '$lib/services/logger.js'; -import { OwnershipTransferService } from '$lib/services/nostr/ownership-transfer-service.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthError, handleAuthorizationError } from '$lib/utils/error-handler.js'; - -const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); -const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); -const ownershipTransferService = new OwnershipTransferService(DEFAULT_NOSTR_RELAYS); +import { createRepoGetHandler, withRepoValidation } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; +import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; /** * GET - Get repository settings */ -export const GET: RequestHandler = async ({ params, url, request }) => { - const { npub, repo } = params; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getSettings' }); - } - - try { - // Decode npub to get pubkey - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getSettings', npub }); - } - +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { // Check if user is owner - if (!userPubkey) { - return handleAuthError('Authentication required', { operation: 'getSettings', npub, repo }); + if (!context.userPubkeyHex) { + throw handleApiError(new Error('Authentication required'), { operation: 'getSettings', npub: context.npub, repo: context.repo }, 'Authentication required'); } - const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey; - - const currentOwner = await ownershipTransferService.getCurrentOwner(repoOwnerPubkey, repo); - if (userPubkeyHex !== currentOwner) { - return handleAuthorizationError('Only the repository owner can access settings', { operation: 'getSettings', npub, repo }); + const currentOwner = await ownershipTransferService.getCurrentOwner(context.repoOwnerPubkey, context.repo); + if (context.userPubkeyHex !== currentOwner) { + throw handleAuthorizationError('Only the repository owner can access settings', { operation: 'getSettings', npub: context.npub, repo: context.repo }); } // Get repository announcement @@ -57,17 +33,17 @@ export const GET: RequestHandler = async ({ params, url, request }) => { { kinds: [KIND.REPO_ANNOUNCEMENT], authors: [currentOwner], - '#d': [repo], + '#d': [context.repo], limit: 1 } ]); if (events.length === 0) { - return handleNotFoundError('Repository announcement not found', { operation: 'getSettings', npub, repo }); + throw handleNotFoundError('Repository announcement not found', { operation: 'getSettings', npub: context.npub, repo: context.repo }); } const announcement = events[0]; - const name = announcement.tags.find(t => t[0] === 'name')?.[1] || repo; + const name = announcement.tags.find(t => t[0] === 'name')?.[1] || context.repo; const description = announcement.tags.find(t => t[0] === 'description')?.[1] || ''; const cloneUrls = announcement.tags .filter(t => t[0] === 'clone') @@ -77,7 +53,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => { .filter(t => t[0] === 'maintainers') .flatMap(t => t.slice(1)) .filter(m => m && typeof m === 'string') as string[]; - const privacyInfo = await maintainerService.getPrivacyInfo(currentOwner, repo); + const privacyInfo = await maintainerService.getPrivacyInfo(currentOwner, context.repo); const isPrivate = privacyInfo.isPrivate; return json({ @@ -87,45 +63,28 @@ export const GET: RequestHandler = async ({ params, url, request }) => { maintainers, isPrivate, owner: currentOwner, - npub + npub: context.npub }); - } catch (err) { - return handleApiError(err, { operation: 'getSettings', npub, repo }, 'Failed to get repository settings'); - } -}; + }, + { operation: 'getSettings', requireRepoAccess: false } // Override to check owner instead +); /** * POST - Update repository settings */ -export const POST: RequestHandler = async ({ params, request }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'updateSettings' }); - } - - try { - const body = await request.json(); - const { userPubkey, name, description, cloneUrls, maintainers, isPrivate } = body; - - if (!userPubkey) { - return handleAuthError('Authentication required', { operation: 'updateSettings', npub, repo }); - } - - // Decode npub to get pubkey - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'updateSettings', npub }); +export const POST: RequestHandler = withRepoValidation( + async ({ repoContext, requestContext, event }) => { + if (!requestContext.userPubkeyHex) { + throw handleApiError(new Error('Authentication required'), { operation: 'updateSettings', npub: repoContext.npub, repo: repoContext.repo }, 'Authentication required'); } - const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey; + const body = await event.request.json(); + const { name, description, cloneUrls, maintainers, isPrivate } = body; // Check if user is owner - const currentOwner = await ownershipTransferService.getCurrentOwner(repoOwnerPubkey, repo); - if (userPubkeyHex !== currentOwner) { - return handleAuthorizationError('Only the repository owner can update settings', { operation: 'updateSettings', npub, repo }); + const currentOwner = await ownershipTransferService.getCurrentOwner(repoContext.repoOwnerPubkey, repoContext.repo); + if (requestContext.userPubkeyHex !== currentOwner) { + throw handleAuthorizationError('Only the repository owner can update settings', { operation: 'updateSettings', npub: repoContext.npub, repo: repoContext.repo }); } // Get existing announcement @@ -133,13 +92,13 @@ export const POST: RequestHandler = async ({ params, request }) => { { kinds: [KIND.REPO_ANNOUNCEMENT], authors: [currentOwner], - '#d': [repo], + '#d': [repoContext.repo], limit: 1 } ]); if (events.length === 0) { - return handleNotFoundError('Repository announcement not found', { operation: 'updateSettings', npub, repo }); + throw handleNotFoundError('Repository announcement not found', { operation: 'updateSettings', npub: repoContext.npub, repo: repoContext.repo }); } const existingAnnouncement = events[0]; @@ -148,11 +107,11 @@ export const POST: RequestHandler = async ({ params, request }) => { const gitDomain = process.env.GIT_DOMAIN || 'localhost:6543'; const isLocalhost = gitDomain.startsWith('localhost') || gitDomain.startsWith('127.0.0.1'); const protocol = isLocalhost ? 'http' : 'https'; - const gitUrl = `${protocol}://${gitDomain}/${npub}/${repo}.git`; + const gitUrl = `${protocol}://${gitDomain}/${repoContext.npub}/${repoContext.repo}.git`; // Get Tor .onion URL if available const { getTorGitUrl } = await import('$lib/services/tor/hidden-service.js'); - const torOnionUrl = await getTorGitUrl(npub, repo); + const torOnionUrl = await getTorGitUrl(repoContext.npub, repoContext.repo); // Filter user-provided clone URLs (exclude localhost and .onion duplicates) const userCloneUrls = (cloneUrls || []).filter((url: string) => { @@ -181,12 +140,12 @@ export const POST: RequestHandler = async ({ params, request }) => { // Validate: If using localhost, require either Tor .onion URL or at least one other clone URL if (isLocalhost && !torOnionUrl && userCloneUrls.length === 0) { - return error(400, 'Cannot update with only localhost. You need either a Tor .onion address or at least one other clone URL.'); + throw error(400, 'Cannot update with only localhost. You need either a Tor .onion address or at least one other clone URL.'); } const tags: string[][] = [ - ['d', repo], - ['name', name || repo], + ['d', repoContext.repo], + ['name', name || repoContext.repo], ...(description ? [['description', description]] : []), ['clone', ...cloneUrlList], ['relays', ...DEFAULT_NOSTR_RELAYS], @@ -220,11 +179,10 @@ export const POST: RequestHandler = async ({ params, request }) => { const result = await nostrClient.publishEvent(signedEvent, combinedRelays); if (result.success.length === 0) { - return error(500, 'Failed to publish updated announcement to relays'); + throw error(500, 'Failed to publish updated announcement to relays'); } return json({ success: true, event: signedEvent }); - } catch (err) { - return handleApiError(err, { operation: 'updateSettings', npub, repo }, 'Failed to update repository settings'); - } -}; + }, + { operation: 'updateSettings', requireRepoAccess: false } // Override to check owner instead +); diff --git a/src/routes/api/repos/[npub]/[repo]/tags/+server.ts b/src/routes/api/repos/[npub]/[repo]/tags/+server.ts index 4bf0a50..40900d3 100644 --- a/src/routes/api/repos/[npub]/[repo]/tags/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/tags/+server.ts @@ -2,93 +2,33 @@ * API endpoint for getting and creating tags */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; // @ts-ignore - SvelteKit generates this type import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; -import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthError, handleAuthorizationError } from '$lib/utils/error-handler.js'; - -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); -const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); - -export const GET: RequestHandler = async ({ params, url, request }: { params: { npub?: string; repo?: string }; url: URL; request: Request }) => { - const { npub, repo } = params; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getTags' }); - } - - try { - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'getTags', npub, repo }); - } - - // Check repository privacy - const { checkRepoAccess } = await import('$lib/utils/repo-privacy.js'); - const access = await checkRepoAccess(npub, repo, userPubkey || null); - if (!access.allowed) { - return handleAuthorizationError(access.error || 'Access denied', { operation: 'getTags', npub, repo }); - } - - const tags = await fileManager.getTags(npub, repo); +import { fileManager } from '$lib/services/service-registry.js'; +import { createRepoGetHandler, createRepoPostHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; +import { handleValidationError } from '$lib/utils/error-handler.js'; + +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const tags = await fileManager.getTags(context.npub, context.repo); return json(tags); - } catch (err) { - return handleApiError(err, { operation: 'getTags', npub, repo }, 'Failed to get tags'); - } -}; - -export const POST: RequestHandler = async ({ params, request }: { params: { npub?: string; repo?: string }; request: Request }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'createTag' }); - } + }, + { operation: 'getTags' } +); - let tagName: string | undefined; - let ref: string | undefined; - let message: string | undefined; - let userPubkey: string | undefined; - try { - const body = await request.json(); - ({ tagName, ref, message, userPubkey } = body); +export const POST: RequestHandler = createRepoPostHandler( + async (context: RepoRequestContext, event: RequestEvent) => { + const body = await event.request.json(); + const { tagName, ref, message } = body; if (!tagName) { - return handleValidationError('Missing tagName parameter', { operation: 'createTag', npub, repo }); - } - - if (!userPubkey) { - return handleAuthError('Authentication required. Please provide userPubkey.', { operation: 'createTag', npub, repo }); - } - - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'createTag', npub, repo }); - } - - // Check if user is a maintainer - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'createTag', npub }); - } - - // Convert userPubkey to hex if needed - const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey; - - const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo); - if (!isMaintainer) { - return handleAuthorizationError('Only repository maintainers can create tags.', { operation: 'createTag', npub, repo }); + throw handleValidationError('Missing tagName parameter', { operation: 'createTag', npub: context.npub, repo: context.repo }); } - await fileManager.createTag(npub, repo, tagName, ref || 'HEAD', message); + await fileManager.createTag(context.npub, context.repo, tagName, ref || 'HEAD', message); return json({ success: true, message: 'Tag created successfully' }); - } catch (err) { - return handleApiError(err, { operation: 'createTag', npub, repo, tagName }, 'Failed to create tag'); - } -}; + }, + { operation: 'createTag' } +); diff --git a/src/routes/api/repos/[npub]/[repo]/transfer/+server.ts b/src/routes/api/repos/[npub]/[repo]/transfer/+server.ts index 8ffd53b..09c5d4c 100644 --- a/src/routes/api/repos/[npub]/[repo]/transfer/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/transfer/+server.ts @@ -4,44 +4,27 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { OwnershipTransferService } from '$lib/services/nostr/ownership-transfer-service.js'; -import { NostrClient } from '$lib/services/nostr/nostr-client.js'; -import { DEFAULT_NOSTR_RELAYS, combineRelays } from '$lib/config.js'; +import { ownershipTransferService, nostrClient } from '$lib/services/service-registry.js'; +import { combineRelays } from '$lib/config.js'; import { KIND } from '$lib/types/nostr.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js'; import { verifyEvent } from 'nostr-tools'; import type { NostrEvent } from '$lib/types/nostr.js'; import { getUserRelays } from '$lib/services/nostr/user-relays.js'; -import { handleApiError, handleValidationError, handleAuthError, handleAuthorizationError } from '$lib/utils/error-handler.js'; - -const ownershipTransferService = new OwnershipTransferService(DEFAULT_NOSTR_RELAYS); -const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); +import { createRepoGetHandler, withRepoValidation } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext } from '$lib/utils/api-context.js'; +import type { RequestEvent } from '@sveltejs/kit'; +import { handleApiError, handleValidationError, handleAuthorizationError } from '$lib/utils/error-handler.js'; /** * GET - Get current owner and transfer history */ -export const GET: RequestHandler = async ({ params }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'getOwnership' }); - } - - try { - // Decode npub to get pubkey - let originalOwnerPubkey: string; - try { - originalOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'getOwnership', npub }); - } - +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { // Get current owner (may be different if transferred) - const currentOwner = await ownershipTransferService.getCurrentOwner(originalOwnerPubkey, repo); + const currentOwner = await ownershipTransferService.getCurrentOwner(context.repoOwnerPubkey, context.repo); // Fetch transfer events for history - const repoTag = `${KIND.REPO_ANNOUNCEMENT}:${originalOwnerPubkey}:${repo}`; + const repoTag = `${KIND.REPO_ANNOUNCEMENT}:${context.repoOwnerPubkey}:${context.repo}`; const transferEvents = await nostrClient.fetchEvents([ { kinds: [KIND.OWNERSHIP_TRANSFER], @@ -54,9 +37,9 @@ export const GET: RequestHandler = async ({ params }) => { transferEvents.sort((a, b) => b.created_at - a.created_at); return json({ - originalOwner: originalOwnerPubkey, + originalOwner: context.repoOwnerPubkey, currentOwner, - transferred: currentOwner !== originalOwnerPubkey, + transferred: currentOwner !== context.repoOwnerPubkey, transfers: transferEvents.map(event => { const pTag = event.tags.find(t => t[0] === 'p'); return { @@ -68,87 +51,76 @@ export const GET: RequestHandler = async ({ params }) => { }; }) }); - } catch (err) { - return handleApiError(err, { operation: 'getOwnership', npub, repo }, 'Failed to fetch ownership info'); - } -}; + }, + { operation: 'getOwnership', requireRepoAccess: false } // Ownership info is public +); /** * POST - Initiate ownership transfer * Requires a pre-signed NIP-98 authenticated event from the current owner */ -export const POST: RequestHandler = async ({ params, request }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'transferOwnership' }); - } +export const POST: RequestHandler = withRepoValidation( + async ({ repoContext, requestContext, event }) => { + if (!requestContext.userPubkeyHex) { + throw handleApiError(new Error('Authentication required'), { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }, 'Authentication required'); + } - try { - const body = await request.json(); - const { transferEvent, userPubkey } = body; + const body = await event.request.json(); + const { transferEvent } = body; - if (!transferEvent || !userPubkey) { - return handleValidationError('Missing transferEvent or userPubkey in request body', { operation: 'transferOwnership', npub, repo }); + if (!transferEvent) { + return handleValidationError('Missing transferEvent in request body', { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }); } // Verify the event is properly signed if (!transferEvent.sig || !transferEvent.id) { - return handleValidationError('Invalid event: missing signature or ID', { operation: 'transferOwnership', npub, repo }); + throw handleValidationError('Invalid event: missing signature or ID', { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }); } if (!verifyEvent(transferEvent)) { - return handleValidationError('Invalid event signature', { operation: 'transferOwnership', npub, repo }); - } - - // Decode npub to get original owner pubkey - let originalOwnerPubkey: string; - try { - originalOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'transferOwnership', npub }); + throw handleValidationError('Invalid event signature', { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }); } // Verify user is the current owner const canTransfer = await ownershipTransferService.canTransfer( - userPubkey, - originalOwnerPubkey, - repo + requestContext.userPubkeyHex, + repoContext.repoOwnerPubkey, + repoContext.repo ); if (!canTransfer) { - return handleAuthorizationError('Only the current repository owner can transfer ownership', { operation: 'transferOwnership', npub, repo }); + throw handleAuthorizationError('Only the current repository owner can transfer ownership', { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }); } // Verify the transfer event is from the current owner - if (transferEvent.pubkey !== userPubkey) { - return handleAuthorizationError('Transfer event must be signed by the current owner', { operation: 'transferOwnership', npub, repo }); + if (transferEvent.pubkey !== requestContext.userPubkeyHex) { + throw handleAuthorizationError('Transfer event must be signed by the current owner', { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }); } // Verify it's an ownership transfer event if (transferEvent.kind !== KIND.OWNERSHIP_TRANSFER) { - return handleValidationError(`Event must be kind ${KIND.OWNERSHIP_TRANSFER} (ownership transfer)`, { operation: 'transferOwnership', npub, repo }); + throw handleValidationError(`Event must be kind ${KIND.OWNERSHIP_TRANSFER} (ownership transfer)`, { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }); } // Verify the 'a' tag references this repo const aTag = transferEvent.tags.find(t => t[0] === 'a'); - const expectedRepoTag = `${KIND.REPO_ANNOUNCEMENT}:${originalOwnerPubkey}:${repo}`; + const expectedRepoTag = `${KIND.REPO_ANNOUNCEMENT}:${repoContext.repoOwnerPubkey}:${repoContext.repo}`; if (!aTag || aTag[1] !== expectedRepoTag) { - return handleValidationError("Transfer event 'a' tag does not match this repository", { operation: 'transferOwnership', npub, repo }); + throw handleValidationError("Transfer event 'a' tag does not match this repository", { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }); } // Get user's relays and publish - const { outbox } = await getUserRelays(userPubkey, nostrClient); + const { outbox } = await getUserRelays(requestContext.userPubkeyHex, nostrClient); const combinedRelays = combineRelays(outbox); const result = await nostrClient.publishEvent(transferEvent as NostrEvent, combinedRelays); if (result.success.length === 0) { - return error(500, 'Failed to publish transfer event to any relays'); + throw handleApiError(new Error('Failed to publish transfer event to any relays'), { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }, 'Failed to publish transfer event to any relays'); } // Clear cache so new owner is recognized immediately - ownershipTransferService.clearCache(originalOwnerPubkey, repo); + ownershipTransferService.clearCache(repoContext.repoOwnerPubkey, repoContext.repo); return json({ success: true, @@ -156,7 +128,6 @@ export const POST: RequestHandler = async ({ params, request }) => { published: result, message: 'Ownership transfer initiated successfully' }); - } catch (err) { - return handleApiError(err, { operation: 'transferOwnership', npub, repo }, 'Failed to transfer ownership'); - } -}; + }, + { operation: 'transferOwnership', requireRepoAccess: false } // Override to check owner instead +); diff --git a/src/routes/api/repos/[npub]/[repo]/tree/+server.ts b/src/routes/api/repos/[npub]/[repo]/tree/+server.ts index d7e1b1e..91ac96e 100644 --- a/src/routes/api/repos/[npub]/[repo]/tree/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/tree/+server.ts @@ -2,50 +2,19 @@ * API endpoint for listing files and directories in a repository */ -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; -import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; -import { nip19 } from 'nostr-tools'; -import { requireNpubHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; - -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); -const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); - -export const GET: RequestHandler = async ({ params, url, request }) => { - const { npub, repo } = params; - const ref = url.searchParams.get('ref') || 'HEAD'; - const path = url.searchParams.get('path') || ''; - const userPubkey = url.searchParams.get('userPubkey') || request.headers.get('x-user-pubkey'); - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'listFiles' }); - } - - try { - if (!fileManager.repoExists(npub, repo)) { - return handleNotFoundError('Repository not found', { operation: 'listFiles', npub, repo }); - } - - // Check repository privacy - let repoOwnerPubkey: string; - try { - repoOwnerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'listFiles', npub }); - } - - const canView = await maintainerService.canView(userPubkey || null, repoOwnerPubkey, repo); - if (!canView) { - return handleAuthorizationError('This repository is private. Only owners and maintainers can view it.', { operation: 'listFiles', npub, repo }); - } - - const files = await fileManager.listFiles(npub, repo, ref, path); +import { fileManager } from '$lib/services/service-registry.js'; +import { createRepoGetHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; + +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + const ref = context.ref || 'HEAD'; + const path = context.path || ''; + + const files = await fileManager.listFiles(context.npub, context.repo, ref, path); return json(files); - } catch (err) { - return handleApiError(err, { operation: 'listFiles', npub, repo, path, ref }, 'Failed to list files'); - } -}; + }, + { operation: 'listFiles' } +); diff --git a/src/routes/api/repos/[npub]/[repo]/verify/+server.ts b/src/routes/api/repos/[npub]/[repo]/verify/+server.ts index 7f2d503..93ca26f 100644 --- a/src/routes/api/repos/[npub]/[repo]/verify/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/verify/+server.ts @@ -5,52 +5,35 @@ import { json, error } from '@sveltejs/kit'; // @ts-ignore - SvelteKit generates this type import type { RequestHandler } from './$types'; -import { FileManager } from '$lib/services/git/file-manager.js'; +import { fileManager } from '$lib/services/service-registry.js'; import { verifyRepositoryOwnership, VERIFICATION_FILE_PATH } from '$lib/services/nostr/repo-verification.js'; -import { NostrClient } from '$lib/services/nostr/nostr-client.js'; -import { OwnershipTransferService } from '$lib/services/nostr/ownership-transfer-service.js'; -import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; +import { nostrClient } from '$lib/services/service-registry.js'; import { KIND } from '$lib/types/nostr.js'; -import { nip19 } from 'nostr-tools'; import { existsSync } from 'fs'; -import logger from '$lib/services/logger.js'; import { join } from 'path'; -import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js'; -import { handleApiError, handleValidationError, handleNotFoundError } from '$lib/utils/error-handler.js'; +import { decodeNpubToHex } from '$lib/utils/npub-utils.js'; +import { createRepoGetHandler } from '$lib/utils/api-handlers.js'; +import type { RepoRequestContext } from '$lib/utils/api-context.js'; +import { handleApiError } from '$lib/utils/error-handler.js'; -const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; -const fileManager = new FileManager(repoRoot); -const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); -const ownershipTransferService = new OwnershipTransferService(DEFAULT_NOSTR_RELAYS); +const repoRoot = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT + ? process.env.GIT_REPO_ROOT + : '/repos'; -export const GET: RequestHandler = async ({ params }: { params: { npub?: string; repo?: string } }) => { - const { npub, repo } = params; - - if (!npub || !repo) { - return handleValidationError('Missing npub or repo parameter', { operation: 'verifyRepo' }); - } - - try { - // Decode npub to get pubkey - let ownerPubkey: string; - try { - ownerPubkey = requireNpubHex(npub); - } catch { - return handleValidationError('Invalid npub format', { operation: 'verifyRepo', npub }); - } - - // Check if repository exists (using FileManager's internal method) - const repoPath = join(repoRoot, npub, `${repo}.git`); +export const GET: RequestHandler = createRepoGetHandler( + async (context: RepoRequestContext) => { + // Check if repository exists + const repoPath = join(repoRoot, context.npub, `${context.repo}.git`); if (!existsSync(repoPath)) { - return handleNotFoundError('Repository not found', { operation: 'verifyRepo', npub, repo }); + throw handleApiError(new Error('Repository not found'), { operation: 'verifyRepo', npub: context.npub, repo: context.repo }, 'Repository not found'); } // Fetch the repository announcement const events = await nostrClient.fetchEvents([ { kinds: [KIND.REPO_ANNOUNCEMENT], - authors: [ownerPubkey], - '#d': [repo], + authors: [context.repoOwnerPubkey], + '#d': [context.repo], limit: 1 } ]); @@ -66,7 +49,7 @@ export const GET: RequestHandler = async ({ params }: { params: { npub?: string; const announcement = events[0]; // Check for ownership transfer events (including self-transfer for initial ownership) - const repoTag = `${KIND.REPO_ANNOUNCEMENT}:${ownerPubkey}:${repo}`; + const repoTag = `${KIND.REPO_ANNOUNCEMENT}:${context.repoOwnerPubkey}:${context.repo}`; const transferEvents = await nostrClient.fetchEvents([ { kinds: [KIND.OWNERSHIP_TRANSFER], @@ -90,8 +73,8 @@ export const GET: RequestHandler = async ({ params }: { params: { npub?: string; } } - return event.pubkey === ownerPubkey && - toPubkey === ownerPubkey; + return event.pubkey === context.repoOwnerPubkey && + toPubkey === context.repoOwnerPubkey; }); // Verify ownership - prefer self-transfer event, fall back to verification file @@ -113,7 +96,7 @@ export const GET: RequestHandler = async ({ params }: { params: { npub?: string; } else { // Fall back to verification file method (for backward compatibility) try { - const verificationFile = await fileManager.getFileContent(npub, repo, VERIFICATION_FILE_PATH, 'HEAD'); + const verificationFile = await fileManager.getFileContent(context.npub, context.repo, VERIFICATION_FILE_PATH, 'HEAD'); const verification = verifyRepositoryOwnership(announcement, verificationFile.content); verified = verification.valid; verificationError = verification.error; @@ -129,7 +112,7 @@ export const GET: RequestHandler = async ({ params }: { params: { npub?: string; return json({ verified: true, announcementId: announcement.id, - ownerPubkey: ownerPubkey, + ownerPubkey: context.repoOwnerPubkey, verificationMethod, selfTransferEventId: selfTransfer?.id, message: 'Repository ownership verified successfully' @@ -143,7 +126,6 @@ export const GET: RequestHandler = async ({ params }: { params: { npub?: string; message: 'Repository ownership verification failed' }); } - } catch (err) { - return handleApiError(err, { operation: 'verifyRepo', npub, repo }, 'Failed to verify repository'); - } -}; + }, + { operation: 'verifyRepo', requireRepoAccess: false } // Verification is public +);