Browse Source

refactor

main
Silberengel 4 weeks ago
parent
commit
4f2fd74309
  1. 158
      src/lib/services/service-registry.ts
  2. 151
      src/lib/utils/api-auth.ts
  3. 162
      src/lib/utils/api-context.ts
  4. 289
      src/lib/utils/api-handlers.ts
  5. 90
      src/routes/api/repos/[npub]/[repo]/branch-protection/+server.ts
  6. 93
      src/routes/api/repos/[npub]/[repo]/branches/+server.ts
  7. 48
      src/routes/api/repos/[npub]/[repo]/commits/+server.ts
  8. 47
      src/routes/api/repos/[npub]/[repo]/diff/+server.ts
  9. 73
      src/routes/api/repos/[npub]/[repo]/download/+server.ts
  10. 93
      src/routes/api/repos/[npub]/[repo]/highlights/+server.ts
  11. 85
      src/routes/api/repos/[npub]/[repo]/issues/+server.ts
  12. 48
      src/routes/api/repos/[npub]/[repo]/maintainers/+server.ts
  13. 84
      src/routes/api/repos/[npub]/[repo]/prs/+server.ts
  14. 59
      src/routes/api/repos/[npub]/[repo]/raw/+server.ts
  15. 57
      src/routes/api/repos/[npub]/[repo]/readme/+server.ts
  16. 122
      src/routes/api/repos/[npub]/[repo]/settings/+server.ts
  17. 102
      src/routes/api/repos/[npub]/[repo]/tags/+server.ts
  18. 111
      src/routes/api/repos/[npub]/[repo]/transfer/+server.ts
  19. 59
      src/routes/api/repos/[npub]/[repo]/tree/+server.ts
  20. 66
      src/routes/api/repos/[npub]/[repo]/verify/+server.ts

158
src/lib/services/service-registry.ts

@ -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();

151
src/lib/utils/api-auth.ts

@ -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);
}

162
src/lib/utils/api-context.ts

@ -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';
}
}

289
src/lib/utils/api-handlers.ts

@ -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;
}

90
src/routes/api/repos/[npub]/[repo]/branch-protection/+server.ts

@ -4,88 +4,47 @@ @@ -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 @@ -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 @@ -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
);

93
src/routes/api/repos/[npub]/[repo]/branches/+server.ts

@ -2,84 +2,33 @@ @@ -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' }
);

48
src/routes/api/repos/[npub]/[repo]/commits/+server.ts

@ -2,40 +2,20 @@ @@ -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' }
);

47
src/routes/api/repos/[npub]/[repo]/diff/+server.ts

@ -2,40 +2,25 @@ @@ -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' }
);

73
src/routes/api/repos/[npub]/[repo]/download/+server.ts

@ -2,85 +2,61 @@ @@ -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 }) => { @@ -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' }
);

93
src/routes/api/repos/[npub]/[repo]/highlights/+server.ts

@ -4,44 +4,27 @@ @@ -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 }) => { @@ -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 }) => { @@ -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
);

85
src/routes/api/repos/[npub]/[repo]/issues/+server.ts

@ -2,79 +2,44 @@ @@ -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
);

48
src/routes/api/repos/[npub]/[repo]/maintainers/+server.ts

@ -2,51 +2,29 @@ @@ -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
);

84
src/routes/api/repos/[npub]/[repo]/prs/+server.ts

@ -2,79 +2,45 @@ @@ -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
);

59
src/routes/api/repos/[npub]/[repo]/raw/+server.ts

@ -2,49 +2,23 @@ @@ -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 }) => { @@ -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' }
);

57
src/routes/api/repos/[npub]/[repo]/readme/+server.ts

@ -2,18 +2,11 @@ @@ -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 = [ @@ -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 }) => { @@ -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 }) => { @@ -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' }
);

122
src/routes/api/repos/[npub]/[repo]/settings/+server.ts

@ -4,52 +4,28 @@ @@ -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 }) => { @@ -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 }) => { @@ -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 }) => { @@ -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 }) => { @@ -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 }) => { @@ -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 }) => { @@ -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 }) => { @@ -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
);

102
src/routes/api/repos/[npub]/[repo]/tags/+server.ts

@ -2,93 +2,33 @@ @@ -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' }
);

111
src/routes/api/repos/[npub]/[repo]/transfer/+server.ts

@ -4,44 +4,27 @@ @@ -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 }) => { @@ -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 }) => { @@ -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 }) => { @@ -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
);

59
src/routes/api/repos/[npub]/[repo]/tree/+server.ts

@ -2,50 +2,19 @@ @@ -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' }
);

66
src/routes/api/repos/[npub]/[repo]/verify/+server.ts

@ -5,52 +5,35 @@ @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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
);

Loading…
Cancel
Save