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