Barazo AppView backend
barazo.forum
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}