1import { parseStructurizr, lowerStructurizr } from '../languages/structurizr'2import { RenderError, RequestError } from '../runtime/errors'3import { parseUnitRenderRequest } from '../runtime/validation'4import { NORMALIZER_BUILD } from '../../../../shared/svg/index.js'56const UNIT_ID = 'structurizr'7const VERSION = 'structurizr@6.2.2/plantuml@1.2026.6-translation-1'8const EXPOSED = 'Cache-Control, Content-Type, X-Diagram-Cache, X-Diagram-Engine, X-Diagram-Engine-Version, X-Diagram-Pipeline, X-Diagram-Renderer, X-Diagram-Unit, X-Renderer-Build'910interface StructurizrEnv { RENDERER_BUILD: string; PLANTUML: Fetcher }1112function json(value: unknown, init: ResponseInit = {}): Response {13 const headers = new Headers(init.headers)14 headers.set('Content-Type', 'application/json; charset=utf-8')15 headers.set('Cache-Control', 'no-store')16 headers.set('Access-Control-Allow-Origin', '*')17 return Response.json(value, { ...init, headers })18}1920function errorResponse(error: unknown): Response {21 if (error instanceof RequestError || error instanceof RenderError) return json({ error: { code: error.code, message: error.message } }, { status: error.status })22 return json({ error: { code: 'internal_error', message: 'The Structurizr diagram could not be rendered.' } }, { status: 500 })23}2425export async function cacheKey(input: unknown, build: string): Promise<Request> {26 const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(input)))27 let binary = ''; for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte)28 const hash = btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')29 return new Request(`https://diagramzip-structurizr-cache.invalid/${encodeURIComponent(NORMALIZER_BUILD)}/${encodeURIComponent(build)}/${hash}`)30}3132export default {33 async fetch(request: Request, env: StructurizrEnv, ctx: ExecutionContext): Promise<Response> {34 const url = new URL(request.url)35 if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Headers': 'Accept, Content-Type', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Origin': '*', 'Access-Control-Max-Age': '86400' } })36 try {37 if (request.method === 'GET' && url.pathname === '/v1/health') return json({ ok: true, unit: UNIT_ID, id: UNIT_ID })38 if (request.method === 'GET' && url.pathname === '/v1/capabilities') return json({39 unit: UNIT_ID, id: UNIT_ID, kind: 'translate', format: 'svg', runtime: 'edge-js', version: VERSION,40 build: env.RENDERER_BUILD, pipeline: [UNIT_ID, 'plantuml-family', 'plantuml'],41 knownLosses: ['Only SVG is supported.', 'Structurizr scripts, docs, plugins, filesystem access, and arbitrary remote themes are rejected.', 'Layout and typography may differ from the legacy Structurizr exporter.'],42 }, { headers: { 'Cache-Control': 'public, max-age=300' } })43 if (request.method !== 'POST' || url.pathname !== '/v1/svg') return json({ error: { code: 'not_found', message: 'Route not found.' } }, { status: 404 })44 const input = await parseUnitRenderRequest(request, 'structurizr')45 const key = await cacheKey(input, env.RENDERER_BUILD)46 let cached: Response | undefined47 try { cached = await caches.default.match(key) } catch (error) { console.warn(JSON.stringify({ message: 'Structurizr cache read failed', error: String(error) })) }48 if (cached) { const headers = new Headers(cached.headers); headers.set('X-Diagram-Cache', 'HIT'); return new Response(cached.body, { status: cached.status, headers }) }49 const lowered = lowerStructurizr(parseStructurizr(input.source), input.options)50 const downstream = await env.PLANTUML.fetch(new Request('https://plantuml.render.diagram.zip/v1/svg', {51 method: 'POST', headers: { Accept: 'image/svg+xml', 'Content-Type': 'application/json' },52 body: JSON.stringify({ source: lowered, format: 'svg', options: {}, metadata: input.metadata, presentation: input.presentation }),53 signal: request.signal,54 }))55 if (!downstream.ok) return new Response(downstream.body, { status: downstream.status, headers: downstream.headers })56 const headers = new Headers(downstream.headers)57 headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Expose-Headers', EXPOSED)58 headers.set('X-Diagram-Engine', 'structurizr'); headers.set('X-Diagram-Engine-Version', VERSION); headers.set('X-Diagram-Unit', UNIT_ID); headers.set('X-Diagram-Pipeline', `${UNIT_ID},plantuml-family,plantuml`); headers.set('X-Diagram-Renderer', 'edge-js'); headers.set('X-Renderer-Build', env.RENDERER_BUILD); headers.set('X-Diagram-Cache', 'MISS')59 const response = new Response(downstream.body, { status: downstream.status, headers })60 try {61 if (typeof caches !== 'undefined') ctx.waitUntil(caches.default.put(key, response.clone()).catch(error => console.warn(JSON.stringify({ message: 'Structurizr cache write failed', error: String(error) }))))62 } catch (error) { console.warn(JSON.stringify({ message: 'Structurizr cache write failed', error: String(error) })) }63 return response64 } catch (error) {65 if (!(error instanceof RequestError || error instanceof RenderError)) console.error(JSON.stringify({ message: 'unhandled Structurizr error', error: error instanceof Error ? error.message : String(error) }))66 return errorResponse(error)67 }68 },69} satisfies ExportedHandler<StructurizrEnv>