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.
100 lines
3.3 KiB
100 lines
3.3 KiB
/** |
|
* Service for checking repository maintainer permissions |
|
* Based on NIP-34 repository announcements |
|
*/ |
|
|
|
import { NostrClient } from './nostr-client.js'; |
|
import { KIND } from '../../types/nostr.js'; |
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
import { nip19 } from 'nostr-tools'; |
|
|
|
export class MaintainerService { |
|
private nostrClient: NostrClient; |
|
private cache: Map<string, { maintainers: string[]; owner: string; timestamp: number }> = new Map(); |
|
private cacheTTL = 5 * 60 * 1000; // 5 minutes |
|
|
|
constructor(relays: string[]) { |
|
this.nostrClient = new NostrClient(relays); |
|
} |
|
|
|
/** |
|
* Get maintainers for a repository from NIP-34 announcement |
|
*/ |
|
async getMaintainers(repoOwnerPubkey: string, repoId: string): Promise<{ owner: string; maintainers: string[] }> { |
|
const cacheKey = `${repoOwnerPubkey}:${repoId}`; |
|
const cached = this.cache.get(cacheKey); |
|
|
|
// Return cached if still valid |
|
if (cached && Date.now() - cached.timestamp < this.cacheTTL) { |
|
return { owner: cached.owner, maintainers: cached.maintainers }; |
|
} |
|
|
|
try { |
|
// Fetch the repository announcement |
|
const events = await this.nostrClient.fetchEvents([ |
|
{ |
|
kinds: [KIND.REPO_ANNOUNCEMENT], |
|
authors: [repoOwnerPubkey], |
|
'#d': [repoId], |
|
limit: 1 |
|
} |
|
]); |
|
|
|
if (events.length === 0) { |
|
// If no announcement found, only the owner is a maintainer |
|
const result = { owner: repoOwnerPubkey, maintainers: [repoOwnerPubkey] }; |
|
this.cache.set(cacheKey, { ...result, timestamp: Date.now() }); |
|
return result; |
|
} |
|
|
|
const announcement = events[0]; |
|
const maintainers: string[] = [announcement.pubkey]; // Owner is always a maintainer |
|
|
|
// Extract maintainers from tags |
|
for (const tag of announcement.tags) { |
|
if (tag[0] === 'maintainers' && tag[1]) { |
|
// Maintainers can be npub or hex pubkey |
|
let pubkey = tag[1]; |
|
try { |
|
// Try to decode if it's an npub |
|
const decoded = nip19.decode(pubkey); |
|
if (decoded.type === 'npub') { |
|
pubkey = decoded.data as string; |
|
} |
|
} catch { |
|
// Assume it's already a hex pubkey |
|
} |
|
if (pubkey && !maintainers.includes(pubkey)) { |
|
maintainers.push(pubkey); |
|
} |
|
} |
|
} |
|
|
|
const result = { owner: announcement.pubkey, maintainers }; |
|
this.cache.set(cacheKey, { ...result, timestamp: Date.now() }); |
|
return result; |
|
} catch (error) { |
|
console.error('Error fetching maintainers:', error); |
|
// Fallback: only owner is maintainer |
|
const result = { owner: repoOwnerPubkey, maintainers: [repoOwnerPubkey] }; |
|
this.cache.set(cacheKey, { ...result, timestamp: Date.now() }); |
|
return result; |
|
} |
|
} |
|
|
|
/** |
|
* Check if a user is a maintainer of a repository |
|
*/ |
|
async isMaintainer(userPubkey: string, repoOwnerPubkey: string, repoId: string): Promise<boolean> { |
|
const { maintainers } = await this.getMaintainers(repoOwnerPubkey, repoId); |
|
return maintainers.includes(userPubkey); |
|
} |
|
|
|
/** |
|
* Clear cache for a repository (useful after maintainer changes) |
|
*/ |
|
clearCache(repoOwnerPubkey: string, repoId: string): void { |
|
const cacheKey = `${repoOwnerPubkey}:${repoId}`; |
|
this.cache.delete(cacheKey); |
|
} |
|
}
|
|
|