1import type {2 AliasInput,3 AliasRecord,4 AliasResponse,5 Diagram,6 LockedAliasInput,7 LockedAliasResponse,8 OpenAliasInput,9 OpenAliasResponse,10} from './model'11import { capabilityMatches, contentId, isAliasId, randomAliasId, writeCapabilityHash } from './crypto'12import {13 canonicalJson,14 normalizeAliasInput,15 normalizeEncryptedBlob,16 normalizeOpenAliasInput,17 readJsonBody,18 RequestError,19} from './validation'20import {21 APPEARANCES,22 MATERIALIZER_BUILD,23 PALETTE_BUILD,24 SVG_SCHEMA,25 SvgNormalizationError,26 materializeSvg,27 type SvgAppearance,28} from '../../../shared/svg/index.js'2930const API_PREFIX = '/api/v1'31const encoder = new TextEncoder()32const aliasPathPattern = /^\/api\/v1\/aliases\/([A-Za-z0-9_-]{16})\/?$/33const renderPathPattern = /^\/api\/v1\/aliases\/([A-Za-z0-9_-]{16})\/renders\/(svg|png)\/?$/34const MAX_RENDER_BYTES = 12 * 1024 * 102435const rendererUnitPattern = /^[a-z][a-z0-9-]{0,31}$/36const rendererBuildPattern = /^[A-Za-z0-9][A-Za-z0-9._@+-]{0,127}$/3738function normalizationError(error: unknown): never {39 if (error instanceof SvgNormalizationError) {40 throw new RequestError(error.status, error.code, error.message)41 }42 throw error43}4445function requestedAppearance(request: Request, format: 'svg' | 'png'): SvgAppearance {46 const value = new URL(request.url).searchParams.get('appearance') ?? 'raw'47 if (!APPEARANCES.some(appearance => appearance === value)) {48 throw new RequestError(400, 'invalid_appearance', `Unknown SVG appearance: ${value}.`)49 }50 if (format !== 'svg' && value !== 'raw') {51 throw new RequestError(400, 'appearance_requires_svg', 'SVG appearances are only available for SVG renders.')52 }53 return value as SvgAppearance54}5556function canonicalSvg(bytes: Uint8Array, appearance: SvgAppearance): Uint8Array {57 let source: string58 try {59 source = new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(bytes)60 } catch {61 throw new RequestError(422, 'invalid_svg', 'Rendered SVG must use valid UTF-8.')62 }63 try {64 return encoder.encode(materializeSvg(source, appearance))65 } catch (error) {66 normalizationError(error)67 }68}6970interface RendererIdentity {71 unit: string72 build: string73 pipeline: string[]74}7576interface RenderHead extends RendererIdentity {77 objectKey: string78}7980function responseHeaders(extra?: HeadersInit): Headers {81 const headers = new Headers(extra)82 headers.set('Cache-Control', 'no-store')83 headers.set('Content-Security-Policy', "default-src 'none'")84 headers.set('X-Content-Type-Options', 'nosniff')85 return headers86}8788function json(value: unknown, init: ResponseInit = {}): Response {89 const headers = responseHeaders(init.headers)90 headers.set('Content-Type', 'application/json; charset=utf-8')91 return Response.json(value, { ...init, headers })92}9394function errorResponse(error: RequestError): Response {95 const headers = error.status === 401 ? { 'WWW-Authenticate': 'Bearer' } : undefined96 return json({ error: { code: error.code, message: error.message } }, { status: error.status, headers })97}9899function parseRevision(request: Request): number {100 const match = request.headers.get('If-Match')?.match(/^"([1-9][0-9]*)"$/)101 if (!match?.[1]) {102 throw new RequestError(428, 'revision_required', 'If-Match must contain the current quoted revision.')103 }104 const revision = Number(match[1])105 if (!Number.isSafeInteger(revision)) {106 throw new RequestError(400, 'invalid_revision', 'The supplied revision is invalid.')107 }108 return revision109}110111function openResponse(112 aliasId: string,113 contentIdValue: string,114 renderId: string,115 revision: number,116 metadata: OpenAliasInput['metadata'],117 diagram: Diagram,118 createdAt: number,119 updatedAt: number,120): OpenAliasResponse {121 return {122 aliasId,123 contentId: contentIdValue,124 renderId,125 revision,126 mode: 'open',127 metadata,128 diagram,129 createdAt,130 updatedAt,131 }132}133134function lockedResponse(135 aliasId: string,136 contentIdValue: string,137 renderId: string,138 revision: number,139 input: LockedAliasInput,140 createdAt: number,141 updatedAt: number,142): LockedAliasResponse {143 return {144 aliasId,145 contentId: contentIdValue,146 renderId,147 revision,148 mode: 'locked',149 encryptedContent: input.encryptedContent,150 encryptedMetadata: input.encryptedMetadata,151 keyEnvelope: input.keyEnvelope,152 createdAt,153 updatedAt,154 }155}156157function aliasResponse(158 aliasId: string,159 contentIdValue: string,160 renderId: string,161 revision: number,162 input: AliasInput,163 createdAt: number,164 updatedAt: number,165): AliasResponse {166 return input.mode === 'open'167 ? openResponse(aliasId, contentIdValue, renderId, revision, input.metadata, input.diagram, createdAt, updatedAt)168 : lockedResponse(aliasId, contentIdValue, renderId, revision, input, createdAt, updatedAt)169}170171async function renderIdFor(input: AliasInput): Promise<string> {172 const identity = input.mode === 'open'173 ? { mode: input.mode, diagram: input.diagram, metadata: input.metadata }174 : {175 mode: input.mode,176 encryptedContent: input.encryptedContent,177 encryptedMetadata: input.encryptedMetadata,178 }179 return (await contentId(encoder.encode(canonicalJson(identity)))).id180}181182interface StoredContent {183 id: string184 key: string185 bytes: Uint8Array186 mode: AliasInput['mode']187 encoding: 'json' | 'json+a256gcm'188}189190async function storeContent(env: Env, input: AliasInput): Promise<StoredContent> {191 const content = input.mode === 'open' ? input.diagram : input.encryptedContent192 const bytes = encoder.encode(canonicalJson(content))193 const { id, digest } = await contentId(bytes)194 const encoding = input.mode === 'open' ? 'json' : 'json+a256gcm'195 const extension = input.mode === 'open' ? 'json' : 'enc'196 const key = `contents/${input.mode}/${id}.${extension}`197 await env.CONTENT.put(key, bytes, {198 onlyIf: { etagDoesNotMatch: '*' },199 httpMetadata: { contentType: 'application/json; charset=utf-8' },200 customMetadata: { contentId: id, mode: input.mode, encoding },201 sha256: digest,202 })203 return { id, key, bytes, mode: input.mode, encoding }204}205206function insertContentStatement(env: Env, stored: StoredContent, now: number): D1PreparedStatement {207 return env.DB.prepare(`208 INSERT OR IGNORE INTO contents209 (content_id, mode, object_key, encoding, size_bytes, created_at)210 VALUES (?, ?, ?, ?, ?, ?)211 `).bind(stored.id, stored.mode, stored.key, stored.encoding, stored.bytes.byteLength, now)212}213214function aliasColumns(input: AliasInput): {215 title: string | null216 description: string | null217 encryptedMetadata: string | null218 keyEnvelope: string | null219} {220 return input.mode === 'open'221 ? {222 title: input.metadata.title,223 description: input.metadata.description,224 encryptedMetadata: null,225 keyEnvelope: null,226 }227 : {228 title: null,229 description: null,230 encryptedMetadata: canonicalJson(input.encryptedMetadata),231 keyEnvelope: canonicalJson(input.keyEnvelope),232 }233}234235async function createAlias(request: Request, env: Env): Promise<Response> {236 const writeHash = await writeCapabilityHash(request)237 const input = normalizeAliasInput(await readJsonBody(request))238 const stored = await storeContent(env, input)239 const renderId = await renderIdFor(input)240 const columns = aliasColumns(input)241 const now = Date.now()242243 for (let attempt = 0; attempt < 4; attempt++) {244 const aliasId = randomAliasId()245 const results = await env.DB.batch([246 insertContentStatement(env, stored, now),247 env.DB.prepare(`248 INSERT OR IGNORE INTO aliases249 (alias_id, content_id, render_id, revision, mode, title, description, encrypted_metadata, key_envelope,250 write_key_hash, created_at, updated_at)251 VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?)252 `).bind(253 aliasId,254 stored.id,255 renderId,256 input.mode,257 columns.title,258 columns.description,259 columns.encryptedMetadata,260 columns.keyEnvelope,261 writeHash,262 now,263 now,264 ),265 ])266 if ((results[1]?.meta.changes ?? 0) === 1) {267 const location = `${API_PREFIX}/aliases/${aliasId}`268 return json(aliasResponse(aliasId, stored.id, renderId, 1, input, now, now), {269 status: 201,270 headers: { ETag: '"1"', Location: location },271 })272 }273 }274 throw new Error('Could not allocate an alias ID after four attempts.')275}276277async function findAlias(env: Env, aliasId: string, primary = false): Promise<AliasRecord | null> {278 const database = primary ? env.DB.withSession('first-primary') : env.DB279 return database.prepare(`280 SELECT aliases.*, contents.object_key281 FROM aliases282 JOIN contents ON contents.content_id = aliases.content_id283 WHERE aliases.alias_id = ?284 `).bind(aliasId).first<AliasRecord>()285}286287async function readAliasInput(env: Env, record: AliasRecord): Promise<AliasInput> {288 const object = await env.CONTENT.get(record.object_key)289 if (object === null) throw new Error(`Content object missing for alias ${record.alias_id}.`)290 if (record.mode === 'open') {291 return normalizeOpenAliasInput({292 mode: 'open',293 diagram: await object.json<unknown>(),294 metadata: { title: record.title ?? '', description: record.description ?? '' },295 })296 }297 if (!record.encrypted_metadata || !record.key_envelope) {298 throw new Error(`Locked alias metadata missing for alias ${record.alias_id}.`)299 }300 return normalizeAliasInput({301 mode: 'locked',302 encryptedContent: await object.json<unknown>(),303 encryptedMetadata: JSON.parse(record.encrypted_metadata) as unknown,304 keyEnvelope: JSON.parse(record.key_envelope) as unknown,305 })306}307308async function getAlias(request: Request, env: Env, aliasId: string): Promise<Response> {309 const record = await findAlias(env, aliasId)310 if (record === null) throw new RequestError(404, 'alias_not_found', 'Diagram alias not found.')311 const etag = `"${record.revision}"`312 if (request.headers.get('If-None-Match') === etag) {313 return new Response(null, { status: 304, headers: responseHeaders({ ETag: etag }) })314 }315 const input = await readAliasInput(env, record)316 return json(aliasResponse(317 record.alias_id,318 record.content_id,319 record.render_id,320 record.revision,321 input,322 record.created_at,323 record.updated_at,324 ), { headers: { ETag: etag } })325}326327async function updateAlias(request: Request, env: Env, aliasId: string): Promise<Response> {328 const writeHash = await writeCapabilityHash(request)329 const expectedRevision = parseRevision(request)330 const record = await findAlias(env, aliasId, true)331 if (record === null) throw new RequestError(404, 'alias_not_found', 'Diagram alias not found.')332 if (!capabilityMatches(writeHash, record.write_key_hash)) {333 throw new RequestError(403, 'write_capability_rejected', 'The write capability is not valid for this alias.')334 }335 if (record.revision !== expectedRevision) {336 throw new RequestError(412, 'revision_conflict', 'The alias changed since it was loaded.')337 }338 const input = normalizeAliasInput(await readJsonBody(request))339 const stored = await storeContent(env, input)340 const renderId = await renderIdFor(input)341 const columns = aliasColumns(input)342 const now = Date.now()343 const results = await env.DB.batch([344 insertContentStatement(env, stored, now),345 env.DB.prepare(`346 UPDATE aliases347 SET content_id = ?, render_id = ?, mode = ?, title = ?, description = ?, encrypted_metadata = ?, key_envelope = ?,348 revision = revision + 1, updated_at = ?349 WHERE alias_id = ? AND revision = ? AND write_key_hash = ?350 `).bind(351 stored.id,352 renderId,353 input.mode,354 columns.title,355 columns.description,356 columns.encryptedMetadata,357 columns.keyEnvelope,358 now,359 aliasId,360 expectedRevision,361 writeHash,362 ),363 ])364 if ((results[1]?.meta.changes ?? 0) !== 1) {365 throw new RequestError(412, 'revision_conflict', 'The alias changed since it was loaded.')366 }367368 const revision = expectedRevision + 1369 return json(aliasResponse(370 aliasId,371 stored.id,372 renderId,373 revision,374 input,375 record.created_at,376 now,377 ), { headers: { ETag: `"${revision}"` } })378}379380function rendererIdentity(request: Request): RendererIdentity {381 const unit = request.headers.get('X-Renderer-Unit')?.trim() ?? ''382 const build = request.headers.get('X-Renderer-Build')?.trim() ?? ''383 const pipeline = (request.headers.get('X-Renderer-Pipeline') ?? '')384 .split(',')385 .map(value => value.trim())386 .filter(Boolean)387 if (!rendererUnitPattern.test(unit)388 || !rendererBuildPattern.test(build)389 || pipeline.length < 1390 || pipeline.length > 8391 || pipeline[0] !== unit392 || pipeline.some(value => !rendererUnitPattern.test(value))) {393 throw new RequestError(400, 'invalid_renderer_identity', 'Renderer unit, build, and pipeline headers are required.')394 }395 return { unit, build, pipeline }396}397398function renderObjectKey(record: AliasRecord, format: 'svg' | 'png', renderer: RendererIdentity): string {399 const extension = record.mode === 'locked' ? `${format}.enc` : format400 return `renders/${record.mode}/${renderer.unit}/${renderer.build}/${record.render_id}.${extension}`401}402403function renderHeadKey(record: AliasRecord, format: 'svg' | 'png'): string {404 return `render-heads/${record.mode}/${record.render_id}.${format}.json`405}406407function normalizeRenderHead(value: unknown, record: AliasRecord, format: 'svg' | 'png'): RenderHead {408 if (!value || typeof value !== 'object') throw new Error(`Invalid render head for ${record.render_id}.`)409 const candidate = value as Record<string, unknown>410 const identity = {411 unit: candidate.unit,412 build: candidate.build,413 pipeline: candidate.pipeline,414 }415 if (typeof identity.unit !== 'string'416 || typeof identity.build !== 'string'417 || !Array.isArray(identity.pipeline)418 || identity.pipeline.some(value => typeof value !== 'string')419 || !rendererUnitPattern.test(identity.unit)420 || !rendererBuildPattern.test(identity.build)421 || identity.pipeline.length < 1422 || identity.pipeline.length > 8423 || identity.pipeline[0] !== identity.unit424 || identity.pipeline.some(value => typeof value !== 'string' || !rendererUnitPattern.test(value))) {425 throw new Error(`Invalid render head for ${record.render_id}.`)426 }427 const expectedKey = renderObjectKey(record, format, identity as RendererIdentity)428 if (candidate.objectKey !== expectedKey) throw new Error(`Invalid render head for ${record.render_id}.`)429 return { ...(identity as RendererIdentity), objectKey: expectedKey }430}431432async function readRenderBody(request: Request): Promise<Uint8Array> {433 const declaredLength = request.headers.get('Content-Length')434 if (declaredLength !== null) {435 if (!/^[0-9]+$/.test(declaredLength)) {436 throw new RequestError(400, 'invalid_render', 'Content-Length is invalid.')437 }438 if (Number(declaredLength) > MAX_RENDER_BYTES) {439 throw new RequestError(413, 'render_too_large', 'Rendered image is too large.')440 }441 }442 const contentEncoding = request.headers.get('Content-Encoding')?.trim().toLowerCase()443 if (contentEncoding && contentEncoding !== 'identity') {444 throw new RequestError(415, 'unsupported_content_encoding', 'Compressed render uploads are not supported.')445 }446 const reader = request.body?.getReader()447 const chunks: Uint8Array[] = []448 let length = 0449 if (reader) {450 while (true) {451 const { done, value } = await reader.read()452 if (done) break453 length += value.byteLength454 if (length > MAX_RENDER_BYTES) {455 await reader.cancel('Rendered image is too large.')456 throw new RequestError(413, 'render_too_large', 'Rendered image is too large.')457 }458 chunks.push(value)459 }460 }461 const bytes = new Uint8Array(length)462 let offset = 0463 for (const chunk of chunks) {464 bytes.set(chunk, offset)465 offset += chunk.byteLength466 }467 return bytes468}469470async function authorizeRenderWrite(request: Request, env: Env, aliasId: string): Promise<AliasRecord> {471 const [writeHash, expectedRevision] = await Promise.all([472 writeCapabilityHash(request),473 Promise.resolve(parseRevision(request)),474 ])475 const record = await findAlias(env, aliasId, true)476 if (record === null) throw new RequestError(404, 'alias_not_found', 'Diagram alias not found.')477 if (!capabilityMatches(writeHash, record.write_key_hash)) {478 throw new RequestError(403, 'write_capability_rejected', 'The write capability is not valid for this alias.')479 }480 if (record.revision !== expectedRevision || request.headers.get('X-Render-Id') !== record.render_id) {481 throw new RequestError(412, 'revision_conflict', 'The alias changed before its render was stored.')482 }483 return record484}485486async function putRender(487 request: Request,488 env: Env,489 aliasId: string,490 format: 'svg' | 'png',491): Promise<Response> {492 const record = await authorizeRenderWrite(request, env, aliasId)493 const renderer = rendererIdentity(request)494 const mediaType = format === 'svg' ? 'image/svg+xml' : 'image/png'495 const bytes = await readRenderBody(request)496 if (bytes.byteLength === 0) throw new RequestError(400, 'invalid_render', 'Rendered image is empty.')497498 let storedBytes = bytes499 let contentType = mediaType500 if (record.mode === 'locked') {501 if (request.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {502 throw new RequestError(415, 'unsupported_media_type', 'Encrypted renders must use application/json.')503 }504 let body: unknown505 try {506 body = JSON.parse(new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(bytes)) as unknown507 } catch {508 throw new RequestError(400, 'invalid_render', 'Encrypted render is not valid JSON.')509 }510 storedBytes = encoder.encode(canonicalJson(normalizeEncryptedBlob(body, 'encryptedRender')))511 contentType = 'application/json; charset=utf-8'512 } else if (request.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase() !== mediaType) {513 throw new RequestError(415, 'unsupported_media_type', `Render must use ${mediaType}.`)514 } else if (format === 'svg') {515 storedBytes = canonicalSvg(bytes, 'raw')516 }517518 const objectKey = renderObjectKey(record, format, renderer)519 await env.CONTENT.put(objectKey, storedBytes, {520 onlyIf: { etagDoesNotMatch: '*' },521 httpMetadata: { contentType },522 customMetadata: {523 contentId: record.content_id,524 renderId: record.render_id,525 mode: record.mode,526 format,527 rendererUnit: renderer.unit,528 rendererBuild: renderer.build,529 rendererPipeline: renderer.pipeline.join(','),530 },531 })532 const head: RenderHead = { ...renderer, objectKey }533 await env.CONTENT.put(renderHeadKey(record, format), canonicalJson(head), {534 httpMetadata: { contentType: 'application/json; charset=utf-8' },535 customMetadata: { renderId: record.render_id, mode: record.mode, format },536 })537 return new Response(null, { status: 204, headers: responseHeaders() })538}539540async function getRender(541 request: Request,542 env: Env,543 aliasId: string,544 format: 'svg' | 'png',545): Promise<Response> {546 const record = await findAlias(env, aliasId)547 if (record === null) throw new RequestError(404, 'alias_not_found', 'Diagram alias not found.')548 const url = new URL(request.url)549 const encryptedRequest = url.searchParams.get('encrypted') === '1'550 if (record.mode === 'locked' && url.searchParams.has('appearance')) {551 throw new RequestError(422, 'encrypted_appearance_unavailable', 'Encrypted renders cannot be materialized by the server.')552 }553 const appearance = requestedAppearance(request, format)554 if (record.mode === 'locked' && !encryptedRequest) {555 throw new RequestError(403, 'encrypted_diagram_not_embeddable', 'Password-locked diagrams cannot be embedded.')556 }557 if (record.mode === 'open' && encryptedRequest) {558 throw new RequestError(400, 'render_not_encrypted', 'This diagram render is not encrypted.')559 }560 const headObject = await env.CONTENT.get(renderHeadKey(record, format))561 if (headObject === null) throw new RequestError(404, 'render_not_found', 'Saved render not found.')562 const head = normalizeRenderHead(await headObject.json<unknown>(), record, format)563 const object = await env.CONTENT.get(head.objectKey)564 if (object === null) throw new RequestError(404, 'render_not_found', 'Saved render not found.')565 let body: ReadableStream | Uint8Array | null = object.body566 let etag = object.httpEtag567 if (record.mode === 'open' && format === 'svg') {568 const bytes = canonicalSvg(new Uint8Array(await object.arrayBuffer()), appearance)569 const identity = await contentId(bytes)570 etag = `"${identity.id}"`571 body = bytes572 }573 if (request.headers.get('If-None-Match') === etag) {574 return new Response(null, { status: 304, headers: { ETag: etag } })575 }576 const headers = new Headers()577 object.writeHttpMetadata(headers)578 headers.set('ETag', etag)579 headers.set('X-Content-Type-Options', 'nosniff')580 headers.set('Content-Security-Policy', format === 'svg'581 ? "default-src 'none'; style-src 'unsafe-inline'; sandbox"582 : "default-src 'none'; sandbox")583 headers.set('X-Renderer-Unit', head.unit)584 headers.set('X-Renderer-Build', head.build)585 headers.set('X-Renderer-Pipeline', head.pipeline.join(','))586 if (format === 'svg' && record.mode === 'open') {587 headers.set('X-Diagram-Appearance', appearance)588 headers.set('X-SVG-Schema', SVG_SCHEMA)589 if (appearance !== 'raw') {590 headers.set('X-SVG-Materializer', MATERIALIZER_BUILD)591 headers.set('X-SVG-Palette', PALETTE_BUILD)592 }593 }594 headers.set('Cache-Control', record.mode === 'open'595 ? 'public, max-age=60, stale-while-revalidate=300'596 : 'no-store')597 return new Response(request.method === 'HEAD' ? null : body, { headers })598}599600async function route(request: Request, env: Env): Promise<Response> {601 const url = new URL(request.url)602 if (url.pathname === `${API_PREFIX}/health` && request.method === 'GET') {603 return json({ status: 'ok' })604 }605 if (url.pathname === `${API_PREFIX}/aliases` || url.pathname === `${API_PREFIX}/aliases/`) {606 if (request.method === 'POST') return createAlias(request, env)607 return json({ error: { code: 'method_not_allowed', message: 'Method not allowed.' } }, {608 status: 405,609 headers: { Allow: 'POST' },610 })611 }612613 const aliasMatch = url.pathname.match(aliasPathPattern)614 if (aliasMatch?.[1] && isAliasId(aliasMatch[1])) {615 if (request.method === 'GET' || request.method === 'HEAD') {616 const response = await getAlias(request, env, aliasMatch[1])617 return request.method === 'HEAD' ? new Response(null, response) : response618 }619 if (request.method === 'PUT') return updateAlias(request, env, aliasMatch[1])620 return json({ error: { code: 'method_not_allowed', message: 'Method not allowed.' } }, {621 status: 405,622 headers: { Allow: 'GET, HEAD, PUT' },623 })624 }625 const renderMatch = url.pathname.match(renderPathPattern)626 if (renderMatch?.[1] && isAliasId(renderMatch[1])) {627 const format = renderMatch[2] as 'svg' | 'png'628 if (request.method === 'GET' || request.method === 'HEAD') {629 return getRender(request, env, renderMatch[1], format)630 }631 if (request.method === 'PUT') return putRender(request, env, renderMatch[1], format)632 return json({ error: { code: 'method_not_allowed', message: 'Method not allowed.' } }, {633 status: 405,634 headers: { Allow: 'GET, HEAD, PUT' },635 })636 }637 throw new RequestError(404, 'not_found', 'Route not found.')638}639640export default {641 async fetch(request: Request, env: Env): Promise<Response> {642 try {643 return await route(request, env)644 } catch (error) {645 if (error instanceof RequestError) return errorResponse(error)646 console.error(JSON.stringify({647 message: 'Unhandled diagramzip-api error',648 method: request.method,649 path: new URL(request.url).pathname,650 error: error instanceof Error ? error.message : 'Unknown error',651 }))652 return errorResponse(new RequestError(500, 'internal_error', 'An internal error occurred.'))653 }654 },655} satisfies ExportedHandler<Env>