Barazo AppView backend barazo.forum
at main 148 lines 4.9 kB view raw
1import type { Cache } from '../cache/index.js' 2import type { Logger } from './logger.js' 3 4// --------------------------------------------------------------------------- 5// Constants 6// --------------------------------------------------------------------------- 7 8export const DID_DOC_CACHE_PREFIX = 'barazo:did-doc:' 9/** Soft TTL in seconds -- triggers background refresh after this. */ 10export const DID_DOC_SOFT_TTL = 3600 // 1 hour 11/** Hard TTL in seconds -- Valkey key expiry. */ 12export const DID_DOC_HARD_TTL = 7200 // 2 hours 13 14const PLC_DIRECTORY_URL = 'https://plc.directory' 15const PLC_FETCH_TIMEOUT = 5000 // 5 seconds 16 17// --------------------------------------------------------------------------- 18// Types 19// --------------------------------------------------------------------------- 20 21export type DidVerificationResult = { active: true } | { active: false; reason: string } 22 23interface CachedDidEntry { 24 active: boolean 25 reason?: string 26 resolvedAt: number 27} 28 29export interface DidDocumentVerifier { 30 /** Verify that a DID is still active. */ 31 verify(did: string): Promise<DidVerificationResult> 32} 33 34// --------------------------------------------------------------------------- 35// Factory 36// --------------------------------------------------------------------------- 37 38export function createDidDocumentVerifier(cache: Cache, logger: Logger): DidDocumentVerifier { 39 /** 40 * Resolve a DID document from PLC directory and return whether it's active. 41 */ 42 async function resolveFromPlc(did: string): Promise<DidVerificationResult> { 43 const url = `${PLC_DIRECTORY_URL}/${did}` 44 45 const response = await fetch(url, { 46 headers: { Accept: 'application/json' }, 47 signal: AbortSignal.timeout(PLC_FETCH_TIMEOUT), 48 }) 49 50 if (response.ok) { 51 return { active: true } 52 } 53 54 if (response.status === 410) { 55 return { active: false, reason: 'DID has been tombstoned' } 56 } 57 58 if (response.status === 404) { 59 return { active: false, reason: 'DID not found in PLC directory' } 60 } 61 62 // Unexpected status -- treat as resolution failure 63 logger.warn({ did, status: response.status }, 'Unexpected PLC directory response') 64 throw new Error(`PLC directory returned ${String(response.status)}`) 65 } 66 67 /** 68 * Cache a verification result with hard TTL. 69 */ 70 async function cacheResult(did: string, result: DidVerificationResult): Promise<void> { 71 const entry: CachedDidEntry = { 72 active: result.active, 73 resolvedAt: Date.now(), 74 ...(!result.active && { reason: (result as { reason: string }).reason }), 75 } 76 77 await cache.set(`${DID_DOC_CACHE_PREFIX}${did}`, JSON.stringify(entry), 'EX', DID_DOC_HARD_TTL) 78 } 79 80 /** 81 * Trigger a non-blocking background refresh for a DID. 82 */ 83 function backgroundRefresh(did: string): void { 84 resolveFromPlc(did) 85 .then(async (result) => { 86 await cacheResult(did, result) 87 logger.debug({ did }, 'Background DID document refresh completed') 88 }) 89 .catch((err: unknown) => { 90 logger.warn({ err, did }, 'Background DID document refresh failed') 91 }) 92 } 93 94 async function verify(did: string): Promise<DidVerificationResult> { 95 // did:web -- skip PLC verification (PLC only handles did:plc) 96 if (!did.startsWith('did:plc:')) { 97 return { active: true } 98 } 99 100 // 1. Try cache 101 let cachedEntry: CachedDidEntry | undefined 102 try { 103 const raw = await cache.get(`${DID_DOC_CACHE_PREFIX}${did}`) 104 if (raw !== null) { 105 cachedEntry = JSON.parse(raw) as CachedDidEntry 106 107 const age = Date.now() - cachedEntry.resolvedAt 108 const isPastSoftTtl = age > DID_DOC_SOFT_TTL * 1000 109 110 if (!isPastSoftTtl) { 111 // Fresh cache hit -- return immediately 112 if (cachedEntry.active) { 113 return { active: true } 114 } 115 return { active: false, reason: cachedEntry.reason ?? 'DID is not active' } 116 } 117 118 // Past soft TTL -- serve stale and trigger background refresh 119 backgroundRefresh(did) 120 121 if (cachedEntry.active) { 122 return { active: true } 123 } 124 return { active: false, reason: cachedEntry.reason ?? 'DID is not active' } 125 } 126 } catch (err: unknown) { 127 logger.warn({ err, did }, 'DID document cache read failed') 128 // Fall through to PLC directory resolution 129 } 130 131 // 2. Cache miss -- resolve from PLC directory 132 try { 133 const result = await resolveFromPlc(did) 134 135 // Cache the result (fire-and-forget on failure) 136 cacheResult(did, result).catch((err: unknown) => { 137 logger.warn({ err, did }, 'Failed to cache DID verification result') 138 }) 139 140 return result 141 } catch (err: unknown) { 142 logger.error({ err, did }, 'DID document resolution failed') 143 return { active: false, reason: 'DID document resolution failed' } 144 } 145 } 146 147 return { verify } 148}