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

/**
* 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');
}
};