sui.ski 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +311 -0
  2. package/CLAUDE.md +292 -0
  3. package/CODEBASE_GUIDE.md +217 -0
  4. package/README.md +77 -0
  5. package/biome.json +28 -0
  6. package/package.json +73 -0
  7. package/scripts/deploy-messaging-mainnet.sh +184 -0
  8. package/scripts/extract-suins-object.ts +180 -0
  9. package/scripts/full-deploy.sh +26 -0
  10. package/scripts/obsidian.ts +243 -0
  11. package/scripts/set-suins-contenthash.ts +130 -0
  12. package/scripts/setup-ika-dwallet.ts +338 -0
  13. package/scripts/transfer-upgrade-cap-from-nft.ts +86 -0
  14. package/src/durable-objects/wallet-session.ts +333 -0
  15. package/src/handlers/app.ts +1430 -0
  16. package/src/handlers/authenticated-events.ts +267 -0
  17. package/src/handlers/dashboard.ts +1659 -0
  18. package/src/handlers/landing.ts +6751 -0
  19. package/src/handlers/mcp.ts +556 -0
  20. package/src/handlers/messaging-sdk.ts +220 -0
  21. package/src/handlers/profile.css.ts +9332 -0
  22. package/src/handlers/profile.ts +12640 -0
  23. package/src/handlers/register2.ts +2811 -0
  24. package/src/handlers/ski-sign.ts +1901 -0
  25. package/src/handlers/ski.ts +314 -0
  26. package/src/handlers/thunder.ts +940 -0
  27. package/src/handlers/vault.ts +284 -0
  28. package/src/handlers/wallet-api.ts +169 -0
  29. package/src/handlers/x402-register.ts +601 -0
  30. package/src/index.test.ts +55 -0
  31. package/src/index.ts +512 -0
  32. package/src/resolvers/content.ts +231 -0
  33. package/src/resolvers/rpc.ts +222 -0
  34. package/src/resolvers/suins.ts +266 -0
  35. package/src/sdk/messaging.ts +279 -0
  36. package/src/types.ts +230 -0
  37. package/src/utils/agent-keypair.ts +40 -0
  38. package/src/utils/authenticated-events.ts +280 -0
  39. package/src/utils/cache.ts +82 -0
  40. package/src/utils/media-pack.ts +27 -0
  41. package/src/utils/mmr.ts +181 -0
  42. package/src/utils/ns-price.ts +529 -0
  43. package/src/utils/og-image.ts +141 -0
  44. package/src/utils/onchain-activity.ts +211 -0
  45. package/src/utils/onchain-listing.ts +39 -0
  46. package/src/utils/premium.ts +29 -0
  47. package/src/utils/pricing.ts +291 -0
  48. package/src/utils/pyth-price-info.ts +63 -0
  49. package/src/utils/response.ts +204 -0
  50. package/src/utils/rpc.ts +25 -0
  51. package/src/utils/shared-wallet-js.ts +166 -0
  52. package/src/utils/social.ts +152 -0
  53. package/src/utils/status.ts +39 -0
  54. package/src/utils/subdomain.ts +116 -0
  55. package/src/utils/surflux-grpc.ts +241 -0
  56. package/src/utils/swap-transactions.ts +1222 -0
  57. package/src/utils/thunder-css.ts +1341 -0
  58. package/src/utils/thunder-js.ts +5046 -0
  59. package/src/utils/transactions.ts +65 -0
  60. package/src/utils/vault.ts +18 -0
  61. package/src/utils/wallet-kit-js.ts +2312 -0
  62. package/src/utils/wallet-session-js.ts +192 -0
  63. package/src/utils/wallet-tx-js.ts +2287 -0
  64. package/src/utils/wallet-ui-js.ts +3057 -0
  65. package/src/utils/x402-middleware.ts +428 -0
  66. package/src/utils/x402-sui.ts +171 -0
  67. package/src/utils/zksend-js.ts +166 -0
  68. package/tsconfig.json +22 -0
  69. package/workers/x402-multichain/src/index.ts +237 -0
  70. package/workers/x402-multichain/src/types.ts +80 -0
  71. package/workers/x402-multichain/tsconfig.json +20 -0
  72. package/workers/x402-multichain/wrangler.toml +11 -0
  73. package/wrangler.toml +84 -0
