1import { attachEditableDocument, extractEditableDocument } from '../../../shared/svg/index.js'2import { BLOCK_TRANSFORM_PATH, TINY_TRANSFORM_PATH, blockTransform, type BlockTransformEnv } from './block-transform'34const ALIAS_PATH = /^\/d\/[A-Za-z0-9_-]{16}$/5const PACKED_SVG_PATH = /^\/svg\/([A-Za-z0-9_-]+)$/6const MAX_PACKED_SVG_LENGTH = 5_242_88078function textResponse(message: string, status: number, method: string): Response {9 return new Response(method === 'HEAD' ? null : message, {10 status,11 headers: {12 'content-type': 'text/plain; charset=utf-8',13 'cache-control': 'no-store',14 'x-content-type-options': 'nosniff',15 },16 })17}1819async function applicationShell(request: Request, assets: Fetcher): Promise<Response> {20 const assetUrl = new URL('/index.html', request.url)21 const assetRequest = new Request(assetUrl, {22 method: request.method,23 headers: request.headers,24 })25 const response = await assets.fetch(assetRequest)26 const headers = new Headers(response.headers)27 headers.set('cache-control', 'no-cache, no-transform')28 return new Response(request.method === 'HEAD' ? null : response.body, {29 status: response.status,30 statusText: response.statusText,31 headers,32 })33}3435function packedBytes(value: string): Uint8Array {36 const normalized = value.replaceAll('-', '+').replaceAll('_', '/')37 const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4)38 const binary = atob(padded)39 return Uint8Array.from(binary, character => character.charCodeAt(0))40}4142async function inflatePackedSvg(value: string): Promise<string> {43 const stream = new Blob([packedBytes(value)]).stream().pipeThrough(new DecompressionStream('deflate'))44 const reader = stream.getReader()45 const chunks: Uint8Array[] = []46 let length = 047 while (true) {48 const { done, value: chunk } = await reader.read()49 if (done) break50 length += chunk.byteLength51 if (length > MAX_PACKED_SVG_LENGTH) {52 await reader.cancel()53 throw new Error('Packed SVG is too large.')54 }55 chunks.push(chunk)56 }57 const bytes = new Uint8Array(length)58 let offset = 059 for (const chunk of chunks) {60 bytes.set(chunk, offset)61 offset += chunk.byteLength62 }63 return new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(bytes)64}6566async function packedSvgResponse(request: Request, payload: string): Promise<Response> {67 try {68 const source = await inflatePackedSvg(payload)69 const safeSource = attachEditableDocument(source, extractEditableDocument(source))70 return new Response(request.method === 'HEAD' ? null : safeSource, {71 headers: {72 'cache-control': 'public, max-age=31536000, immutable',73 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; sandbox",74 'content-type': 'image/svg+xml; charset=utf-8',75 'x-content-type-options': 'nosniff',76 'x-diagram-document': 'editable-svg-1',77 },78 })79 } catch {80 return textResponse('Invalid packed SVG.', 400, request.method)81 }82}8384export default {85 async fetch(request, env): Promise<Response> {86 const url = new URL(request.url)8788 if (url.pathname === BLOCK_TRANSFORM_PATH) return blockTransform(request, env)89 if (url.pathname === TINY_TRANSFORM_PATH) return blockTransform(request, env, true)9091 if (request.method !== 'GET' && request.method !== 'HEAD') {92 return textResponse('Not found', 404, request.method)93 }9495 if (url.pathname === '/' || ALIAS_PATH.test(url.pathname)) {96 return applicationShell(request, env.ASSETS)97 }9899 const packedSvg = url.pathname.match(PACKED_SVG_PATH)100 if (packedSvg) return packedSvgResponse(request, packedSvg[1])101102 if (url.pathname === '/healthz') {103 return textResponse('ok', 200, request.method)104 }105106 return textResponse('Not found', 404, request.method)107 },108} satisfies ExportedHandler<Env & BlockTransformEnv>