20 changed files with 1125 additions and 872 deletions
@ -0,0 +1,158 @@
@@ -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(); |
||||
@ -0,0 +1,151 @@
@@ -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<void> { |
||||
const canView = await maintainerService.canView( |
||||
requestContext.userPubkeyHex || null, |
||||
repoContext.repoOwnerPubkey, |
||||
repoContext.repo |
||||
); |
||||
|
||||
if (!canView) { |
||||
throw handleAuthorizationError( |
||||
'This repository is private. Only owners and maintainers can view it.', |
||||
{ operation, npub: repoContext.npub, repo: repoContext.repo } |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Check if repository exists |
||||
*
|
||||
* @param repoContext - Repository context |
||||
* @param operation - Operation name for error context |
||||
* @returns void if exists, throws error if not found |
||||
*/ |
||||
export function requireRepoExists( |
||||
repoContext: RepoContext, |
||||
operation?: string |
||||
): void { |
||||
if (!fileManager.repoExists(repoContext.npub, repoContext.repo)) { |
||||
throw handleNotFoundError( |
||||
'Repository not found', |
||||
{ operation, npub: repoContext.npub, repo: repoContext.repo } |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Check if user is a maintainer of the repository |
||||
*
|
||||
* @param repoContext - Repository context |
||||
* @param requestContext - Request context with user pubkey |
||||
* @param operation - Operation name for error context |
||||
* @returns void if maintainer, throws error if not |
||||
*/ |
||||
export async function requireMaintainer( |
||||
repoContext: RepoContext, |
||||
requestContext: RequestContext, |
||||
operation?: string |
||||
): Promise<void> { |
||||
if (!requestContext.userPubkeyHex) { |
||||
throw handleAuthError( |
||||
'Authentication required. Please provide userPubkey.', |
||||
{ operation, npub: repoContext.npub, repo: repoContext.repo } |
||||
); |
||||
} |
||||
|
||||
const isMaintainer = await maintainerService.isMaintainer( |
||||
requestContext.userPubkeyHex, |
||||
repoContext.repoOwnerPubkey, |
||||
repoContext.repo |
||||
); |
||||
|
||||
if (!isMaintainer) { |
||||
throw handleAuthorizationError( |
||||
'Only repository maintainers can perform this action. Please submit a pull request instead.', |
||||
{ operation, npub: repoContext.npub, repo: repoContext.repo } |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Verify NIP-98 authentication from request headers |
||||
*
|
||||
* @param request - Request object |
||||
* @param url - URL object |
||||
* @param method - HTTP method |
||||
* @param operation - Operation name for error context |
||||
* @returns NIP-98 event if valid, throws error if invalid |
||||
*/ |
||||
export function requireNIP98Auth( |
||||
request: Request, |
||||
url: URL, |
||||
method: string, |
||||
operation?: string |
||||
): NostrEvent { |
||||
const authHeader = request.headers.get('Authorization'); |
||||
|
||||
if (!authHeader || !authHeader.startsWith('Nostr ')) { |
||||
throw handleAuthError( |
||||
'NIP-98 authentication required', |
||||
{ operation } |
||||
); |
||||
} |
||||
|
||||
// Build absolute request URL for NIP-98 validation
|
||||
const protocol = request.headers.get('x-forwarded-proto') ||
|
||||
(url.protocol === 'https:' ? 'https' : 'http'); |
||||
const host = request.headers.get('host') || url.host; |
||||
const requestUrl = `${protocol}://${host}${url.pathname}${url.search}`; |
||||
|
||||
const authResult = verifyNIP98Auth(authHeader, requestUrl, method); |
||||
|
||||
if (!authResult.valid || !authResult.event) { |
||||
throw handleAuthError( |
||||
authResult.error || 'Invalid NIP-98 authentication', |
||||
{ operation } |
||||
); |
||||
} |
||||
|
||||
return authResult.event; |
||||
} |
||||
|
||||
/** |
||||
* Combined check: repository exists and user has access |
||||
*
|
||||
* @param repoContext - Repository context |
||||
* @param requestContext - Request context with user pubkey |
||||
* @param operation - Operation name for error context |
||||
* @returns void if all checks pass, throws error if any fail |
||||
*/ |
||||
export async function requireRepoAccessWithExists( |
||||
repoContext: RepoContext, |
||||
requestContext: RequestContext, |
||||
operation?: string |
||||
): Promise<void> { |
||||
requireRepoExists(repoContext, operation); |
||||
await requireRepoAccess(repoContext, requestContext, operation); |
||||
} |
||||
@ -0,0 +1,162 @@
@@ -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'; |
||||
} |
||||
} |
||||
@ -0,0 +1,289 @@
@@ -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<Response>; |
||||
|
||||
/** |
||||
* Handler function with full repo request context |
||||
*/ |
||||
export type RepoRequestHandler = (context: { |
||||
repoRequestContext: RepoRequestContext; |
||||
event: RequestEvent; |
||||
}) => Promise<Response>; |
||||
|
||||
/** |
||||
* 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<Response>, |
||||
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<Response>, |
||||
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<Response>; |
||||
post?: (context: RepoRequestContext, event: RequestEvent) => Promise<Response>; |
||||
put?: (context: RepoRequestContext, event: RequestEvent) => Promise<Response>; |
||||
delete?: (context: RepoRequestContext, event: RequestEvent) => Promise<Response>; |
||||
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; |
||||
} |
||||
Loading…
Reference in new issue