package/src/index.ts ADDED
@@ -0,0 +1,512 @@
1
+ import { Hono } from 'hono'
2
+ import { WalletSession } from './durable-objects/wallet-session'
3
+ import { handleAppRequest } from './handlers/app'
4
+ import { handleAuthenticatedEvents } from './handlers/authenticated-events'
5
+ import { generateDashboardPage } from './handlers/dashboard'
6
+ import {
7
+ apiRoutes,
8
+ cloudflareGracePageHTML,
9
+ generateCancelBidPage,
10
+ landingPageHTML,
11
+ } from './handlers/landing'
12
+ import { createSuiMcpHandler } from './handlers/mcp'
13
+ import { handleMessagingApi } from './handlers/messaging-sdk'
14
+ import { generateEmbedProfilePage, generateProfilePage } from './handlers/profile'
15
+ import { handleBuildRegisterTx, handleRegistrationSubmission } from './handlers/register2'
16
+ import { generateSkiPage } from './handlers/ski'
17
+ import { generateSkiSignPage } from './handlers/ski-sign'
18
+ import { vaultRoutes } from './handlers/vault'
19
+ import {
20
+ handleWalletChallenge,
21
+ handleWalletCheck,
22
+ handleWalletConnect,
23
+ handleWalletDisconnect,
24
+ } from './handlers/wallet-api'
25
+ import { thunderRoutes } from './handlers/thunder'
26
+ import { x402RegisterRoutes } from './handlers/x402-register'
27
+ import { resolveContent, resolveDirectContent } from './resolvers/content'
28
+ import { handleRPCRequest } from './resolvers/rpc'
29
+ import { resolveSuiNS } from './resolvers/suins'
30
+ import type { Env, ParsedSubdomain, SuiNSRecord } from './types'
31
+ import {
32
+ generateDotSkiPngBytes,
33
+ generateSkiLogoSvg,
34
+ generateSuiIconSvg,
35
+ generateThunderIconBytes,
36
+ } from './utils/media-pack'
37
+ import {
38
+ generateBrandOgPng,
39
+ generateBrandOgSvg,
40
+ generateFaviconSvg,
41
+ generateProfileOgSvg,
42
+ } from './utils/og-image'
43
+ import { errorResponse, htmlResponse, jsonResponse, notFoundPage } from './utils/response'
44
+ import { ensureRpcEnv } from './utils/rpc'
45
+ import { isTwitterPreviewBot } from './utils/social'
46
+ import { parseSubdomain } from './utils/subdomain'
47
+
48
+ export { WalletSession }
49
+
50
+ type AppEnv = {
51
+ Bindings: Env
52
+ Variables: {
53
+ parsed: ParsedSubdomain
54
+ hostname: string
55
+ env: Env
56
+ session: {
57
+ address: string | null
58
+ walletName: string | null
59
+ verified: boolean
60
+ }
61
+ }
62
+ }
63
+
64
+ const app = new Hono<AppEnv>()
65
+
66
+ app.use('*', async (c, next) => {
67
+ if (c.req.method === 'OPTIONS') {
68
+ const requestedHeaders = c.req.header('Access-Control-Request-Headers') || 'Content-Type'
69
+ return new Response(null, {
70
+ headers: {
71
+ 'Access-Control-Allow-Origin': '*',
72
+ 'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, DELETE, OPTIONS',
73
+ 'Access-Control-Allow-Headers': requestedHeaders,
74
+ 'Access-Control-Max-Age': '86400',
75
+ },
76
+ })
77
+ }
78
+ await next()
79
+ })
80
+
81
+ app.use('*', async (c, next) => {
82
+ const url = new URL(c.req.url)
83
+ const testHost = url.searchParams.get('host') || c.req.header('X-Host')
84
+ const hostname = testHost || url.hostname
85
+ const parsed = parseSubdomain(hostname)
86
+
87
+ let env = c.env
88
+ if (parsed.networkOverride) {
89
+ env = { ...env, SUI_NETWORK: parsed.networkOverride, SUI_RPC_URL: '' }
90
+ if (parsed.networkOverride === 'testnet') {
91
+ env = {
92
+ ...env,
93
+ SEAL_PACKAGE_ID: env.SEAL_TESTNET_PACKAGE_ID || env.SEAL_PACKAGE_ID,
94
+ SEAL_KEY_SERVERS: env.SEAL_TESTNET_KEY_SERVERS || env.SEAL_KEY_SERVERS,
95
+ SEAL_APPROVE_TARGET: env.SEAL_TESTNET_APPROVE_TARGET || env.SEAL_APPROVE_TARGET,
96
+ }
97
+ }
98
+ }
99
+ c.set('env', ensureRpcEnv(env))
100
+ c.set('parsed', parsed)
101
+ c.set('hostname', hostname)
102
+ await next()
103
+ })
104
+
105
+ const SESSION_ROUTES = new Set(['root', 'suins'])
106
+
107
+ function getCookieValue(cookieHeader: string, name: string): string | null {
108
+ if (!cookieHeader) return null
109
+ const parts = cookieHeader.split(';')
110
+ for (let i = 0; i < parts.length; i++) {
111
+ const part = parts[i].trim()
112
+ if (!part) continue
113
+ const eqIndex = part.indexOf('=')
114
+ if (eqIndex <= 0) continue
115
+ if (part.slice(0, eqIndex).trim() !== name) continue
116
+ return part.slice(eqIndex + 1).trim()
117
+ }
118
+ return null
119
+ }
120
+
121
+ function decodeCookieValue(value: string | null): string | null {
122
+ if (!value) return null
123
+ try {
124
+ return decodeURIComponent(value)
125
+ } catch {
126
+ return value
127
+ }
128
+ }
129
+
130
+ function routeNeedsSession(parsed: ParsedSubdomain, pathname: string): boolean {
131
+ if (!SESSION_ROUTES.has(parsed.type)) return false
132
+ if (pathname.startsWith('/api/') || pathname === '/favicon.svg') return false
133
+ if (
134
+ pathname.startsWith('/og/') ||
135
+ pathname.startsWith('/walrus/') ||
136
+ pathname.startsWith('/ipfs/')
137
+ )
138
+ return false
139
+ return true
140
+ }
141
+
142
+ app.use('*', async (c, next) => {
143
+ const cookieHeader = c.req.header('Cookie') || ''
144
+ const sessionId = decodeCookieValue(getCookieValue(cookieHeader, 'session_id'))
145
+ const walletAddress = decodeCookieValue(getCookieValue(cookieHeader, 'wallet_address'))
146
+ const walletName = decodeCookieValue(getCookieValue(cookieHeader, 'wallet_name'))
147
+
148
+ const session = {
149
+ address: walletAddress || null,
150
+ walletName: walletName || null,
151
+ verified: false,
152
+ }
153
+
154
+ if (sessionId) {
155
+ const parsed = c.get('parsed')
156
+ const url = new URL(c.req.url)
157
+ if (routeNeedsSession(parsed, url.pathname)) {
158
+ const stub = c.env.WALLET_SESSIONS.getByName('global')
159
+ const info = await stub.getSessionInfo(sessionId)
160
+ if (info) {
161
+ session.address = info.address
162
+ session.verified = info.verified
163
+ }
164
+ }
165
+ }
166
+
167
+ c.set('session', session)
168
+ await next()
169
+ })
170
+
171
+ app.post('/api/wallet/challenge', async (c) => {
172
+ return handleWalletChallenge(c.req.raw, c.env)
173
+ })
174
+
175
+ app.post('/api/wallet/connect', async (c) => {
176
+ return handleWalletConnect(c.req.raw, c.env)
177
+ })
178
+
179
+ app.get('/api/wallet/check', async (c) => {
180
+ return handleWalletCheck(c.req.raw, c.env)
181
+ })
182
+
183
+ app.post('/api/wallet/disconnect', async (c) => {
184
+ return handleWalletDisconnect(c.req.raw, c.env)
185
+ })
186
+
187
+ app.post('/api/register/build-tx', async (c) => {
188
+ return handleBuildRegisterTx(c.req.raw, c.get('env'))
189
+ })
190
+
191
+ app.post('/api/register/submit', async (c) => {
192
+ return handleRegistrationSubmission(c.req.raw, c.get('env'))
193
+ })
194
+
195
+ app.use('*', async (c, next) => {
196
+ const parsed = c.get('parsed')
197
+ const env = c.get('env')
198
+
199
+ switch (parsed.type) {
200
+ case 'rpc':
201
+ return handleRPCRequest(c.req.raw, env)
202
+ case 'app':
203
+ return handleAppRequest(c.req.raw, env, c.get('session'))
204
+ case 'dashboard':
205
+ return htmlResponse(generateDashboardPage(env))
206
+ case 'content': {
207
+ const result = await resolveDirectContent(parsed.subdomain, env)
208
+ if (!result.found) return errorResponse(result.error || 'Content not found', 'NOT_FOUND', 404)
209
+ return result.data as Response
210
+ }
211
+ case 'mvr': {
212
+ const mvrInfo = parsed.mvrInfo
213
+ if (!mvrInfo) return errorResponse('Missing MVR info', 'INVALID_MVR', 400)
214
+ return jsonResponse({
215
+ mvrPackage: `@${mvrInfo.suinsName}/${mvrInfo.packageName}`,
216
+ version: mvrInfo.version || 'latest',
217
+ message: 'MVR package resolution coming soon',
218
+ })
219
+ }
220
+ default:
221
+ await next()
222
+ }
223
+ })
224
+
225
+ app.all('/api/events/*', async (c) => handleAuthenticatedEvents(c.req.raw, c.get('env')))
226
+ app.all('/api/app/*', async (c) => handleAppRequest(c.req.raw, c.get('env'), c.get('session')))
227
+ app.use('/api/agents/x402-register/*', async (c, next) => {
228
+ if (c.get('parsed').type !== 'root') return c.notFound()
229
+ await next()
230
+ })
231
+ app.route('/api/agents/x402-register', x402RegisterRoutes)
232
+ app.use('/api/thunder', async (c, next) => {
233
+ if (c.get('parsed').type !== 'root') return c.notFound()
234
+ await next()
235
+ })
236
+ app.use('/api/thunder/*', async (c, next) => {
237
+ if (c.get('parsed').type !== 'root') return c.notFound()
238
+ await next()
239
+ })
240
+ app.route('/api/thunder', thunderRoutes)
241
+ app.all('/api/agents/*', async (c) => handleAppRequest(c.req.raw, c.get('env'), c.get('session')))
242
+ app.all('/api/ika/*', async (c) => handleAppRequest(c.req.raw, c.get('env'), c.get('session')))
243
+ app.all('/api/llm/*', async (c) => handleAppRequest(c.req.raw, c.get('env'), c.get('session')))
244
+ app.all('/api/messaging/*', async (c) =>
245
+ handleMessagingApi(c.req.raw, c.get('env'), new URL(c.req.url)),
246
+ )
247
+
248
+ app.route('/api/vault', vaultRoutes)
249
+ app.route('/api', apiRoutes)
250
+
251
+ const SVG_HEADERS = { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=604800' }
252
+
253
+ app.get('/favicon.svg', () => new Response(generateFaviconSvg(), { headers: SVG_HEADERS }))
254
+
255
+ app.get('/og-image.svg', () => new Response(generateBrandOgSvg(), { headers: SVG_HEADERS }))
256
+
257
+ app.get(
258
+ '/media-pack/SuiIcon.svg',
259
+ () => new Response(generateSuiIconSvg(), { headers: SVG_HEADERS }),
260
+ )
261
+
262
+ app.get(
263
+ '/media-pack/skilogo.svg',
264
+ () => new Response(generateSkiLogoSvg(), { headers: SVG_HEADERS }),
265
+ )
266
+
267
+ const PNG_HEADERS = { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=604800' }
268
+
269
+ app.get(
270
+ '/media-pack/dotSKI.png',
271
+ () =>
272
+ new Response(generateDotSkiPngBytes(), {
273
+ headers: {
274
+ 'Content-Type': 'image/webp',
275
+ 'Cache-Control': 'public, max-age=604800',
276
+ },
277
+ }),
278
+ )
279
+
280
+ app.get(
281
+ '/media-pack/ThunderIcon.png',
282
+ () =>
283
+ new Response(generateThunderIconBytes(), {
284
+ headers: {
285
+ 'Content-Type': 'image/webp',
286
+ 'Cache-Control': 'public, max-age=604800',
287
+ },
288
+ }),
289
+ )
290
+
291
+ app.get('/og-image.png', () => new Response(generateBrandOgPng(), { headers: PNG_HEADERS }))
292
+
293
+ app.get('/og/:name{.+\\.svg}', (c) => {
294
+ const rawName = c.req.param('name').replace(/\.svg$/, '')
295
+ const name = decodeURIComponent(rawName)
296
+ return new Response(generateProfileOgSvg(name, ''), { headers: SVG_HEADERS })
297
+ })
298
+
299
+ app.get('/walrus/:id{.+}', async (c) => {
300
+ if (c.get('parsed').type !== 'root') return c.notFound()
301
+ const subdomain = `walrus-${c.req.param('id')}`
302
+ const result = await resolveDirectContent(subdomain, c.get('env'))
303
+ if (!result.found) return errorResponse(result.error || 'Content not found', 'NOT_FOUND', 404)
304
+ return result.data as Response
305
+ })
306
+
307
+ app.get('/ipfs/:id{.+}', async (c) => {
308
+ if (c.get('parsed').type !== 'root') return c.notFound()
309
+ const subdomain = `ipfs-${c.req.param('id')}`
310
+ const result = await resolveDirectContent(subdomain, c.get('env'))
311
+ if (!result.found) return errorResponse(result.error || 'Content not found', 'NOT_FOUND', 404)
312
+ return result.data as Response
313
+ })
314
+
315
+ app.get('/in', async (c) => {
316
+ return htmlResponse(generateSkiPage(c.get('env'), c.get('session')))
317
+ })
318
+
319
+ app.get('/sign', async (c) => {
320
+ return htmlResponse(generateSkiSignPage(c.get('env')), 200, {
321
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
322
+ Pragma: 'no-cache',
323
+ })
324
+ })
325
+
326
+ app.get('/cancel-bid', async (c) => {
327
+ if (c.get('parsed').type !== 'root') return c.notFound()
328
+ const bidId = new URL(c.req.url).searchParams.get('bid') || ''
329
+ const env = c.get('env')
330
+ return htmlResponse(generateCancelBidPage(env, bidId), 200, {
331
+ 'Cache-Control': 'no-store',
332
+ })
333
+ })
334
+
335
+ app.get('/cloudflare', async (c) => {
336
+ if (c.get('parsed').type !== 'root') return c.notFound()
337
+ return htmlResponse(cloudflareGracePageHTML(c.get('env').SUI_NETWORK))
338
+ })
339
+
340
+ app.get('/grace', async (c) => {
341
+ if (c.get('parsed').type !== 'root') return c.notFound()
342
+ return htmlResponse(cloudflareGracePageHTML(c.get('env').SUI_NETWORK))
343
+ })
344
+
345
+ app.all('/app', async (c) => {
346
+ if (c.get('parsed').type !== 'root') return c.notFound()
347
+ return handleAppRequest(c.req.raw, c.get('env'), c.get('session'))
348
+ })
349
+
350
+ app.all('/app/*', async (c) => {
351
+ if (c.get('parsed').type !== 'root') return c.notFound()
352
+ return handleAppRequest(c.req.raw, c.get('env'), c.get('session'))
353
+ })
354
+
355
+ app.all('/mcp', async (c) => {
356
+ if (c.get('parsed').type !== 'root') return c.notFound()
357
+ const handler = createSuiMcpHandler(c.get('env'))
358
+ return handler(c.req.raw, c.env, c.executionCtx)
359
+ })
360
+
361
+ app.all('*', async (c) => {
362
+ const parsed = c.get('parsed')
363
+ const env = c.get('env')
364
+ const url = new URL(c.req.url)
365
+
366
+ if (parsed.type === 'root') {
367
+ const session = c.get('session')
368
+ const hasSession = !!session.address
369
+ if (!hasSession) {
370
+ const cache = caches.default
371
+ const landingCacheUrl = `https://cache.internal/landing:${url.hostname}${url.pathname || '/'}`
372
+ const cached = await cache.match(landingCacheUrl)
373
+ if (cached) return cached
374
+ }
375
+ const canonicalUrl = `${url.protocol}//${url.hostname}${url.pathname || '/'}`
376
+ const response = htmlResponse(
377
+ landingPageHTML(env.SUI_NETWORK, {
378
+ canonicalUrl,
379
+ rpcUrl: env.SUI_RPC_URL,
380
+ network: env.SUI_NETWORK,
381
+ session: c.get('session'),
382
+ }),
383
+ 200,
384
+ hasSession ? {} : { 'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=300' },
385
+ )
386
+ if (!hasSession) {
387
+ const cache = caches.default
388
+ const landingCacheUrl = `https://cache.internal/landing:${url.hostname}${url.pathname || '/'}`
389
+ c.executionCtx.waitUntil(cache.put(landingCacheUrl, response.clone()))
390
+ }
391
+ return response
392
+ }
393
+
394
+ if (parsed.type === 'suins') {
395
+ if (url.pathname === '/favicon.svg') {
396
+ return new Response(generateFaviconSvg(), { headers: SVG_HEADERS })
397
+ }
398
+ if (url.pathname === '/og-image.svg') {
399
+ return new Response(generateBrandOgSvg(), { headers: SVG_HEADERS })
400
+ }
401
+ if (url.pathname.startsWith('/og/') && url.pathname.endsWith('.svg')) {
402
+ const nameSlug = url.pathname.slice(4, -4)
403
+ const ogResult = await resolveSuiNS(parsed.subdomain, env)
404
+ const ogAddr =
405
+ ogResult.found && ogResult.data && 'address' in ogResult.data
406
+ ? (ogResult.data as SuiNSRecord).address
407
+ : ''
408
+ return new Response(generateProfileOgSvg(decodeURIComponent(nameSlug), ogAddr), {
409
+ headers: SVG_HEADERS,
410
+ })
411
+ }
412
+
413
+ const hostname = c.get('hostname')
414
+ const userAgent = c.req.header('user-agent')
415
+ const skipCache = url.searchParams.has('nocache') || url.searchParams.has('refresh')
416
+ const hasSession = !!c.get('session').address
417
+ const wantsJson = url.pathname === '/json' || url.searchParams.has('json')
418
+ const wantsProfile = url.pathname === '/home' || url.searchParams.has('profile')
419
+ const wantsEmbed = url.searchParams.has('embed')
420
+
421
+ const canServeCached = !skipCache && !hasSession && !wantsJson && !wantsProfile
422
+ if (canServeCached) {
423
+ const cache = caches.default
424
+ const cacheUrl = new URL(`https://cache.internal/profile:${hostname}${url.pathname || '/'}`)
425
+ const cached = await cache.match(cacheUrl.toString())
426
+ if (cached) return cached
427
+ }
428
+
429
+ const result = await resolveSuiNS(parsed.subdomain, env, skipCache)
430
+
431
+ if (!result.found)
432
+ return notFoundPage(parsed.subdomain, env, result.available, c.get('session'))
433
+
434
+ const record = result.data as SuiNSRecord
435
+ const normalizedPath = url.pathname || '/'
436
+ const canonicalUrl = `${url.protocol}//${hostname}${normalizedPath}`
437
+ const profileOptions = {
438
+ canonicalUrl,
439
+ hostname,
440
+ inGracePeriod: result.inGracePeriod || false,
441
+ session: c.get('session'),
442
+ }
443
+ const shouldServeProfileForTwitter =
444
+ isTwitterPreviewBot(userAgent ?? null) && normalizedPath === '/'
445
+ let cachedProfileHtml: string | null = null
446
+ const renderProfile = () => {
447
+ if (cachedProfileHtml === null) {
448
+ cachedProfileHtml = generateProfilePage(parsed.subdomain, record, env, profileOptions)
449
+ }
450
+ return cachedProfileHtml
451
+ }
452
+
453
+ if (wantsEmbed) {
454
+ const embedHtml = generateEmbedProfilePage(parsed.subdomain, record, env, hostname)
455
+ return htmlResponse(embedHtml, 200, {
456
+ 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
457
+ })
458
+ }
459
+
460
+ if (wantsJson) return jsonResponse(record)
461
+ if (wantsProfile)
462
+ return htmlResponse(renderProfile(), 200, {
463
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
464
+ Pragma: 'no-cache',
465
+ })
466
+ if (shouldServeProfileForTwitter)
467
+ return htmlResponse(renderProfile(), 200, {
468
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
469
+ Pragma: 'no-cache',
470
+ })
471
+
472
+ if (record.content) {
473
+ const contentResponse = await resolveContent(record.content, env)
474
+ if (!contentResponse.ok && (url.pathname === '/' || url.pathname === '')) {
475
+ const profileResponse = htmlResponse(renderProfile(), 200, {
476
+ 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
477
+ })
478
+ if (canServeCached) {
479
+ const cache = caches.default
480
+ const cacheUrl = new URL(
481
+ `https://cache.internal/profile:${hostname}${url.pathname || '/'}`,
482
+ )
483
+ c.executionCtx.waitUntil(cache.put(cacheUrl.toString(), profileResponse.clone()))
484
+ }
485
+ return profileResponse
486
+ }
487
+ return contentResponse
488
+ }
489
+
490
+ const profileResponse = htmlResponse(renderProfile(), 200, {
491
+ 'Cache-Control': canServeCached
492
+ ? 'public, s-maxage=60, stale-while-revalidate=300'
493
+ : 'no-store, no-cache, must-revalidate',
494
+ })
495
+ if (canServeCached) {
496
+ const cache = caches.default
497
+ const cacheUrl = new URL(`https://cache.internal/profile:${hostname}${url.pathname || '/'}`)
498
+ c.executionCtx.waitUntil(cache.put(cacheUrl.toString(), profileResponse.clone()))
499
+ }
500
+ return profileResponse
501
+ }
502
+
503
+ return errorResponse('Unknown route type', 'UNKNOWN_ROUTE', 400)
504
+ })
505
+
506
+ app.onError((err, _c) => {
507
+ console.error('Gateway error:', err)
508
+ const message = err instanceof Error ? err.message : 'Unknown error'
509
+ return errorResponse(`Gateway error: ${message}`, 'GATEWAY_ERROR', 500)
510
+ })
511
+
512
+ export default app