Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.

etags, request tracing, and in-memory caching for settings/handles

+178 -65
+29 -9
apps/hosting-service/src/lib/db.ts
··· 43 43 customDomainCache.delete(key); 44 44 } 45 45 } 46 + 47 + for (const [key, entry] of settingsCache.entries()) { 48 + if (now - entry.timestamp > SETTINGS_CACHE_TTL) { 49 + settingsCache.delete(key); 50 + } 51 + } 46 52 }, 30 * 60 * 1000); // Run every 30 minutes 47 53 } 48 54 ··· 247 253 248 254 // Site cache queries 249 255 250 - export async function getSiteCache(did: string, rkey: string): Promise<SiteCache | null> { 251 - const result = await sql<SiteCache[]>` 252 - SELECT did, rkey, record_cid, file_cids, cached_at, updated_at 253 - FROM site_cache 254 - WHERE did = ${did} AND rkey = ${rkey} 255 - LIMIT 1 256 - `; 257 - return result[0] || null; 258 - } 256 + const SETTINGS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes 257 + const settingsCache = new Map<string, CachedDomain<SiteSettingsCache | null>>(); 259 258 260 259 export async function getSiteSettingsCache(did: string, rkey: string): Promise<SiteSettingsCache | null> { 260 + const key = `${did}:${rkey}`; 261 + 262 + const cached = settingsCache.get(key); 263 + if (cached && Date.now() - cached.timestamp < SETTINGS_CACHE_TTL) { 264 + return cached.value; 265 + } 266 + 261 267 const result = await sql<SiteSettingsCache[]>` 262 268 SELECT did, rkey, record_cid, directory_listing, spa_mode, custom_404, index_files, clean_urls, headers, cached_at, updated_at 263 269 FROM site_settings_cache 264 270 WHERE did = ${did} AND rkey = ${rkey} 265 271 LIMIT 1 266 272 `; 273 + const data = result[0] || null; 274 + 275 + settingsCache.set(key, { value: data, timestamp: Date.now() }); 276 + return data; 277 + } 278 + 279 + export async function getSiteCache(did: string, rkey: string): Promise<SiteCache | null> { 280 + const result = await sql<SiteCache[]>` 281 + SELECT did, rkey, record_cid, file_cids, cached_at, updated_at 282 + FROM site_cache 283 + WHERE did = ${did} AND rkey = ${rkey} 284 + LIMIT 1 285 + `; 267 286 return result[0] || null; 268 287 } 288 + 269 289 270 290 export async function listSiteCachesForDid(did: string): Promise<SiteCache[]> { 271 291 return await sql<SiteCache[]>`
+79 -54
apps/hosting-service/src/lib/file-serving.ts
··· 21 21 import { fetchAndCacheSite } from './on-demand-cache'; 22 22 import type { StorageResult } from '@wispplace/tiered-storage'; 23 23 import { createLogger } from '@wispplace/observability'; 24 + import { createTrace, span, logTrace, type RequestTrace } from './trace'; 24 25 25 26 const logger = createLogger('file-serving'); 26 27 ··· 118 119 const content = Buffer.from(result.data); 119 120 const meta = result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined; 120 121 const mimeType = meta?.mimeType || lookup(filePath) || 'application/octet-stream'; 122 + const cacheControl = mimeType.startsWith('text/html') 123 + ? 'public, max-age=300' 124 + : 'public, max-age=31536000, immutable'; 125 + const etag = result.metadata.checksum ? `"${result.metadata.checksum}"` : undefined; 126 + 127 + // Handle conditional requests (If-None-Match → 304 Not Modified) 128 + if (etag && requestHeaders?.['if-none-match']) { 129 + const ifNoneMatch = requestHeaders['if-none-match']; 130 + const matches = ifNoneMatch === '*' || ifNoneMatch.split(',').map(e => e.trim()).includes(etag); 131 + if (matches) { 132 + return new Response(null, { 133 + status: 304, 134 + headers: { 'ETag': etag, 'Cache-Control': cacheControl }, 135 + }); 136 + } 137 + } 121 138 122 139 const headers: Record<string, string> = { 123 140 'Content-Type': mimeType, 124 - 'Cache-Control': mimeType.startsWith('text/html') 125 - ? 'public, max-age=300' 126 - : 'public, max-age=31536000, immutable', 141 + 'Cache-Control': cacheControl, 127 142 'X-Cache-Tier': result.source, 128 143 }; 144 + 145 + if (etag) { 146 + headers['ETag'] = etag; 147 + } 129 148 130 149 if (meta?.encoding === 'gzip') { 131 150 const shouldServeCompressed = shouldCompressMimeType(mimeType); ··· 161 180 fullUrl?: string, 162 181 headers?: Record<string, string> 163 182 ): Promise<Response> { 183 + const trace = createTrace(); 184 + 164 185 // Load settings for this site 165 - const settings = await getCachedSettings(did, rkey); 186 + const settings = await span(trace, 'db:settings', () => getCachedSettings(did, rkey)); 166 187 const indexFiles = getIndexFiles(settings); 167 188 168 189 // Check for redirect rules first (_redirects wins over settings) ··· 170 191 171 192 if (redirectRules === null) { 172 193 // Load rules (not in cache or evicted) 173 - redirectRules = await loadRedirectRules(did, rkey); 194 + redirectRules = await span(trace, 'storage:redirectRules', () => loadRedirectRules(did, rkey)); 174 195 setRedirectRulesInCache(did, rkey, redirectRules); 175 196 } 176 197 ··· 201 222 202 223 // If file exists and redirect is not forced, serve the file normally 203 224 if (fileExistsInStorage) { 204 - return serveFileInternal(did, rkey, filePath, settings, headers); 225 + const response = await serveFileInternal(did, rkey, filePath, settings, headers, trace); 226 + logTrace(trace, filePath || '/', logger); 227 + return response; 205 228 } 206 229 } 207 230 ··· 210 233 // Rewrite: serve different content but keep URL the same 211 234 // Remove leading slash for internal path resolution 212 235 const rewritePath = targetPath.startsWith('/') ? targetPath.slice(1) : targetPath; 213 - return serveFileInternal(did, rkey, rewritePath, settings, headers); 236 + const response = await serveFileInternal(did, rkey, rewritePath, settings, headers, trace); 237 + logTrace(trace, filePath || '/', logger); 238 + return response; 214 239 } else if (status === 301 || status === 302) { 215 240 // External redirect: change the URL 241 + logTrace(trace, filePath || '/', logger); 216 242 return new Response(null, { 217 243 status, 218 244 headers: { ··· 223 249 } else if (status === 404) { 224 250 // Custom 404 page from _redirects (wins over settings.custom404) 225 251 const custom404Path = targetPath.startsWith('/') ? targetPath.slice(1) : targetPath; 226 - const response = await serveFileInternal(did, rkey, custom404Path, settings, headers); 252 + const response = await serveFileInternal(did, rkey, custom404Path, settings, headers, trace); 253 + logTrace(trace, filePath || '/', logger); 227 254 // Override status to 404 228 255 return new Response(response.body, { 229 256 status: 404, ··· 234 261 } 235 262 236 263 // No redirect matched, serve normally with settings 237 - return serveFileInternal(did, rkey, filePath, settings, headers); 264 + const response = await serveFileInternal(did, rkey, filePath, settings, headers, trace); 265 + logTrace(trace, filePath || '/', logger); 266 + return response; 238 267 } 239 268 240 269 /** ··· 245 274 rkey: string, 246 275 filePath: string, 247 276 settings: WispSettings | null = null, 248 - requestHeaders?: Record<string, string> 277 + requestHeaders?: Record<string, string>, 278 + trace?: RequestTrace | null 249 279 ): Promise<Response> { 250 280 let expectedFileCids: Record<string, string> | null | undefined; 251 281 let expectedMissPath: string | null = null; 252 282 253 283 const getExpectedFileCids = async (): Promise<Record<string, string> | null> => { 254 284 if (expectedFileCids !== undefined) return expectedFileCids; 255 - const siteCache = await getSiteCache(did, rkey); 285 + const siteCache = await span(trace, 'db:siteCache', () => getSiteCache(did, rkey)); 256 286 if (!siteCache) { 257 287 expectedFileCids = null; 258 288 return null; ··· 291 321 if (!requestPath || !requestPath.includes('.')) { 292 322 for (const indexFile of indexFiles) { 293 323 const indexPath = requestPath ? `${requestPath}/${indexFile}` : indexFile; 294 - const result = await getFileWithMetadata(did, rkey, indexPath); 324 + const result = await span(trace, `storage:${indexPath}`, () => getFileWithMetadata(did, rkey, indexPath)); 295 325 if (result) { 296 326 return buildResponseFromStorageResult(result, indexPath, settings, requestHeaders); 297 327 } ··· 320 350 const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html'; 321 351 322 352 // Retrieve from tiered storage 323 - const result = await getFileWithMetadata(did, rkey, fileRequestPath); 353 + const result = await span(trace, `storage:${fileRequestPath}`, () => getFileWithMetadata(did, rkey, fileRequestPath)); 324 354 325 355 if (result) { 326 356 return buildResponseFromStorageResult(result, fileRequestPath, settings, requestHeaders); ··· 331 361 if (!fileRequestPath.includes('.')) { 332 362 for (const indexFileName of indexFiles) { 333 363 const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName; 334 - 335 - const indexResult = await getFileWithMetadata(did, rkey, indexPath); 336 - 364 + const indexResult = await span(trace, `storage:${indexPath}`, () => getFileWithMetadata(did, rkey, indexPath)); 337 365 if (indexResult) { 338 - const indexContent = Buffer.from(indexResult.data); 339 - const indexMeta = indexResult.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined; 340 - 341 - const headers: Record<string, string> = { 342 - 'Content-Type': 'text/html; charset=utf-8', 343 - 'Cache-Control': 'public, max-age=300', 344 - 'X-Cache-Tier': indexResult.source, 345 - }; 346 - 347 - if (indexMeta?.encoding === 'gzip') { 348 - headers['Content-Encoding'] = 'gzip'; 349 - } 350 - 351 - applyCustomHeaders(headers, indexPath, settings); 352 - return new Response(indexContent, { headers }); 366 + return buildResponseFromStorageResult(indexResult, indexPath, settings, requestHeaders); 353 367 } 354 368 await markExpectedMiss(indexPath); 355 369 } ··· 359 373 if (settings?.cleanUrls && !fileRequestPath.includes('.')) { 360 374 const htmlPath = `${fileRequestPath}.html`; 361 375 if (await storageExists(did, rkey, htmlPath)) { 362 - return serveFileInternal(did, rkey, htmlPath, settings, requestHeaders); 376 + return serveFileInternal(did, rkey, htmlPath, settings, requestHeaders, trace); 363 377 } 364 378 await markExpectedMiss(htmlPath); 365 379 ··· 367 381 for (const indexFileName of indexFiles) { 368 382 const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName; 369 383 if (await storageExists(did, rkey, indexPath)) { 370 - return serveFileInternal(did, rkey, indexPath, settings, requestHeaders); 384 + return serveFileInternal(did, rkey, indexPath, settings, requestHeaders, trace); 371 385 } 372 386 await markExpectedMiss(indexPath); 373 387 } ··· 377 391 if (settings?.spaMode) { 378 392 const spaFile = settings.spaMode; 379 393 if (await storageExists(did, rkey, spaFile)) { 380 - return serveFileInternal(did, rkey, spaFile, settings, requestHeaders); 394 + return serveFileInternal(did, rkey, spaFile, settings, requestHeaders, trace); 381 395 } 382 396 await markExpectedMiss(spaFile); 383 397 } ··· 386 400 if (settings?.custom404) { 387 401 const custom404File = settings.custom404; 388 402 if (await storageExists(did, rkey, custom404File)) { 389 - const response: Response = await serveFileInternal(did, rkey, custom404File, settings, requestHeaders); 403 + const response: Response = await serveFileInternal(did, rkey, custom404File, settings, requestHeaders, trace); 390 404 // Override status to 404 391 405 return new Response(response.body, { 392 406 status: 404, ··· 400 414 const auto404Pages = ['404.html', 'not_found.html']; 401 415 for (const auto404Page of auto404Pages) { 402 416 if (await storageExists(did, rkey, auto404Page)) { 403 - const response: Response = await serveFileInternal(did, rkey, auto404Page, settings, requestHeaders); 417 + const response: Response = await serveFileInternal(did, rkey, auto404Page, settings, requestHeaders, trace); 404 418 // Override status to 404 405 419 return new Response(response.body, { 406 420 status: 404, ··· 438 452 if (success) { 439 453 // Retry serving the originally requested file 440 454 const retryPath = filePath || indexFiles[0] || 'index.html'; 441 - const retryResult = await getFileWithMetadata(did, rkey, retryPath); 455 + const retryResult = await span(trace, `storage:${retryPath}`, () => getFileWithMetadata(did, rkey, retryPath)); 442 456 if (retryResult) { 443 457 return buildResponseFromStorageResult(retryResult, retryPath, settings, requestHeaders); 444 458 } ··· 467 481 fullUrl?: string, 468 482 headers?: Record<string, string> 469 483 ): Promise<Response> { 484 + const trace = createTrace(); 485 + 470 486 // Load settings for this site 471 - const settings = await getCachedSettings(did, rkey); 487 + const settings = await span(trace, 'db:settings', () => getCachedSettings(did, rkey)); 472 488 const indexFiles = getIndexFiles(settings); 473 489 474 490 // Check for redirect rules first (_redirects wins over settings) ··· 476 492 477 493 if (redirectRules === null) { 478 494 // Load rules (not in cache or evicted) 479 - redirectRules = await loadRedirectRules(did, rkey); 495 + redirectRules = await span(trace, 'storage:redirectRules', () => loadRedirectRules(did, rkey)); 480 496 setRedirectRulesInCache(did, rkey, redirectRules); 481 497 } 482 498 ··· 507 523 508 524 // If file exists and redirect is not forced, serve the file normally 509 525 if (fileExistsInStorage) { 510 - return serveFileInternalWithRewrite(did, rkey, filePath, basePath, settings, headers); 526 + const response = await serveFileInternalWithRewrite(did, rkey, filePath, basePath, settings, headers, trace); 527 + logTrace(trace, filePath || '/', logger); 528 + return response; 511 529 } 512 530 } 513 531 ··· 515 533 if (status === 200) { 516 534 // Rewrite: serve different content but keep URL the same 517 535 const rewritePath = targetPath.startsWith('/') ? targetPath.slice(1) : targetPath; 518 - return serveFileInternalWithRewrite(did, rkey, rewritePath, basePath, settings, headers); 536 + const response = await serveFileInternalWithRewrite(did, rkey, rewritePath, basePath, settings, headers, trace); 537 + logTrace(trace, filePath || '/', logger); 538 + return response; 519 539 } else if (status === 301 || status === 302) { 520 540 // External redirect: change the URL 521 541 // For sites.wisp.place, we need to adjust the target path to include the base path ··· 524 544 if (!targetPath.startsWith('http://') && !targetPath.startsWith('https://')) { 525 545 redirectTarget = basePath + (targetPath.startsWith('/') ? targetPath.slice(1) : targetPath); 526 546 } 547 + logTrace(trace, filePath || '/', logger); 527 548 return new Response(null, { 528 549 status, 529 550 headers: { ··· 534 555 } else if (status === 404) { 535 556 // Custom 404 page from _redirects (wins over settings.custom404) 536 557 const custom404Path = targetPath.startsWith('/') ? targetPath.slice(1) : targetPath; 537 - const response = await serveFileInternalWithRewrite(did, rkey, custom404Path, basePath, settings, headers); 558 + const response = await serveFileInternalWithRewrite(did, rkey, custom404Path, basePath, settings, headers, trace); 559 + logTrace(trace, filePath || '/', logger); 538 560 // Override status to 404 539 561 return new Response(response.body, { 540 562 status: 404, ··· 545 567 } 546 568 547 569 // No redirect matched, serve normally with settings 548 - return serveFileInternalWithRewrite(did, rkey, filePath, basePath, settings, headers); 570 + const response = await serveFileInternalWithRewrite(did, rkey, filePath, basePath, settings, headers, trace); 571 + logTrace(trace, filePath || '/', logger); 572 + return response; 549 573 } 550 574 551 575 /** ··· 557 581 filePath: string, 558 582 basePath: string, 559 583 settings: WispSettings | null = null, 560 - requestHeaders?: Record<string, string> 584 + requestHeaders?: Record<string, string>, 585 + trace?: RequestTrace | null 561 586 ): Promise<Response> { 562 587 let expectedFileCids: Record<string, string> | null | undefined; 563 588 let expectedMissPath: string | null = null; 564 589 565 590 const getExpectedFileCids = async (): Promise<Record<string, string> | null> => { 566 591 if (expectedFileCids !== undefined) return expectedFileCids; 567 - const siteCache = await getSiteCache(did, rkey); 592 + const siteCache = await span(trace, 'db:siteCache', () => getSiteCache(did, rkey)); 568 593 if (!siteCache) { 569 594 expectedFileCids = null; 570 595 return null; ··· 603 628 if (!requestPath || !requestPath.includes('.')) { 604 629 for (const indexFile of indexFiles) { 605 630 const indexPath = requestPath ? `${requestPath}/${indexFile}` : indexFile; 606 - const fileResult = await getFileForRequest(did, rkey, indexPath, true); 631 + const fileResult = await span(trace, `storage:${indexPath}`, () => getFileForRequest(did, rkey, indexPath, true)); 607 632 if (fileResult) { 608 633 return buildResponseFromStorageResult(fileResult.result, indexPath, settings, requestHeaders); 609 634 } ··· 631 656 // Not a directory, try to serve as a file 632 657 const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html'; 633 658 634 - const fileResult = await getFileForRequest(did, rkey, fileRequestPath, true); 659 + const fileResult = await span(trace, `storage:${fileRequestPath}`, () => getFileForRequest(did, rkey, fileRequestPath, true)); 635 660 if (fileResult) { 636 661 return buildResponseFromStorageResult(fileResult.result, fileRequestPath, settings, requestHeaders); 637 662 } ··· 641 666 if (!fileRequestPath.includes('.')) { 642 667 for (const indexFileName of indexFiles) { 643 668 const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName; 644 - const indexResult = await getFileForRequest(did, rkey, indexPath, true); 669 + const indexResult = await span(trace, `storage:${indexPath}`, () => getFileForRequest(did, rkey, indexPath, true)); 645 670 if (indexResult) { 646 671 return buildResponseFromStorageResult(indexResult.result, indexPath, settings, requestHeaders); 647 672 } ··· 653 678 if (settings?.cleanUrls && !fileRequestPath.includes('.')) { 654 679 const htmlPath = `${fileRequestPath}.html`; 655 680 if (await storageExists(did, rkey, htmlPath)) { 656 - return serveFileInternalWithRewrite(did, rkey, htmlPath, basePath, settings, requestHeaders); 681 + return serveFileInternalWithRewrite(did, rkey, htmlPath, basePath, settings, requestHeaders, trace); 657 682 } 658 683 await markExpectedMiss(htmlPath); 659 684 ··· 661 686 for (const indexFileName of indexFiles) { 662 687 const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName; 663 688 if (await storageExists(did, rkey, indexPath)) { 664 - return serveFileInternalWithRewrite(did, rkey, indexPath, basePath, settings, requestHeaders); 689 + return serveFileInternalWithRewrite(did, rkey, indexPath, basePath, settings, requestHeaders, trace); 665 690 } 666 691 await markExpectedMiss(indexPath); 667 692 } ··· 671 696 if (settings?.spaMode) { 672 697 const spaFile = settings.spaMode; 673 698 if (await storageExists(did, rkey, spaFile)) { 674 - return serveFileInternalWithRewrite(did, rkey, spaFile, basePath, settings, requestHeaders); 699 + return serveFileInternalWithRewrite(did, rkey, spaFile, basePath, settings, requestHeaders, trace); 675 700 } 676 701 await markExpectedMiss(spaFile); 677 702 } ··· 680 705 if (settings?.custom404) { 681 706 const custom404File = settings.custom404; 682 707 if (await storageExists(did, rkey, custom404File)) { 683 - const response: Response = await serveFileInternalWithRewrite(did, rkey, custom404File, basePath, settings, requestHeaders); 708 + const response: Response = await serveFileInternalWithRewrite(did, rkey, custom404File, basePath, settings, requestHeaders, trace); 684 709 // Override status to 404 685 710 return new Response(response.body, { 686 711 status: 404, ··· 694 719 const auto404Pages = ['404.html', 'not_found.html']; 695 720 for (const auto404Page of auto404Pages) { 696 721 if (await storageExists(did, rkey, auto404Page)) { 697 - const response: Response = await serveFileInternalWithRewrite(did, rkey, auto404Page, basePath, settings, requestHeaders); 722 + const response: Response = await serveFileInternalWithRewrite(did, rkey, auto404Page, basePath, settings, requestHeaders, trace); 698 723 // Override status to 404 699 724 return new Response(response.body, { 700 725 status: 404, ··· 732 757 if (success) { 733 758 // Retry serving the originally requested file 734 759 const retryPath = filePath || indexFiles[0] || 'index.html'; 735 - const retryResult = await getFileWithMetadata(did, rkey, retryPath); 760 + const retryResult = await span(trace, `storage:${retryPath}`, () => getFileWithMetadata(did, rkey, retryPath)); 736 761 if (retryResult) { 737 762 return buildResponseFromStorageResult(retryResult, retryPath, settings, requestHeaders); 738 763 }
+53
apps/hosting-service/src/lib/trace.ts
··· 1 + /** 2 + * Lightweight request tracing, toggled via TRACE_REQUESTS=true. 3 + * 4 + * Usage: 5 + * const trace = createTrace(); 6 + * const result = await span(trace, 'db:settings', () => getCachedSettings(...)); 7 + * logTrace(trace, 'GET /index.html', logger); 8 + */ 9 + 10 + export const TRACE_ENABLED = process.env.TRACE_REQUESTS === 'true'; 11 + 12 + export interface Span { 13 + name: string; 14 + durationMs: number; 15 + } 16 + 17 + export interface RequestTrace { 18 + spans: Span[]; 19 + startMs: number; 20 + } 21 + 22 + export function createTrace(): RequestTrace | null { 23 + if (!TRACE_ENABLED) return null; 24 + return { spans: [], startMs: performance.now() }; 25 + } 26 + 27 + export async function span<T>( 28 + trace: RequestTrace | null | undefined, 29 + name: string, 30 + fn: () => Promise<T> 31 + ): Promise<T> { 32 + if (!trace) return fn(); 33 + const t0 = performance.now(); 34 + try { 35 + return await fn(); 36 + } finally { 37 + trace.spans.push({ name, durationMs: +(performance.now() - t0).toFixed(2) }); 38 + } 39 + } 40 + 41 + export function logTrace( 42 + trace: RequestTrace | null, 43 + label: string, 44 + logger: { info: (msg: string, ctx?: Record<string, unknown>) => void } 45 + ) { 46 + if (!trace) return; 47 + const totalMs = +(performance.now() - trace.startMs).toFixed(2); 48 + const breakdown: Record<string, unknown> = { total: totalMs }; 49 + for (const s of trace.spans) { 50 + breakdown[s.name] = s.durationMs; 51 + } 52 + logger.info(`TRACE ${label}`, breakdown); 53 + }
+17 -2
apps/hosting-service/src/server.ts
··· 16 16 17 17 const logger = createLogger('hosting-service'); 18 18 19 + // Cache handle → DID resolutions for 10 minutes to avoid hitting bsky API on every request 20 + const HANDLE_CACHE_TTL = 10 * 60 * 1000; 21 + const handleCache = new Map<string, { did: string; timestamp: number }>(); 22 + 23 + async function resolveDidCached(identifier: string): Promise<string | null> { 24 + if (identifier.startsWith('did:')) return identifier; 25 + const cached = handleCache.get(identifier); 26 + if (cached && Date.now() - cached.timestamp < HANDLE_CACHE_TTL) { 27 + return cached.did; 28 + } 29 + const did = await resolveDid(identifier); 30 + if (did) handleCache.set(identifier, { did, timestamp: Date.now() }); 31 + return did; 32 + } 33 + 19 34 const BASE_HOST_ENV = process.env.BASE_HOST || 'wisp.place'; 20 35 const BASE_HOST = BASE_HOST_ENV.split(':')[0] || BASE_HOST_ENV; 21 36 ··· 26 41 origin: '*', 27 42 allowMethods: ['GET', 'HEAD', 'OPTIONS'], 28 43 allowHeaders: ['Content-Type', 'Authorization'], 29 - exposeHeaders: ['Content-Length', 'Content-Type', 'Content-Encoding', 'Cache-Control'], 44 + exposeHeaders: ['Content-Length', 'Content-Type', 'Content-Encoding', 'Cache-Control', 'ETag'], 30 45 maxAge: 86400, // 24 hours 31 46 credentials: false, 32 47 })); ··· 78 93 } 79 94 80 95 // Resolve identifier to DID 81 - const did = await resolveDid(identifier); 96 + const did = await resolveDidCached(identifier); 82 97 if (!did) { 83 98 return c.text('Invalid identifier', 400); 84 99 }