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.
114 lines
3.7 KiB
114 lines
3.7 KiB
/** |
|
* API endpoint for listing repositories with privacy checks |
|
* Returns only repositories the current user can view |
|
*/ |
|
|
|
import { json } from '@sveltejs/kit'; |
|
import type { RequestHandler } from './$types'; |
|
import { NostrClient } from '$lib/services/nostr/nostr-client.js'; |
|
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; |
|
import { DEFAULT_NOSTR_RELAYS, GIT_DOMAIN } from '$lib/config.js'; |
|
import { KIND } from '$lib/types/nostr.js'; |
|
import { nip19 } from 'nostr-tools'; |
|
import { handleApiError } from '$lib/utils/error-handler.js'; |
|
import { extractRequestContext } from '$lib/utils/api-context.js'; |
|
import logger from '$lib/services/logger.js'; |
|
import type { NostrEvent } from '$lib/types/nostr.js'; |
|
import type { RequestEvent } from '@sveltejs/kit'; |
|
|
|
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); |
|
const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); |
|
|
|
interface RepoListItem { |
|
event: NostrEvent; |
|
npub: string; |
|
repoName: string; |
|
isRegistered: boolean; // Has this domain in clone URLs |
|
} |
|
|
|
export const GET: RequestHandler = async (event) => { |
|
try { |
|
const requestContext = extractRequestContext(event); |
|
const userPubkey = requestContext.userPubkeyHex || null; |
|
const gitDomain = event.url.searchParams.get('domain') || GIT_DOMAIN; |
|
|
|
// Fetch all repository announcements |
|
const events = await nostrClient.fetchEvents([ |
|
{ kinds: [KIND.REPO_ANNOUNCEMENT], limit: 100 } |
|
]); |
|
|
|
const repos: RepoListItem[] = []; |
|
|
|
// Process each announcement |
|
for (const event of events) { |
|
const cloneUrls = event.tags |
|
.filter(t => t[0] === 'clone') |
|
.flatMap(t => t.slice(1)) |
|
.filter(url => url && typeof url === 'string'); |
|
|
|
// Check if repo has this domain in clone URLs |
|
const hasDomain = cloneUrls.some(url => url.includes(gitDomain)); |
|
|
|
// Extract repo name from d-tag |
|
const dTag = event.tags.find(t => t[0] === 'd')?.[1]; |
|
if (!dTag) continue; |
|
|
|
// Check privacy |
|
const isPrivate = event.tags.some(t => |
|
(t[0] === 'private' && t[1] === 'true') || |
|
(t[0] === 't' && t[1] === 'private') |
|
); |
|
|
|
// Check if user can view this repo |
|
let canView = false; |
|
if (!isPrivate) { |
|
canView = true; // Public repos are viewable by anyone |
|
} else if (userPubkey) { |
|
// Private repos require authentication |
|
try { |
|
canView = await maintainerService.canView(userPubkey, event.pubkey, dTag); |
|
} catch (err) { |
|
logger.warn({ error: err, pubkey: event.pubkey, repo: dTag }, 'Failed to check repo access'); |
|
canView = false; |
|
} |
|
} |
|
|
|
// Only include repos the user can view |
|
if (!canView) continue; |
|
|
|
// Extract npub from clone URLs or convert pubkey |
|
let npub: string; |
|
const domainUrl = cloneUrls.find(url => url.includes(gitDomain)); |
|
if (domainUrl) { |
|
const match = domainUrl.match(/\/(npub[a-z0-9]+)\//); |
|
if (match) { |
|
npub = match[1]; |
|
} else { |
|
npub = nip19.npubEncode(event.pubkey); |
|
} |
|
} else { |
|
npub = nip19.npubEncode(event.pubkey); |
|
} |
|
|
|
repos.push({ |
|
event, |
|
npub, |
|
repoName: dTag, |
|
isRegistered: hasDomain |
|
}); |
|
} |
|
|
|
// Only return registered repos (repos with this domain in clone URLs) |
|
const registered = repos.filter(r => r.isRegistered); |
|
|
|
// Sort by created_at descending |
|
registered.sort((a, b) => b.event.created_at - a.event.created_at); |
|
|
|
return json({ |
|
registered, |
|
total: registered.length |
|
}); |
|
} catch (err) { |
|
return handleApiError(err, { operation: 'listRepos' }, 'Failed to list repositories'); |
|
} |
|
};
|
|
|