Barazo AppView backend barazo.forum
at main 160 lines 5.0 kB view raw
1import type { Cache } from '../cache/index.js' 2import type { Database } from '../db/index.js' 3import type { Logger } from './logger.js' 4import { users } from '../db/schema/users.js' 5import { eq } from 'drizzle-orm' 6 7// --------------------------------------------------------------------------- 8// Constants 9// --------------------------------------------------------------------------- 10 11const HANDLE_CACHE_PREFIX = 'barazo:handle:' 12const HANDLE_CACHE_TTL = 3600 // 1 hour 13const PLC_DIRECTORY_URL = 'https://plc.directory' 14 15// --------------------------------------------------------------------------- 16// Types 17// --------------------------------------------------------------------------- 18 19/** DID document from PLC directory. */ 20interface PlcDidDocument { 21 id: string 22 alsoKnownAs?: string[] 23} 24 25export interface HandleResolver { 26 /** 27 * Resolve a DID to its AT Protocol handle. 28 * 29 * Resolution order: 30 * 1. Valkey cache (1-hour TTL) 31 * 2. Users table in PostgreSQL (populated by firehose) 32 * 3. PLC directory (for did:plc:*) -- fetches the DID document 33 * 34 * Returns the DID itself as fallback if resolution fails (never blocks auth). 35 */ 36 resolve(did: string): Promise<string> 37} 38 39// --------------------------------------------------------------------------- 40// Helpers 41// --------------------------------------------------------------------------- 42 43/** 44 * Extract handle from a DID document's alsoKnownAs field. 45 * AT Protocol handles appear as "at://{handle}" entries. 46 */ 47function extractHandleFromDidDocument(doc: PlcDidDocument): string | undefined { 48 if (!doc.alsoKnownAs || !Array.isArray(doc.alsoKnownAs)) { 49 return undefined 50 } 51 52 for (const aka of doc.alsoKnownAs) { 53 if (typeof aka === 'string' && aka.startsWith('at://')) { 54 return aka.slice('at://'.length) 55 } 56 } 57 58 return undefined 59} 60 61// --------------------------------------------------------------------------- 62// Factory 63// --------------------------------------------------------------------------- 64 65export function createHandleResolver(cache: Cache, db: Database, logger: Logger): HandleResolver { 66 async function resolveFromCache(did: string): Promise<string | undefined> { 67 const cached = await cache.get(`${HANDLE_CACHE_PREFIX}${did}`) 68 if (cached !== null) { 69 return cached 70 } 71 return undefined 72 } 73 74 async function resolveFromDb(did: string): Promise<string | undefined> { 75 const rows = await db 76 .select({ handle: users.handle }) 77 .from(users) 78 .where(eq(users.did, did)) 79 .limit(1) 80 81 const row = rows[0] 82 if (row !== undefined && row.handle !== did) { 83 return row.handle 84 } 85 return undefined 86 } 87 88 async function resolveFromPlcDirectory(did: string): Promise<string | undefined> { 89 if (!did.startsWith('did:plc:')) { 90 // did:web resolution is not yet needed for MVP 91 logger.debug({ did }, 'Non-PLC DID, skipping PLC directory lookup') 92 return undefined 93 } 94 95 const url = `${PLC_DIRECTORY_URL}/${encodeURIComponent(did)}` 96 97 const response = await fetch(url, { 98 headers: { Accept: 'application/json' }, 99 signal: AbortSignal.timeout(5000), 100 }) 101 102 if (!response.ok) { 103 logger.warn({ did, status: response.status }, 'PLC directory lookup failed') 104 return undefined 105 } 106 107 const doc = (await response.json()) as PlcDidDocument 108 return extractHandleFromDidDocument(doc) 109 } 110 111 async function cacheHandle(did: string, handle: string): Promise<void> { 112 await cache.set(`${HANDLE_CACHE_PREFIX}${did}`, handle, 'EX', HANDLE_CACHE_TTL) 113 } 114 115 async function resolve(did: string): Promise<string> { 116 // 1. Check Valkey cache 117 try { 118 const cached = await resolveFromCache(did) 119 if (cached) { 120 return cached 121 } 122 } catch (err: unknown) { 123 logger.warn({ err, did }, 'Handle cache lookup failed, continuing') 124 } 125 126 // 2. Check users table (firehose may have indexed the handle) 127 try { 128 const dbHandle = await resolveFromDb(did) 129 if (dbHandle) { 130 // Populate cache for next time 131 await cacheHandle(did, dbHandle).catch((err: unknown) => { 132 logger.warn({ err, did }, 'Failed to cache handle from DB') 133 }) 134 return dbHandle 135 } 136 } catch (err: unknown) { 137 logger.warn({ err, did }, 'Handle DB lookup failed, continuing') 138 } 139 140 // 3. Resolve from PLC directory 141 try { 142 const plcHandle = await resolveFromPlcDirectory(did) 143 if (plcHandle) { 144 // Populate cache for next time 145 await cacheHandle(did, plcHandle).catch((err: unknown) => { 146 logger.warn({ err, did }, 'Failed to cache handle from PLC') 147 }) 148 return plcHandle 149 } 150 } catch (err: unknown) { 151 logger.warn({ err, did }, 'PLC directory lookup failed, continuing') 152 } 153 154 // 4. Fallback: return DID itself (auth should never fail due to handle resolution) 155 logger.info({ did }, 'Handle resolution failed, using DID as fallback') 156 return did 157 } 158 159 return { resolve } 160}