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

171 lines
6.3 KiB

/**
* API endpoint for verifying repository ownership
*/
import { json, error } from '@sveltejs/kit';
// @ts-ignore - SvelteKit generates this type
import type { RequestHandler } from './$types';
import { fileManager } from '$lib/services/service-registry.js';
import { verifyRepositoryOwnership } from '$lib/services/nostr/repo-verification.js';
import type { NostrEvent } from '$lib/types/nostr.js';
import { nostrClient } from '$lib/services/service-registry.js';
import { KIND } from '$lib/types/nostr.js';
import { existsSync } from 'fs';
import { join } from 'path';
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 = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT
? process.env.GIT_REPO_ROOT
: '/repos';
export const GET: RequestHandler = createRepoGetHandler(
async (context: RepoRequestContext) => {
// Check if repository exists - verification doesn't require the repo to be cloned locally
// We can verify ownership from Nostr events alone
// Fetch the repository announcement
const events = await nostrClient.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [context.repoOwnerPubkey],
'#d': [context.repo],
limit: 1
}
]);
if (events.length === 0) {
return json({
verified: false,
error: 'Repository announcement not found',
message: 'Could not find a NIP-34 repository announcement for this repository.'
});
}
const announcement = events[0];
// Extract clone URLs from announcement
const cloneUrls: string[] = [];
for (const tag of announcement.tags) {
if (tag[0] === 'clone') {
for (let i = 1; i < tag.length; i++) {
const url = tag[i];
if (url && typeof url === 'string') {
cloneUrls.push(url);
}
}
}
}
// Verify ownership for each clone separately
// Ownership is determined by the most recent announcement file checked into each clone
const cloneVerifications: Array<{ url: string; verified: boolean; ownerPubkey: string | null; error?: string }> = [];
// First, verify the local GitRepublic clone (if it exists)
let localVerified = false;
let localOwner: string | null = null;
let localError: string | undefined;
try {
// Get current owner from the most recent announcement file in the repo
localOwner = await fileManager.getCurrentOwnerFromRepo(context.npub, context.repo);
if (localOwner) {
// Verify the announcement in nostr/repo-events.jsonl matches the announcement event
try {
const repoEventsFile = await fileManager.getFileContent(context.npub, context.repo, 'nostr/repo-events.jsonl', 'HEAD');
// Parse repo-events.jsonl and find the most recent announcement
const lines = repoEventsFile.content.trim().split('\n').filter(Boolean);
let repoAnnouncement: NostrEvent | null = null;
let latestTimestamp = 0;
for (const line of lines) {
try {
const entry = JSON.parse(line);
if (entry.type === 'announcement' && entry.event && entry.timestamp) {
if (entry.timestamp > latestTimestamp) {
latestTimestamp = entry.timestamp;
repoAnnouncement = entry.event;
}
}
} catch {
continue;
}
}
if (repoAnnouncement) {
const verification = verifyRepositoryOwnership(announcement, JSON.stringify(repoAnnouncement));
localVerified = verification.valid;
if (!verification.valid) {
localError = verification.error;
}
} else {
localVerified = false;
localError = 'No announcement found in nostr/repo-events.jsonl';
}
} catch (err) {
localVerified = false;
localError = 'Announcement file not found in repository';
}
} else {
localVerified = false;
localError = 'No announcement found in repository';
}
} catch (err) {
localVerified = false;
localError = err instanceof Error ? err.message : 'Failed to verify local clone';
}
// Add local clone verification
const localUrl = cloneUrls.find(url => url.includes(context.npub) || url.includes(context.repoOwnerPubkey));
if (localUrl) {
cloneVerifications.push({
url: localUrl,
verified: localVerified,
ownerPubkey: localOwner,
error: localError
});
}
// For other clones (GitHub, GitLab, etc.), we'd need to fetch them first to check their announcement files
// This is a future enhancement - for now we only verify the local GitRepublic clone
// Overall verification: at least one clone must be verified
const overallVerified = cloneVerifications.some(cv => cv.verified);
const verifiedClones = cloneVerifications.filter(cv => cv.verified);
const currentOwner = localOwner || context.repoOwnerPubkey;
if (overallVerified) {
return json({
verified: true,
announcementId: announcement.id,
ownerPubkey: currentOwner,
verificationMethod: 'announcement-file',
cloneVerifications: cloneVerifications.map(cv => ({
url: cv.url,
verified: cv.verified,
ownerPubkey: cv.ownerPubkey,
error: cv.error
})),
message: `Repository ownership verified successfully for ${verifiedClones.length} clone(s)`
});
} else {
return json({
verified: false,
error: localError || 'Repository ownership verification failed',
announcementId: announcement.id,
verificationMethod: 'announcement-file',
cloneVerifications: cloneVerifications.map(cv => ({
url: cv.url,
verified: cv.verified,
ownerPubkey: cv.ownerPubkey,
error: cv.error
})),
message: 'Repository ownership verification failed for all clones'
});
}
},
{ operation: 'verifyRepo', requireRepoExists: false, requireRepoAccess: false } // Verification is public, doesn't need repo to exist
);