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