@hyperframes/gcp-cloud-run 0.7.70 → 0.7.72

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/server.ts", "../src/chromium.ts", "../src/formatExtension.ts", "../src/gcsTransport.ts"],
4
- "sourcesContent": ["/**\n * Cloud Run request handler for HyperFrames distributed rendering.\n *\n * One container image, three roles. Cloud Workflows POSTs a JSON body with\n * an `Action` field; the handler unwraps any `Payload`/`Input` envelope,\n * primes the runtime (Chrome path), and forwards to the matching OSS\n * primitive from `@hyperframes/producer/distributed`.\n *\n * Everything heavy \u2014 capture, encode, audio mix \u2014 happens inside the OSS\n * primitives. The handler is thin glue: parse body \u2192 GCS download \u2192 call\n * primitive \u2192 GCS upload \u2192 return small JSON result.\n *\n * `dispatch()` is the testable core (inject `storage` + `primitives`); the\n * Hono app at the bottom is the HTTP shell the Dockerfile runs. The shape\n * deliberately tracks `@hyperframes/aws-lambda`'s `handler.ts` so the two\n * adapters stay easy to diff.\n */\n\nimport { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, extname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { serve } from \"@hono/node-server\";\nimport { Storage } from \"@google-cloud/storage\";\nimport { Hono } from \"hono\";\nimport {\n assemble,\n type AssembleResult,\n type ChunkResult,\n type DistributedRenderConfig,\n plan,\n type PlanResult,\n renderChunk,\n} from \"@hyperframes/producer/distributed\";\nimport { resolveChromeExecutablePath } from \"./chromium.js\";\nimport type {\n AssembleEvent,\n AssembleResultBody,\n CloudRunAction,\n CloudRunEvent,\n CloudRunResult,\n PlanEvent,\n PlanResultBody,\n RenderChunkEvent,\n RenderChunkResultBody,\n} from \"./events.js\";\nimport { type DistributedFormat, formatExtension } from \"./formatExtension.js\";\nimport {\n downloadGcsObjectToFile,\n parseGcsUri,\n tarDirectory,\n untarDirectory,\n uploadFileToGcs,\n} from \"./gcsTransport.js\";\n\n/**\n * Lazily-constructed Storage client. Cached at module scope so warm\n * container instances reuse the underlying HTTP keep-alive pool across\n * requests.\n */\nlet cachedStorage: Storage | null = null;\nfunction getStorage(): Storage {\n if (cachedStorage) return cachedStorage;\n cachedStorage = new Storage();\n return cachedStorage;\n}\n\n/**\n * Optional injection points used by the handler's unit tests. Production\n * callers leave these unset; the real OSS primitives are used. Tests inject\n * `storage` and `primitives` directly rather than mutating module state.\n */\nexport interface HandlerDeps {\n storage?: Storage;\n primitives?: {\n plan: typeof plan;\n renderChunk: typeof renderChunk;\n assemble: typeof assemble;\n };\n /** Override the per-request workdir root (defaults to the OS tmpdir). */\n tmpRoot?: string;\n /** Skip Chrome resolution (used by dispatch tests that mock renderChunk). */\n skipChromeResolution?: boolean;\n}\n\n/**\n * Dispatch a single render request. Cloud Workflows (or a direct caller)\n * sometimes wraps the body in `{ Payload: ... }` or `{ Input: ... }`; unwrap\n * until we hit a discriminated event.\n */\n// fallow-ignore-next-line complexity\nexport async function dispatch(event: CloudRunEvent, deps?: HandlerDeps): Promise<CloudRunResult> {\n const unwrapped = unwrapEvent(event);\n validateEventGcsUris(unwrapped);\n logEvent({ event: \"handler_start\", action: unwrapped.Action, input: summarizeEvent(unwrapped) });\n try {\n switch (unwrapped.Action) {\n case \"plan\":\n return await handlePlan(unwrapped, deps);\n case \"renderChunk\":\n return await handleRenderChunk(unwrapped, deps);\n case \"assemble\":\n return await handleAssemble(unwrapped, deps);\n default: {\n // Compile-time exhaustiveness: a new CloudRunAction member trips\n // the `never` assignment before the runtime error is reachable.\n const _exhaustive: never = unwrapped;\n throw new Error(\n `[handler] unknown Action: ${JSON.stringify(\n (_exhaustive as { Action?: string }).Action,\n )}. Expected one of \"plan\", \"renderChunk\", \"assemble\".`,\n );\n }\n }\n } catch (err) {\n logEvent({\n event: \"handler_error\",\n action: unwrapped.Action,\n message: err instanceof Error ? err.message : String(err),\n name: err instanceof Error ? err.name : undefined,\n });\n throw err;\n }\n}\n\n// At most `{Payload: {Input: ...}}` is expected; 4 levels is 2\u00D7 headroom\n// and prevents infinite loops on malformed input.\nconst MAX_ENVELOPE_DEPTH = 4;\n\n// fallow-ignore-next-line complexity\nexport function unwrapEvent(event: CloudRunEvent): PlanEvent | RenderChunkEvent | AssembleEvent {\n let cursor: CloudRunEvent = event;\n for (let i = 0; i < MAX_ENVELOPE_DEPTH; i++) {\n if (cursor && typeof cursor === \"object\") {\n const obj = cursor as Record<string, unknown>;\n if (typeof obj.Action === \"string\" && isCloudRunAction(obj.Action)) {\n return cursor as PlanEvent | RenderChunkEvent | AssembleEvent;\n }\n if (\"Payload\" in obj) {\n cursor = obj.Payload as CloudRunEvent;\n continue;\n }\n if (\"Input\" in obj) {\n cursor = obj.Input as CloudRunEvent;\n continue;\n }\n }\n break;\n }\n throw new Error(\n `[handler] body has no recognised Action; unwrapped ${MAX_ENVELOPE_DEPTH} levels of Payload/Input without finding one.`,\n );\n}\n\nfunction isCloudRunAction(value: string): value is CloudRunAction {\n return value === \"plan\" || value === \"renderChunk\" || value === \"assemble\";\n}\n\n/**\n * Emit a single JSON line to stdout. Cloud Logging ingests each stdout line\n * as a structured `jsonPayload` entry, so Logs Explorer can filter on\n * `jsonPayload.event=\"handler_start\"` and project specific fields when\n * triaging without attaching a debugger.\n */\nfunction logEvent(payload: Record<string, unknown>): void {\n console.log(JSON.stringify(payload));\n}\n\n/**\n * Compact, non-PII summary of an event for logging. The full body can\n * include the entire project config; we only emit the routable fields\n * needed to triage a failure from Cloud Logging.\n */\nfunction summarizeEvent(\n event: PlanEvent | RenderChunkEvent | AssembleEvent,\n): Record<string, unknown> {\n switch (event.Action) {\n case \"plan\":\n return {\n projectGcsUri: event.ProjectGcsUri,\n planOutputGcsPrefix: event.PlanOutputGcsPrefix,\n format: event.Config.format,\n fps: event.Config.fps,\n };\n case \"renderChunk\":\n return {\n planGcsUri: event.PlanGcsUri,\n chunkIndex: event.ChunkIndex,\n format: event.Format,\n };\n case \"assemble\":\n return {\n planGcsUri: event.PlanGcsUri,\n chunkCount: event.ChunkGcsUris.length,\n hasAudio: event.AudioGcsUri !== null,\n outputGcsUri: event.OutputGcsUri,\n format: event.Format,\n };\n }\n}\n\n/**\n * Point the engine at the in-image Chrome binary. The OSS engine resolves\n * Chrome via `PRODUCER_HEADLESS_SHELL_PATH` first; set it once per instance\n * before invoking any browser-touching primitive. ffmpeg is on the image's\n * PATH (apt-installed by the Dockerfile), so nothing to prime there.\n */\nfunction primeChrome(deps?: HandlerDeps): void {\n if (deps?.skipChromeResolution) return;\n if (process.env.PRODUCER_HEADLESS_SHELL_PATH) return;\n process.env.PRODUCER_HEADLESS_SHELL_PATH = resolveChromeExecutablePath();\n}\n\n// \u2500\u2500 Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// fallow-ignore-next-line complexity\nasync function handlePlan(event: PlanEvent, deps?: HandlerDeps): Promise<PlanResultBody> {\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.plan ?? plan;\n\n // The producer's probe stage launches Chromium whenever the composition\n // needs a runtime duration probe or has unresolved sub-compositions, so\n // plan has to resolve Chrome the same way renderChunk does.\n primeChrome(deps);\n\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-plan-\"));\n const projectArchive = join(work, \"project.tar.gz\");\n const projectDir = join(work, \"project\");\n const planDir = join(work, \"plan\");\n\n try {\n await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);\n await untarDirectory(projectArchive, projectDir);\n\n const config: DistributedRenderConfig = {\n ...event.Config,\n };\n const result: PlanResult = await primitive(projectDir, config, planDir);\n\n // Upload the planDir as a single tarball. The workflow cannot pass a\n // directory-shaped artifact between steps; we serialize and rely on the\n // consumer (renderChunk / assemble) to untar. `audio.aac` lives inside\n // planDir, so it already rides along in this tarball \u2014 every consumer\n // (including assemble) gets it from the untar. We deliberately do NOT\n // upload a separate audio object: it would duplicate the bytes on every\n // plan upload and be re-downloaded + overwritten by assemble. `AudioGcsUri`\n // stays in the result shape for wire compatibility but is null.\n const planTar = join(work, \"plan.tar.gz\");\n await tarDirectory(planDir, planTar);\n const planTarUri = `${trimTrailingSlash(event.PlanOutputGcsPrefix)}/plan.tar.gz`;\n const audioPath = join(planDir, \"audio.aac\");\n const hasAudio = existsSync(audioPath) && statSync(audioPath).size > 0;\n await uploadFileToGcs(storage, planTar, planTarUri, \"application/gzip\");\n\n return {\n Action: \"plan\",\n PlanGcsUri: planTarUri,\n PlanHash: result.planHash,\n ChunkCount: result.chunkCount,\n TotalFrames: result.totalFrames,\n Fps: result.fps,\n Width: result.width,\n Height: result.height,\n Format: result.format,\n HasAudio: hasAudio,\n AudioGcsUri: null,\n FfmpegVersion: result.ffmpegVersion,\n ProducerVersion: result.producerVersion,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\n// \u2500\u2500 RenderChunk \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// fallow-ignore-next-line complexity\nasync function handleRenderChunk(\n event: RenderChunkEvent,\n deps?: HandlerDeps,\n): Promise<RenderChunkResultBody> {\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.renderChunk ?? renderChunk;\n\n primeChrome(deps);\n\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-chunk-\"));\n const planTar = join(work, \"plan.tar.gz\");\n const planDir = join(work, \"plan\");\n\n try {\n await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);\n await untarDirectory(planTar, planDir);\n\n // Verify the plan's hash matches what the workflow told us to render.\n // The producer's renderChunk re-checks internally (defense-in-depth),\n // but doing it here at the handler boundary lets us fail before paying\n // the Chrome-launch + render cost on a misrouted chunk. Throws a typed\n // PLAN_HASH_MISMATCH the workflow can route as non-retryable.\n verifyPlanHash(planDir, event.PlanHash);\n\n const chunkOutputBase = join(\n work,\n event.Format === \"png-sequence\"\n ? `chunk-${pad(event.ChunkIndex)}`\n : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`,\n );\n\n const result: ChunkResult = await primitive(planDir, event.ChunkIndex, chunkOutputBase);\n\n const chunkUri = await uploadChunkOutput(\n storage,\n result,\n event.ChunkOutputGcsPrefix,\n event.ChunkIndex,\n );\n\n return {\n Action: \"renderChunk\",\n ChunkGcsUri: chunkUri,\n ChunkIndex: event.ChunkIndex,\n Sha256: result.sha256,\n FramesEncoded: result.framesEncoded,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\nasync function uploadChunkOutput(\n storage: Storage,\n result: ChunkResult,\n prefix: string,\n chunkIndex: number,\n): Promise<string> {\n const trimmed = trimTrailingSlash(prefix);\n if (result.outputKind === \"file\") {\n const ext = extname(result.outputPath);\n const uri = `${trimmed}/chunks/${pad(chunkIndex)}${ext}`;\n await uploadFileToGcs(storage, result.outputPath, uri);\n return uri;\n }\n // frame-dir: upload as a tarball so a single GCS object represents the\n // chunk. Assemble's png-sequence path expects a directory per chunk; it\n // untars on its end.\n const tarball = `${result.outputPath}.tar.gz`;\n await tarDirectory(result.outputPath, tarball);\n const uri = `${trimmed}/chunks/${pad(chunkIndex)}.tar.gz`;\n await uploadFileToGcs(storage, tarball, uri, \"application/gzip\");\n return uri;\n}\n\n// \u2500\u2500 Assemble \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// fallow-ignore-next-line complexity\nasync function handleAssemble(\n event: AssembleEvent,\n deps?: HandlerDeps,\n): Promise<AssembleResultBody> {\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.assemble ?? assemble;\n\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-assemble-\"));\n const planTar = join(work, \"plan.tar.gz\");\n const planDir = join(work, \"plan\");\n\n try {\n await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);\n await untarDirectory(planTar, planDir);\n\n const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);\n\n // Audio rides inside the plan tarball, so it's already on disk after the\n // untar above \u2014 no separate download. Fall back to a supplied AudioGcsUri\n // only for backward compatibility with an older Plan that uploaded it\n // standalone.\n let audioPath: string | null = null;\n const planAudio = join(planDir, \"audio.aac\");\n if (existsSync(planAudio) && statSync(planAudio).size > 0) {\n audioPath = planAudio;\n } else if (event.AudioGcsUri) {\n audioPath = planAudio;\n await downloadGcsObjectToFile(storage, event.AudioGcsUri, audioPath);\n }\n\n const finalOutput =\n event.Format === \"png-sequence\"\n ? join(work, \"output-frames\")\n : join(work, `output${formatExtension(event.Format)}`);\n\n const result: AssembleResult = await primitive(planDir, chunkPaths, audioPath, finalOutput, {\n cfr: event.Cfr === true,\n });\n\n if (event.Format === \"png-sequence\") {\n const tarball = `${finalOutput}.tar.gz`;\n await tarDirectory(finalOutput, tarball);\n await uploadFileToGcs(storage, tarball, event.OutputGcsUri, \"application/gzip\");\n } else {\n await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);\n }\n\n return {\n Action: \"assemble\",\n OutputGcsUri: event.OutputGcsUri,\n FramesEncoded: result.framesEncoded,\n FileSize: result.fileSize,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\nasync function downloadChunkObjects(\n storage: Storage,\n uris: string[],\n workDir: string,\n format: DistributedFormat,\n): Promise<string[]> {\n const chunksDir = join(workDir, \"chunks\");\n mkdirSync(chunksDir, { recursive: true });\n // Each chunk is an independent GCS GET (+ untar for png-sequence). Run\n // them in parallel \u2014 assemble's wall-clock is otherwise dominated by\n // `\u03A3 chunk-download-ms` instead of `max(chunk-download-ms)`. Preserve the\n // input order by writing into a pre-sized array rather than pushing as\n // each task settles.\n const local: string[] = new Array<string>(uris.length);\n await Promise.all(\n uris.map(async (uri, i) => {\n if (!uri) {\n throw new Error(`[handler] chunk URI at index ${i} is empty`);\n }\n const { key } = parseGcsUri(uri);\n const localPath = join(chunksDir, basename(key));\n await downloadGcsObjectToFile(storage, uri, localPath);\n if (format === \"png-sequence\") {\n const dirPath = join(chunksDir, `frames-${pad(i)}`);\n await untarDirectory(localPath, dirPath);\n local[i] = dirPath;\n } else {\n local[i] = localPath;\n }\n }),\n );\n return local;\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Collect every GCS URI that the handler will touch for a given event. */\nfunction getEventGcsUris(event: PlanEvent | RenderChunkEvent | AssembleEvent): string[] {\n switch (event.Action) {\n case \"plan\":\n return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];\n case \"renderChunk\":\n return [event.PlanGcsUri, event.ChunkOutputGcsPrefix];\n case \"assemble\":\n return [\n event.PlanGcsUri,\n ...event.ChunkGcsUris,\n event.OutputGcsUri,\n event.AudioGcsUri,\n ].filter((u): u is string => u != null);\n }\n}\n\n/** Emit the \"guard disabled\" warning at most once per instance. */\nlet warnedAllowlistDisabled = false;\n\n/**\n * Verify every GCS URI in the event resolves to the configured render\n * bucket. Throws `GCS_URI_NOT_ALLOWED` (non-retryable) when a URI targets a\n * different bucket, preventing request injection from reading or writing\n * arbitrary GCS data.\n *\n * Opt-out is explicit: set `HYPERFRAMES_RENDER_BUCKET=\"*\"` to disable the\n * guard intentionally. If the env var is simply unset (or empty), the guard\n * is disabled but a warning is logged once so the gap is visible in Cloud\n * Logging \u2014 it shouldn't silently fail open. The Terraform module always\n * wires the bucket name, so the prod path enforces.\n */\n// fallow-ignore-next-line complexity\nfunction validateEventGcsUris(event: PlanEvent | RenderChunkEvent | AssembleEvent): void {\n const allowedBucket = process.env.HYPERFRAMES_RENDER_BUCKET?.trim();\n if (allowedBucket === \"*\") return; // explicit, intentional opt-out\n if (!allowedBucket) {\n if (!warnedAllowlistDisabled) {\n warnedAllowlistDisabled = true;\n logEvent({\n event: \"bucket_allowlist_disabled\",\n level: \"WARNING\",\n message:\n \"HYPERFRAMES_RENDER_BUCKET is unset \u2014 the GCS bucket-allowlist guard is DISABLED. \" +\n 'Set it to the render bucket name to enforce, or to \"*\" to opt out intentionally.',\n });\n }\n return;\n }\n\n for (const uri of getEventGcsUris(event)) {\n const { bucket } = parseGcsUri(uri);\n if (bucket !== allowedBucket) {\n const err = new Error(\n `[handler] GCS_URI_NOT_ALLOWED: URI ${JSON.stringify(uri)} targets bucket \"${bucket}\" but only \"${allowedBucket}\" is permitted`,\n );\n err.name = \"GCS_URI_NOT_ALLOWED\";\n throw err;\n }\n }\n}\n\nfunction pad(n: number): string {\n return n.toString().padStart(4, \"0\");\n}\n\nfunction trimTrailingSlash(prefix: string): string {\n return prefix.endsWith(\"/\") ? prefix.slice(0, -1) : prefix;\n}\n\nfunction cleanupDir(dir: string): void {\n try {\n // Cloud Run re-uses an instance's filesystem across requests; clean up\n // aggressively so we don't leak a chunk-sized footprint between renders\n // (the writable filesystem counts against the instance's memory).\n rmSync(dir, { recursive: true, force: true });\n } catch {\n // Best-effort \u2014 leak is preferable to crashing on the success path.\n }\n}\n\n/**\n * Read the untarred planDir's `plan.json` and assert its `planHash` matches\n * what the workflow event claims. Throws on mismatch with a typed\n * `PLAN_HASH_MISMATCH` error name so the workflow's non-retryable list\n * routes it correctly. Defense-in-depth \u2014 the producer's `renderChunk` does\n * the same check internally \u2014 but performing it here lets us fail before\n * paying the Chrome-launch + per-frame capture cost on a misrouted chunk.\n */\n// fallow-ignore-next-line complexity\nfunction verifyPlanHash(planDir: string, expected: string): void {\n const planJsonPath = join(planDir, \"plan.json\");\n let parsed: { planHash?: unknown };\n try {\n parsed = JSON.parse(readFileSync(planJsonPath, \"utf-8\")) as { planHash?: unknown };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const error = new Error(`PLAN_HASH_MISMATCH: failed to read ${planJsonPath}: ${msg}`);\n error.name = \"PLAN_HASH_MISMATCH\";\n throw error;\n }\n const actual = parsed.planHash;\n if (typeof actual !== \"string\" || actual !== expected) {\n const error = new Error(\n `PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match plan.json planHash=${String(actual)}`,\n );\n error.name = \"PLAN_HASH_MISMATCH\";\n throw error;\n }\n}\n\n// \u2500\u2500 HTTP shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Error names the workflow treats as non-retryable. A request that fails\n * with one of these is the caller's fault (bad input, misrouted chunk) and\n * retrying it just burns instance-seconds, so we map them to HTTP 400 while\n * any other failure maps to 500 (which the workflow retry policy backs off\n * and re-attempts). Keep this list in sync with the `retry` predicate in\n * `packages/gcp-cloud-run/terraform/workflow.yaml`.\n */\nconst NON_RETRYABLE_ERROR_NAMES = new Set([\n // Handler-boundary guards.\n \"GCS_URI_NOT_ALLOWED\",\n \"PLAN_HASH_MISMATCH\",\n // Producer error class names (`.name`) + their string code aliases \u2014 the\n // class sets `.name` to the class name but wraps a `code`; cover both so a\n // raw-code throw is caught too. Mirrors the AWS state machine's\n // non-retryable list.\n \"FormatNotSupportedInDistributedError\",\n \"PlanTooLargeError\",\n \"RenderChunkValidationError\",\n \"FFMPEG_VERSION_MISMATCH\",\n \"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED\",\n \"PLAN_TOO_LARGE\",\n \"BROWSER_GPU_NOT_SOFTWARE\",\n \"FONT_FETCH_FAILED\",\n \"ChromeBinaryUnavailableError\",\n]);\n\n/**\n * Build the Hono app. A single `POST /` endpoint dispatches on the body's\n * `Action` field \u2014 the workflow points every step (plan, each renderChunk,\n * assemble) at the same URL and varies only the body. `GET /healthz` backs\n * the Cloud Run startup/liveness probe.\n *\n * `deps` is threaded through so tests can drive the real HTTP surface with\n * an injected Storage double + mocked primitives.\n */\nexport function createApp(deps?: HandlerDeps): Hono {\n const app = new Hono();\n\n app.get(\"/healthz\", (c) => c.json({ status: \"ok\" }));\n\n // fallow-ignore-next-line complexity\n app.post(\"/\", async (c) => {\n let body: CloudRunEvent;\n try {\n body = (await c.req.json()) as CloudRunEvent;\n } catch {\n return c.json({ error: \"BAD_REQUEST\", message: \"request body must be JSON\" }, 400);\n }\n try {\n const result = await dispatch(body, deps);\n return c.json(result, 200);\n } catch (err) {\n const name = err instanceof Error ? err.name : undefined;\n const message = err instanceof Error ? err.message : String(err);\n const status = name && NON_RETRYABLE_ERROR_NAMES.has(name) ? 400 : 500;\n // Surface `error` (the name) as the discriminator the workflow's\n // retry predicate keys off, plus `message` for human triage.\n return c.json({ error: name ?? \"RenderError\", message }, status);\n }\n });\n\n return app;\n}\n\n/** Start the HTTP server. Cloud Run injects `PORT` (default 8080). */\nexport function startServer(): void {\n const port = Number(process.env.PORT ?? 8080);\n const app = createApp();\n serve({ fetch: app.fetch, port }, (info) => {\n logEvent({ event: \"server_listening\", port: info.port });\n });\n}\n\n// Boot when executed directly (the Dockerfile runs `node dist/server.js`),\n// but not when imported by tests or the SDK.\nif (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {\n startServer();\n}\n", "/**\n * Cloud Run Chrome resolver.\n *\n * `renderChunk()` (the only primitive that needs a browser) launches Chrome\n * via the engine's `BrowserManager`. Because Cloud Run runs a container\n * image rather than a size-capped ZIP, the Chrome story is far simpler than\n * the Lambda adapter's: the `Dockerfile` installs `chrome-headless-shell`\n * (the same BeginFrame-capable build the K8s deploy uses) into the image at\n * a known path and exports `HYPERFRAMES_CHROME_PATH`. There is no runtime\n * decompression-into-/tmp step and no 250 MB packaging ceiling to fight.\n *\n * Resolution order:\n * 1. `PRODUCER_HEADLESS_SHELL_PATH` \u2014 the engine's own override. If a\n * caller (or the Docker image) already set it, honour it untouched.\n * 2. `HYPERFRAMES_CHROME_PATH` \u2014 set by the Dockerfile to the installed\n * `chrome-headless-shell` binary.\n * 3. A small list of conventional install paths, as a last resort for\n * images built outside our Dockerfile.\n *\n * Throws {@link ChromeBinaryUnavailableError} when nothing resolves, so a\n * misconfigured image fails loudly at the first chunk rather than emitting\n * a confusing puppeteer-core \"executablePath must be specified\" assertion.\n */\n\nimport { existsSync } from \"node:fs\";\n\n/**\n * Thrown when the Chrome binary resolver can't produce a usable path. The\n * class name is the workflow's non-retryable error discriminator.\n */\nexport class ChromeBinaryUnavailableError extends Error {\n // Read indirectly via the error envelope / Error.prototype.toString.\n // fallow-ignore-next-line unused-class-member\n override readonly name = \"ChromeBinaryUnavailableError\";\n readonly resolvedPath: string | null;\n constructor(resolvedPath: string | null, hint: string) {\n super(`[chromium] Chrome binary unavailable: ${hint}`);\n this.resolvedPath = resolvedPath;\n }\n}\n\n/**\n * Conventional locations a `chrome-headless-shell` (or full Chrome) binary\n * may live at in a Debian/Ubuntu-based container. Checked only after the\n * two env-var overrides miss.\n */\nconst FALLBACK_CHROME_PATHS = [\n \"/opt/chrome/chrome-headless-shell\",\n \"/usr/bin/chrome-headless-shell\",\n \"/usr/bin/google-chrome\",\n \"/usr/bin/google-chrome-stable\",\n \"/usr/bin/chromium\",\n \"/usr/bin/chromium-browser\",\n];\n\n/**\n * Resolve the absolute path to a Chrome binary suitable for BeginFrame.\n * Pure (no env mutation) so callers decide whether to export the result\n * into `PRODUCER_HEADLESS_SHELL_PATH`.\n */\n// fallow-ignore-next-line complexity\nexport function resolveChromeExecutablePath(): string {\n const fromEngineOverride = process.env.PRODUCER_HEADLESS_SHELL_PATH?.trim();\n if (fromEngineOverride) {\n if (!existsSync(fromEngineOverride)) {\n throw new ChromeBinaryUnavailableError(\n fromEngineOverride,\n `PRODUCER_HEADLESS_SHELL_PATH=${JSON.stringify(fromEngineOverride)} does not exist on disk.`,\n );\n }\n return fromEngineOverride;\n }\n\n const fromImage = process.env.HYPERFRAMES_CHROME_PATH?.trim();\n if (fromImage) {\n if (!existsSync(fromImage)) {\n throw new ChromeBinaryUnavailableError(\n fromImage,\n `HYPERFRAMES_CHROME_PATH=${JSON.stringify(fromImage)} does not exist on disk.`,\n );\n }\n return fromImage;\n }\n\n for (const candidate of FALLBACK_CHROME_PATHS) {\n if (existsSync(candidate)) return candidate;\n }\n\n throw new ChromeBinaryUnavailableError(\n null,\n \"no Chrome binary found. Set HYPERFRAMES_CHROME_PATH (the Dockerfile does this) or \" +\n \"PRODUCER_HEADLESS_SHELL_PATH to the absolute path of a chrome-headless-shell binary. \" +\n `Searched: ${FALLBACK_CHROME_PATHS.join(\", \")}.`,\n );\n}\n", "/**\n * Map a distributed `format` to the file extension the assembled output\n * should carry on disk + in GCS. Shared by `src/server.ts` (chunk +\n * assemble output paths) and `src/sdk/renderToCloudRun.ts` (final\n * output key construction) so the two sides agree on what an mp4\n * looks like vs a png-sequence.\n */\n\nimport type { DistributedFormat } from \"@hyperframes/producer/distributed\";\n\nexport type { DistributedFormat } from \"@hyperframes/producer/distributed\";\n\n// Closed-enum lookup table. TS enforces exhaustiveness via the\n// `Record<DistributedFormat, string>` annotation \u2014 adding a format to\n// `DistributedFormat` without adding the matching key here fails to\n// typecheck, which is the same exhaustiveness guarantee a switch +\n// `_exhaustive: never` arm provides but at lower complexity.\nconst FORMAT_EXTENSIONS: Record<DistributedFormat, string> = {\n mp4: \".mp4\",\n mov: \".mov\",\n webm: \".webm\",\n \"png-sequence\": \"\",\n};\n\nexport function formatExtension(format: DistributedFormat): string {\n return FORMAT_EXTENSIONS[format];\n}\n", "/**\n * Thin GCS transport for the Cloud Run handler.\n *\n * The OSS distributed primitives are pure functions over local file paths;\n * the handler bridges GCS \u2194 the container's writable `/tmp` filesystem on\n * each request. Functions here are intentionally narrow: parse a URI,\n * download an object to a local path, upload a path, tar-pack a planDir,\n * tar-extract a planDir back out.\n *\n * Tar (not zip) for planDir transit:\n * - planDirs contain symlinks (the extract stage materializes them but\n * the compiled/ subtree may include linked assets); tar preserves them,\n * zip does not.\n * - We use the `tar` npm package (pure JS over `node:zlib`) so the\n * archive format doesn't depend on a system `tar`/`unzip` being present\n * in the container image.\n *\n * Apart from the `gs://` scheme and the `@google-cloud/storage` client this\n * is the same shape as `@hyperframes/aws-lambda`'s `s3Transport.ts`.\n */\n\nimport { createWriteStream, existsSync, mkdirSync, rmSync, statSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { Storage } from \"@google-cloud/storage\";\nimport * as tar from \"tar\";\n\n/** Parsed `gs://bucket/key` URI. */\nexport interface GcsLocation {\n bucket: string;\n key: string;\n}\n\n/** Parse `gs://bucket/key/path` \u2192 `{ bucket, key }`. Throws on malformed input. */\n// fallow-ignore-next-line complexity\nexport function parseGcsUri(uri: string): GcsLocation {\n if (!uri.startsWith(\"gs://\")) {\n throw new Error(`[gcsTransport] expected gs:// URI, got: ${JSON.stringify(uri)}`);\n }\n const rest = uri.slice(\"gs://\".length);\n const slash = rest.indexOf(\"/\");\n if (slash === -1) {\n throw new Error(`[gcsTransport] missing key in gs URI: ${JSON.stringify(uri)}`);\n }\n const bucket = rest.slice(0, slash);\n const key = rest.slice(slash + 1);\n if (!bucket || !key) {\n throw new Error(`[gcsTransport] empty bucket or key in gs URI: ${JSON.stringify(uri)}`);\n }\n return { bucket, key };\n}\n\n/** Build `gs://bucket/key` from a location. */\nexport function formatGcsUri(loc: GcsLocation): string {\n return `gs://${loc.bucket}/${loc.key}`;\n}\n\n/** Stream a GCS object to a local file path. */\nexport async function downloadGcsObjectToFile(\n storage: Storage,\n uri: string,\n destPath: string,\n): Promise<void> {\n const { bucket, key } = parseGcsUri(uri);\n mkdirSync(dirname(destPath), { recursive: true });\n const file = storage.bucket(bucket).file(key);\n // `createReadStream` streams the object body; piping into a write stream\n // keeps memory flat for large plan tarballs / chunk files rather than\n // buffering the whole object the way `file.download()` would.\n await pipeline(file.createReadStream(), createWriteStream(destPath));\n}\n\n/**\n * Upload a local file's contents to a GCS URI using a resumable upload.\n * GCS objects have no practical size ceiling for the artifacts this adapter\n * handles (plan tarballs \u2264 2 GB, chunks \u2264 a few hundred MB), so a single\n * upload call works for every case.\n */\nexport async function uploadFileToGcs(\n storage: Storage,\n localPath: string,\n uri: string,\n contentType?: string,\n): Promise<void> {\n if (!existsSync(localPath)) {\n throw new Error(`[gcsTransport] upload source missing: ${localPath}`);\n }\n const { bucket, key } = parseGcsUri(uri);\n await storage.bucket(bucket).upload(localPath, {\n destination: key,\n // `resumable: false` (simple upload) is faster for the small-to-medium\n // objects this adapter moves and avoids the extra round-trip a resumable\n // session start costs; GCS recommends resumable only past ~8 MB but our\n // chunks are reliably above that, so let the client pick by default.\n contentType,\n });\n}\n\n/**\n * Pack a directory into a `.tar.gz` at `destTarball`. Uses the `tar` npm\n * package (pure JS over `node:zlib`) rather than spawning a system tar\n * binary so the archive format is independent of the container's userland.\n */\nexport async function tarDirectory(sourceDir: string, destTarball: string): Promise<void> {\n if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) {\n throw new Error(`[gcsTransport] tar source must be an existing directory: ${sourceDir}`);\n }\n mkdirSync(dirname(destTarball), { recursive: true });\n await tar.create({ gzip: true, file: destTarball, cwd: sourceDir }, [\".\"]);\n}\n\n/**\n * Extract a `.tar.gz` produced by {@link tarDirectory} into `destDir`.\n * The directory is created (or cleared) before extraction so a retried\n * request doesn't observe stale files from a prior run on the same warm\n * container instance.\n */\nexport async function untarDirectory(tarballPath: string, destDir: string): Promise<void> {\n if (!existsSync(tarballPath)) {\n throw new Error(`[gcsTransport] tarball missing: ${tarballPath}`);\n }\n // Wipe target so a warm container instance's prior planDir doesn't bleed\n // into the new request. Cloud Run re-uses the instance filesystem across\n // requests served by the same instance.\n if (existsSync(destDir)) {\n rmSync(destDir, { recursive: true, force: true });\n }\n mkdirSync(destDir, { recursive: true });\n await tar.extract({ file: tarballPath, cwd: destDir });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Cloud Run request handler for HyperFrames distributed rendering.\n *\n * One container image, three roles. Cloud Workflows POSTs a JSON body with\n * an `Action` field; the handler unwraps any `Payload`/`Input` envelope,\n * primes the runtime (Chrome path), and forwards to the matching OSS\n * primitive from `@hyperframes/producer/distributed`.\n *\n * Everything heavy \u2014 capture, encode, audio mix \u2014 happens inside the OSS\n * primitives. The handler is thin glue: parse body \u2192 GCS download \u2192 call\n * primitive \u2192 GCS upload \u2192 return small JSON result.\n *\n * `dispatch()` is the testable core (inject `storage` + `primitives`); the\n * Hono app at the bottom is the HTTP shell the Dockerfile runs. The shape\n * deliberately tracks `@hyperframes/aws-lambda`'s `handler.ts` so the two\n * adapters stay easy to diff.\n */\n\nimport { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, extname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { serve } from \"@hono/node-server\";\nimport { Storage } from \"@google-cloud/storage\";\nimport { Hono } from \"hono\";\nimport {\n assemble,\n type AssembleResult,\n type ChunkResult,\n type DistributedRenderConfig,\n listPlanV2ArtifactsForTarget,\n materializePlanV2Target,\n plan,\n planV2,\n type PlanResult,\n type PlanV2Artifact,\n type PlanV2MaterializationTarget,\n type PlanV2Result,\n readPlanV2Manifest,\n renderChunk,\n} from \"@hyperframes/producer/distributed\";\nimport { resolveChromeExecutablePath } from \"./chromium.js\";\nimport type {\n AssembleEvent,\n AssembleResultBody,\n CloudRunAction,\n CloudRunEvent,\n CloudRunResult,\n PlanEvent,\n PlanResultBody,\n RenderChunkEvent,\n RenderChunkResultBody,\n} from \"./events.js\";\nimport { type DistributedFormat, formatExtension } from \"./formatExtension.js\";\nimport {\n downloadGcsObjectToFile,\n downloadGcsObjectToFileVerified,\n parseGcsUri,\n sha256File,\n tarDirectory,\n untarDirectory,\n uploadContentAddressedFileToGcs,\n uploadFileToGcs,\n} from \"./gcsTransport.js\";\n\n/**\n * Lazily-constructed Storage client. Cached at module scope so warm\n * container instances reuse the underlying HTTP keep-alive pool across\n * requests.\n */\nlet cachedStorage: Storage | null = null;\nfunction getStorage(): Storage {\n if (cachedStorage) return cachedStorage;\n cachedStorage = new Storage();\n return cachedStorage;\n}\n\n/**\n * Optional injection points used by the handler's unit tests. Production\n * callers leave these unset; the real OSS primitives are used. Tests inject\n * `storage` and `primitives` directly rather than mutating module state.\n */\nexport interface HandlerDeps {\n storage?: Storage;\n primitives?: {\n plan: typeof plan;\n planV2?: typeof planV2;\n renderChunk: typeof renderChunk;\n assemble: typeof assemble;\n };\n /** Override the per-request workdir root (defaults to the OS tmpdir). */\n tmpRoot?: string;\n /** Skip Chrome resolution (used by dispatch tests that mock renderChunk). */\n skipChromeResolution?: boolean;\n}\n\n/**\n * Dispatch a single render request. Cloud Workflows (or a direct caller)\n * sometimes wraps the body in `{ Payload: ... }` or `{ Input: ... }`; unwrap\n * until we hit a discriminated event.\n */\n// fallow-ignore-next-line complexity\nexport async function dispatch(event: CloudRunEvent, deps?: HandlerDeps): Promise<CloudRunResult> {\n const unwrapped = unwrapEvent(event);\n validatePlanProtocolShape(unwrapped);\n validateEventGcsUris(unwrapped);\n logEvent({ event: \"handler_start\", action: unwrapped.Action, input: summarizeEvent(unwrapped) });\n try {\n switch (unwrapped.Action) {\n case \"plan\":\n return await handlePlan(unwrapped, deps);\n case \"renderChunk\":\n return await handleRenderChunk(unwrapped, deps);\n case \"assemble\":\n return await handleAssemble(unwrapped, deps);\n default: {\n // Compile-time exhaustiveness: a new CloudRunAction member trips\n // the `never` assignment before the runtime error is reachable.\n const _exhaustive: never = unwrapped;\n throw new Error(\n `[handler] unknown Action: ${JSON.stringify(\n (_exhaustive as { Action?: string }).Action,\n )}. Expected one of \"plan\", \"renderChunk\", \"assemble\".`,\n );\n }\n }\n } catch (err) {\n normalizeTerminalErrorName(err);\n logEvent({\n event: \"handler_error\",\n action: unwrapped.Action,\n input: summarizeEvent(unwrapped),\n message: err instanceof Error ? err.message : String(err),\n name: err instanceof Error ? err.name : undefined,\n });\n throw err;\n }\n}\n\n// This is the single fail-closed boundary for the wire union. Keeping all\n// forbidden locator combinations together makes mixed-protocol input auditable.\n// fallow-ignore-next-line complexity\nfunction validatePlanProtocolShape(event: PlanEvent | RenderChunkEvent | AssembleEvent): void {\n const raw = event as unknown as Record<string, unknown>;\n const protocol = raw.PlanProtocol;\n if (protocol !== undefined && protocol !== \"v1\" && protocol !== \"v2\") {\n const error = new Error(\n `[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected \"v1\", \"v2\", or absent`,\n );\n error.name = \"PLAN_PROTOCOL_UNSUPPORTED\";\n throw error;\n }\n if (event.Action === \"plan\") return;\n\n const hasV1Locator = typeof raw.PlanGcsUri === \"string\";\n const hasV2Manifest = typeof raw.PlanV2ManifestGcsUri === \"string\";\n const hasV2Prefix = typeof raw.PlanV2ArtifactGcsPrefix === \"string\";\n const valid =\n protocol === \"v2\"\n ? !hasV1Locator && hasV2Manifest && hasV2Prefix\n : hasV1Locator && !hasV2Manifest && !hasV2Prefix;\n if (!valid) {\n const error = new Error(\n `[handler] ${protocol === \"v2\" ? \"v2\" : \"v1\"} ${event.Action} event has mixed or missing plan locators`,\n );\n error.name = \"PLAN_PROTOCOL_UNSUPPORTED\";\n throw error;\n }\n if (protocol === \"v2\" && event.Action === \"assemble\" && event.AudioGcsUri !== null) {\n const error = new Error(\"[handler] v2 assemble audio must be materialized from the manifest\");\n error.name = \"PLAN_PROTOCOL_UNSUPPORTED\";\n throw error;\n }\n}\n\n/** Normalize producer error codes to the stable HTTP/workflow discriminator. */\n// The explicit mapping is the public Cloud Workflows retry contract.\n// fallow-ignore-next-line complexity\nfunction normalizeTerminalErrorName(error: unknown): void {\n if (!error || typeof error !== \"object\") return;\n const candidate = error as { code?: unknown; name?: string };\n if (\n candidate.code === \"PLAN_PROTOCOL_UNSUPPORTED\" ||\n candidate.code === \"PLAN_TOO_LARGE\" ||\n candidate.code === \"PLAN_V2_INTEGRITY_UNRECOVERABLE\"\n ) {\n candidate.name = candidate.code;\n }\n}\n\n// At most `{Payload: {Input: ...}}` is expected; 4 levels is 2\u00D7 headroom\n// and prevents infinite loops on malformed input.\nconst MAX_ENVELOPE_DEPTH = 4;\n\n// fallow-ignore-next-line complexity\nexport function unwrapEvent(event: CloudRunEvent): PlanEvent | RenderChunkEvent | AssembleEvent {\n let cursor: CloudRunEvent = event;\n for (let i = 0; i < MAX_ENVELOPE_DEPTH; i++) {\n if (cursor && typeof cursor === \"object\") {\n const obj = cursor as Record<string, unknown>;\n if (typeof obj.Action === \"string\" && isCloudRunAction(obj.Action)) {\n return cursor as PlanEvent | RenderChunkEvent | AssembleEvent;\n }\n if (\"Payload\" in obj) {\n cursor = obj.Payload as CloudRunEvent;\n continue;\n }\n if (\"Input\" in obj) {\n cursor = obj.Input as CloudRunEvent;\n continue;\n }\n }\n break;\n }\n throw new Error(\n `[handler] body has no recognised Action; unwrapped ${MAX_ENVELOPE_DEPTH} levels of Payload/Input without finding one.`,\n );\n}\n\nfunction isCloudRunAction(value: string): value is CloudRunAction {\n return value === \"plan\" || value === \"renderChunk\" || value === \"assemble\";\n}\n\n/**\n * Emit a single JSON line to stdout. Cloud Logging ingests each stdout line\n * as a structured `jsonPayload` entry, so Logs Explorer can filter on\n * `jsonPayload.event=\"handler_start\"` and project specific fields when\n * triaging without attaching a debugger.\n */\nfunction logEvent(payload: Record<string, unknown>): void {\n console.log(JSON.stringify(payload));\n}\n\n/**\n * Compact, non-PII summary of an event for logging. The full body can\n * include the entire project config; we only emit the routable fields\n * needed to triage a failure from Cloud Logging.\n */\n// Keep event variants together so Cloud Logging has one redaction boundary.\n// fallow-ignore-next-line complexity\nfunction summarizeEvent(\n event: PlanEvent | RenderChunkEvent | AssembleEvent,\n): Record<string, unknown> {\n switch (event.Action) {\n case \"plan\":\n return {\n projectGcsUri: event.ProjectGcsUri,\n planOutputGcsPrefix: event.PlanOutputGcsPrefix,\n planProtocol: event.PlanProtocol ?? \"v1\",\n format: event.Config.format,\n fps: event.Config.fps,\n };\n case \"renderChunk\":\n return {\n planProtocol: event.PlanProtocol ?? \"v1\",\n ...(event.PlanProtocol === \"v2\"\n ? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri }\n : { planGcsUri: event.PlanGcsUri }),\n chunkIndex: event.ChunkIndex,\n format: event.Format,\n };\n case \"assemble\":\n return {\n planProtocol: event.PlanProtocol ?? \"v1\",\n ...(event.PlanProtocol === \"v2\"\n ? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri }\n : { planGcsUri: event.PlanGcsUri }),\n chunkCount: event.ChunkGcsUris.length,\n hasAudio: event.AudioGcsUri !== null,\n outputGcsUri: event.OutputGcsUri,\n format: event.Format,\n };\n }\n}\n\n/**\n * Point the engine at the in-image Chrome binary. The OSS engine resolves\n * Chrome via `PRODUCER_HEADLESS_SHELL_PATH` first; set it once per instance\n * before invoking any browser-touching primitive. ffmpeg is on the image's\n * PATH (apt-installed by the Dockerfile), so nothing to prime there.\n */\nfunction primeChrome(deps?: HandlerDeps): void {\n if (deps?.skipChromeResolution) return;\n if (process.env.PRODUCER_HEADLESS_SHELL_PATH) return;\n process.env.PRODUCER_HEADLESS_SHELL_PATH = resolveChromeExecutablePath();\n}\n\n// \u2500\u2500 Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// fallow-ignore-next-line complexity\nasync function handlePlan(event: PlanEvent, deps?: HandlerDeps): Promise<PlanResultBody> {\n if (event.PlanProtocol === \"v2\") {\n return handlePlanV2(event, deps);\n }\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.plan ?? plan;\n\n // The producer's probe stage launches Chromium whenever the composition\n // needs a runtime duration probe or has unresolved sub-compositions, so\n // plan has to resolve Chrome the same way renderChunk does.\n primeChrome(deps);\n\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-plan-\"));\n const projectArchive = join(work, \"project.tar.gz\");\n const projectDir = join(work, \"project\");\n const planDir = join(work, \"plan\");\n\n try {\n await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);\n await untarDirectory(projectArchive, projectDir);\n\n const config: DistributedRenderConfig = {\n ...event.Config,\n };\n const result: PlanResult = await primitive(projectDir, config, planDir);\n\n // Upload the planDir as a single tarball. The workflow cannot pass a\n // directory-shaped artifact between steps; we serialize and rely on the\n // consumer (renderChunk / assemble) to untar. `audio.aac` lives inside\n // planDir, so it already rides along in this tarball \u2014 every consumer\n // (including assemble) gets it from the untar. We deliberately do NOT\n // upload a separate audio object: it would duplicate the bytes on every\n // plan upload and be re-downloaded + overwritten by assemble. `AudioGcsUri`\n // stays in the result shape for wire compatibility but is null.\n const planTar = join(work, \"plan.tar.gz\");\n await tarDirectory(planDir, planTar);\n const planTarUri = `${trimTrailingSlash(event.PlanOutputGcsPrefix)}/plan.tar.gz`;\n const audioPath = join(planDir, \"audio.aac\");\n const hasAudio = existsSync(audioPath) && statSync(audioPath).size > 0;\n await uploadFileToGcs(storage, planTar, planTarUri, \"application/gzip\");\n\n return {\n Action: \"plan\",\n PlanGcsUri: planTarUri,\n PlanHash: result.planHash,\n ChunkCount: result.chunkCount,\n TotalFrames: result.totalFrames,\n Fps: result.fps,\n Width: result.width,\n Height: result.height,\n Format: result.format,\n HasAudio: hasAudio,\n AudioGcsUri: null,\n FfmpegVersion: result.ffmpegVersion,\n ProducerVersion: result.producerVersion,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\n/**\n * Stage immutable v2 artifacts, upload them to content-addressed keys, then\n * publish the manifest as the final commit point.\n */\n// fallow-ignore-next-line complexity\nasync function handlePlanV2(\n event: Extract<PlanEvent, { PlanProtocol: \"v2\" }>,\n deps?: HandlerDeps,\n): Promise<Extract<PlanResultBody, { PlanProtocol: \"v2\" }>> {\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.planV2 ?? planV2;\n primeChrome(deps);\n\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-plan-v2-\"));\n const projectArchive = join(work, \"project.tar.gz\");\n const projectDir = join(work, \"project\");\n const planV2Dir = join(work, \"plan-v2\");\n try {\n await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);\n await untarDirectory(projectArchive, projectDir);\n const result: PlanV2Result = await primitive(projectDir, { ...event.Config }, planV2Dir);\n const manifest = readPlanV2Manifest(planV2Dir);\n if (manifest.planHash !== result.planHash) {\n throwPlanHashMismatch(result.planHash, manifest.planHash);\n }\n\n const outputPrefix = `${trimTrailingSlash(event.PlanOutputGcsPrefix)}/v2`;\n const artifactPrefix = `${outputPrefix}/artifacts/sha256`;\n const uniqueArtifacts = [\n ...new Map(manifest.artifacts.map((artifact) => [artifact.sha256, artifact])).values(),\n ];\n await mapConcurrent(uniqueArtifacts, 16, async (artifact) => {\n await uploadContentAddressedFileToGcs(\n storage,\n planV2BlobPath(planV2Dir, artifact.sha256),\n planV2BlobUri(artifactPrefix, artifact.sha256),\n artifact.sha256,\n );\n });\n\n const manifestUri = `${outputPrefix}/manifest.json`;\n await uploadContentAddressedFileToGcs(\n storage,\n result.manifestPath,\n manifestUri,\n await sha256File(result.manifestPath),\n \"application/json\",\n );\n\n return {\n Action: \"plan\",\n PlanProtocol: \"v2\",\n PlanV2ManifestGcsUri: manifestUri,\n PlanV2ArtifactGcsPrefix: artifactPrefix,\n PlanHash: result.planHash,\n ChunkCount: result.chunkCount,\n TotalFrames: result.totalFrames,\n Fps: result.fps,\n Width: result.width,\n Height: result.height,\n Format: result.format,\n HasAudio: manifest.artifacts.some((artifact) => artifact.path === \"audio.aac\"),\n AudioGcsUri: null,\n FfmpegVersion: result.ffmpegVersion,\n ProducerVersion: result.producerVersion,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\n// \u2500\u2500 RenderChunk \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// fallow-ignore-next-line complexity\nasync function handleRenderChunk(\n event: RenderChunkEvent,\n deps?: HandlerDeps,\n): Promise<RenderChunkResultBody> {\n if (event.PlanProtocol === \"v2\") {\n return handleRenderChunkV2(event, deps);\n }\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.renderChunk ?? renderChunk;\n\n primeChrome(deps);\n\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-chunk-\"));\n const planTar = join(work, \"plan.tar.gz\");\n const planDir = join(work, \"plan\");\n\n try {\n await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);\n await untarDirectory(planTar, planDir);\n\n // Verify the plan's hash matches what the workflow told us to render.\n // The producer's renderChunk re-checks internally (defense-in-depth),\n // but doing it here at the handler boundary lets us fail before paying\n // the Chrome-launch + render cost on a misrouted chunk. Throws a typed\n // PLAN_HASH_MISMATCH the workflow can route as non-retryable.\n verifyPlanHash(planDir, event.PlanHash);\n\n const chunkOutputBase = join(\n work,\n event.Format === \"png-sequence\"\n ? `chunk-${pad(event.ChunkIndex)}`\n : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`,\n );\n\n const result: ChunkResult = await primitive(planDir, event.ChunkIndex, chunkOutputBase);\n\n const chunkUri = await uploadChunkOutput(\n storage,\n result,\n event.ChunkOutputGcsPrefix,\n event.ChunkIndex,\n );\n\n return {\n Action: \"renderChunk\",\n ChunkGcsUri: chunkUri,\n ChunkIndex: event.ChunkIndex,\n Sha256: result.sha256,\n FramesEncoded: result.framesEncoded,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\n/** Materialize only this chunk's verified v2 dependencies before rendering. */\n// fallow-ignore-next-line complexity\nasync function handleRenderChunkV2(\n event: Extract<RenderChunkEvent, { PlanProtocol: \"v2\" }>,\n deps?: HandlerDeps,\n): Promise<RenderChunkResultBody> {\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.renderChunk ?? renderChunk;\n primeChrome(deps);\n\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-chunk-v2-\"));\n try {\n const planDir = await downloadAndMaterializePlanV2(\n storage,\n event,\n { role: \"chunk\", chunkIndex: event.ChunkIndex },\n work,\n );\n const chunkOutputBase = join(\n work,\n event.Format === \"png-sequence\"\n ? `chunk-${pad(event.ChunkIndex)}`\n : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`,\n );\n const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);\n const chunkUri = await uploadChunkOutput(\n storage,\n result,\n event.ChunkOutputGcsPrefix,\n event.ChunkIndex,\n );\n return {\n Action: \"renderChunk\",\n ChunkGcsUri: chunkUri,\n ChunkIndex: event.ChunkIndex,\n Sha256: result.sha256,\n FramesEncoded: result.framesEncoded,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\nasync function uploadChunkOutput(\n storage: Storage,\n result: ChunkResult,\n prefix: string,\n chunkIndex: number,\n): Promise<string> {\n const trimmed = trimTrailingSlash(prefix);\n if (result.outputKind === \"file\") {\n const ext = extname(result.outputPath);\n const uri = `${trimmed}/chunks/${pad(chunkIndex)}${ext}`;\n await uploadFileToGcs(storage, result.outputPath, uri);\n return uri;\n }\n // frame-dir: upload as a tarball so a single GCS object represents the\n // chunk. Assemble's png-sequence path expects a directory per chunk; it\n // untars on its end.\n const tarball = `${result.outputPath}.tar.gz`;\n await tarDirectory(result.outputPath, tarball);\n const uri = `${trimmed}/chunks/${pad(chunkIndex)}.tar.gz`;\n await uploadFileToGcs(storage, tarball, uri, \"application/gzip\");\n return uri;\n}\n\n// \u2500\u2500 Assemble \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// fallow-ignore-next-line complexity\nasync function handleAssemble(\n event: AssembleEvent,\n deps?: HandlerDeps,\n): Promise<AssembleResultBody> {\n if (event.PlanProtocol === \"v2\") {\n return handleAssembleV2(event, deps);\n }\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.assemble ?? assemble;\n\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-assemble-\"));\n const planTar = join(work, \"plan.tar.gz\");\n const planDir = join(work, \"plan\");\n\n try {\n await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);\n await untarDirectory(planTar, planDir);\n\n const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);\n\n // Audio rides inside the plan tarball, so it's already on disk after the\n // untar above \u2014 no separate download. Fall back to a supplied AudioGcsUri\n // only for backward compatibility with an older Plan that uploaded it\n // standalone.\n let audioPath: string | null = null;\n const planAudio = join(planDir, \"audio.aac\");\n if (existsSync(planAudio) && statSync(planAudio).size > 0) {\n audioPath = planAudio;\n } else if (event.AudioGcsUri) {\n audioPath = planAudio;\n await downloadGcsObjectToFile(storage, event.AudioGcsUri, audioPath);\n }\n\n const finalOutput =\n event.Format === \"png-sequence\"\n ? join(work, \"output-frames\")\n : join(work, `output${formatExtension(event.Format)}`);\n\n const result: AssembleResult = await primitive(planDir, chunkPaths, audioPath, finalOutput, {\n cfr: event.Cfr === true,\n });\n\n if (event.Format === \"png-sequence\") {\n const tarball = `${finalOutput}.tar.gz`;\n await tarDirectory(finalOutput, tarball);\n await uploadFileToGcs(storage, tarball, event.OutputGcsUri, \"application/gzip\");\n } else {\n await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);\n }\n\n return {\n Action: \"assemble\",\n OutputGcsUri: event.OutputGcsUri,\n FramesEncoded: result.framesEncoded,\n FileSize: result.fileSize,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\n/**\n * Materialize the assembler target. Audio is declared assembler-only by the\n * v2 manifest and therefore is never downloaded by chunk workers.\n */\n// fallow-ignore-next-line complexity\nasync function handleAssembleV2(\n event: Extract<AssembleEvent, { PlanProtocol: \"v2\" }>,\n deps?: HandlerDeps,\n): Promise<AssembleResultBody> {\n const started = Date.now();\n const storage = deps?.storage ?? getStorage();\n const primitive = deps?.primitives?.assemble ?? assemble;\n const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), \"hf-cr-assemble-v2-\"));\n try {\n const planDir = await downloadAndMaterializePlanV2(storage, event, { role: \"assembler\" }, work);\n const audioPath = existsSync(join(planDir, \"audio.aac\")) ? join(planDir, \"audio.aac\") : null;\n const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);\n const finalOutput =\n event.Format === \"png-sequence\"\n ? join(work, \"output-frames\")\n : join(work, `output${formatExtension(event.Format)}`);\n const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {\n cfr: event.Cfr === true,\n });\n if (event.Format === \"png-sequence\") {\n const tarball = `${finalOutput}.tar.gz`;\n await tarDirectory(finalOutput, tarball);\n await uploadFileToGcs(storage, tarball, event.OutputGcsUri, \"application/gzip\");\n } else {\n await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);\n }\n return {\n Action: \"assemble\",\n OutputGcsUri: event.OutputGcsUri,\n FramesEncoded: result.framesEncoded,\n FileSize: result.fileSize,\n DurationMs: Date.now() - started,\n };\n } finally {\n cleanupDir(work);\n }\n}\n\nasync function downloadAndMaterializePlanV2(\n storage: Storage,\n event: {\n PlanV2ManifestGcsUri: string;\n PlanV2ArtifactGcsPrefix: string;\n PlanHash: string;\n },\n target: PlanV2MaterializationTarget,\n work: string,\n): Promise<string> {\n const transportDir = join(work, \"plan-v2\");\n mkdirSync(transportDir, { recursive: true });\n await downloadGcsObjectToFile(\n storage,\n event.PlanV2ManifestGcsUri,\n join(transportDir, \"plan.json\"),\n );\n const manifest = readPlanV2Manifest(transportDir);\n if (manifest.planHash !== event.PlanHash) {\n throwPlanHashMismatch(event.PlanHash, manifest.planHash);\n }\n const artifacts = listPlanV2ArtifactsForTarget(manifest, target);\n const uniqueArtifacts = [\n ...new Map(artifacts.map((artifact) => [artifact.sha256, artifact])).values(),\n ];\n await mapConcurrent(uniqueArtifacts, 16, async (artifact) => {\n await downloadPlanV2Artifact(storage, event.PlanV2ArtifactGcsPrefix, transportDir, artifact);\n });\n const planDir = join(work, \"plan\");\n materializePlanV2Target(transportDir, target, planDir);\n return planDir;\n}\n\nasync function downloadPlanV2Artifact(\n storage: Storage,\n artifactPrefix: string,\n planV2Dir: string,\n artifact: Readonly<PlanV2Artifact>,\n): Promise<void> {\n await downloadGcsObjectToFileVerified(\n storage,\n planV2BlobUri(artifactPrefix, artifact.sha256),\n planV2BlobPath(planV2Dir, artifact.sha256),\n artifact.sha256,\n );\n}\n\nfunction planV2BlobPath(planV2Dir: string, digest: string): string {\n return join(planV2Dir, \"artifacts\", \"sha256\", digest.slice(0, 2), digest);\n}\n\nfunction planV2BlobUri(prefix: string, digest: string): string {\n return `${trimTrailingSlash(prefix)}/${digest.slice(0, 2)}/${digest}`;\n}\n\nfunction throwPlanHashMismatch(expected: string, actual: string): never {\n const error = new Error(\n `PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match v2 manifest planHash=${actual}`,\n );\n error.name = \"PLAN_HASH_MISMATCH\";\n throw error;\n}\n\nasync function mapConcurrent<T>(\n values: readonly T[],\n concurrency: number,\n fn: (value: T) => Promise<void>,\n): Promise<void> {\n let cursor = 0;\n async function worker(): Promise<void> {\n while (cursor < values.length) {\n const index = cursor++;\n await fn(values[index]!);\n }\n }\n await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => worker()));\n}\n\nasync function downloadChunkObjects(\n storage: Storage,\n uris: string[],\n workDir: string,\n format: DistributedFormat,\n): Promise<string[]> {\n const chunksDir = join(workDir, \"chunks\");\n mkdirSync(chunksDir, { recursive: true });\n // Each chunk is an independent GCS GET (+ untar for png-sequence). Run\n // them in parallel \u2014 assemble's wall-clock is otherwise dominated by\n // `\u03A3 chunk-download-ms` instead of `max(chunk-download-ms)`. Preserve the\n // input order by writing into a pre-sized array rather than pushing as\n // each task settles.\n const local: string[] = new Array<string>(uris.length);\n await Promise.all(\n uris.map(async (uri, i) => {\n if (!uri) {\n throw new Error(`[handler] chunk URI at index ${i} is empty`);\n }\n const { key } = parseGcsUri(uri);\n const localPath = join(chunksDir, basename(key));\n await downloadGcsObjectToFile(storage, uri, localPath);\n if (format === \"png-sequence\") {\n const dirPath = join(chunksDir, `frames-${pad(i)}`);\n await untarDirectory(localPath, dirPath);\n local[i] = dirPath;\n } else {\n local[i] = localPath;\n }\n }),\n );\n return local;\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Collect every GCS URI that the handler will touch for a given event. */\n// This exhaustive event projection is the bucket-allowlist security boundary.\n// fallow-ignore-next-line complexity\nfunction getEventGcsUris(event: PlanEvent | RenderChunkEvent | AssembleEvent): string[] {\n switch (event.Action) {\n case \"plan\":\n return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];\n case \"renderChunk\":\n return event.PlanProtocol === \"v2\"\n ? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix, event.ChunkOutputGcsPrefix]\n : [event.PlanGcsUri, event.ChunkOutputGcsPrefix];\n case \"assemble\":\n return [\n ...(event.PlanProtocol === \"v2\"\n ? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix]\n : [event.PlanGcsUri]),\n ...event.ChunkGcsUris,\n event.OutputGcsUri,\n event.AudioGcsUri,\n ].filter((u): u is string => u != null);\n }\n}\n\n/** Emit the \"guard disabled\" warning at most once per instance. */\nlet warnedAllowlistDisabled = false;\n\n/**\n * Verify every GCS URI in the event resolves to the configured render\n * bucket. Throws `GCS_URI_NOT_ALLOWED` (non-retryable) when a URI targets a\n * different bucket, preventing request injection from reading or writing\n * arbitrary GCS data.\n *\n * Opt-out is explicit: set `HYPERFRAMES_RENDER_BUCKET=\"*\"` to disable the\n * guard intentionally. If the env var is simply unset (or empty), the guard\n * is disabled but a warning is logged once so the gap is visible in Cloud\n * Logging \u2014 it shouldn't silently fail open. The Terraform module always\n * wires the bucket name, so the prod path enforces.\n */\n// fallow-ignore-next-line complexity\nfunction validateEventGcsUris(event: PlanEvent | RenderChunkEvent | AssembleEvent): void {\n const allowedBucket = process.env.HYPERFRAMES_RENDER_BUCKET?.trim();\n if (allowedBucket === \"*\") return; // explicit, intentional opt-out\n if (!allowedBucket) {\n if (!warnedAllowlistDisabled) {\n warnedAllowlistDisabled = true;\n logEvent({\n event: \"bucket_allowlist_disabled\",\n level: \"WARNING\",\n message:\n \"HYPERFRAMES_RENDER_BUCKET is unset \u2014 the GCS bucket-allowlist guard is DISABLED. \" +\n 'Set it to the render bucket name to enforce, or to \"*\" to opt out intentionally.',\n });\n }\n return;\n }\n\n for (const uri of getEventGcsUris(event)) {\n const { bucket } = parseGcsUri(uri);\n if (bucket !== allowedBucket) {\n const err = new Error(\n `[handler] GCS_URI_NOT_ALLOWED: URI ${JSON.stringify(uri)} targets bucket \"${bucket}\" but only \"${allowedBucket}\" is permitted`,\n );\n err.name = \"GCS_URI_NOT_ALLOWED\";\n throw err;\n }\n }\n}\n\nfunction pad(n: number): string {\n return n.toString().padStart(4, \"0\");\n}\n\nfunction trimTrailingSlash(prefix: string): string {\n return prefix.endsWith(\"/\") ? prefix.slice(0, -1) : prefix;\n}\n\nfunction cleanupDir(dir: string): void {\n try {\n // Cloud Run re-uses an instance's filesystem across requests; clean up\n // aggressively so we don't leak a chunk-sized footprint between renders\n // (the writable filesystem counts against the instance's memory).\n rmSync(dir, { recursive: true, force: true });\n } catch {\n // Best-effort \u2014 leak is preferable to crashing on the success path.\n }\n}\n\n/**\n * Read the untarred planDir's `plan.json` and assert its `planHash` matches\n * what the workflow event claims. Throws on mismatch with a typed\n * `PLAN_HASH_MISMATCH` error name so the workflow's non-retryable list\n * routes it correctly. Defense-in-depth \u2014 the producer's `renderChunk` does\n * the same check internally \u2014 but performing it here lets us fail before\n * paying the Chrome-launch + per-frame capture cost on a misrouted chunk.\n */\n// fallow-ignore-next-line complexity\nfunction verifyPlanHash(planDir: string, expected: string): void {\n const planJsonPath = join(planDir, \"plan.json\");\n let parsed: { planHash?: unknown };\n try {\n parsed = JSON.parse(readFileSync(planJsonPath, \"utf-8\")) as { planHash?: unknown };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const error = new Error(`PLAN_HASH_MISMATCH: failed to read ${planJsonPath}: ${msg}`);\n error.name = \"PLAN_HASH_MISMATCH\";\n throw error;\n }\n const actual = parsed.planHash;\n if (typeof actual !== \"string\" || actual !== expected) {\n const error = new Error(\n `PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match plan.json planHash=${String(actual)}`,\n );\n error.name = \"PLAN_HASH_MISMATCH\";\n throw error;\n }\n}\n\n// \u2500\u2500 HTTP shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Error names the workflow treats as non-retryable. A request that fails\n * with one of these is the caller's fault (bad input, misrouted chunk) and\n * retrying it just burns instance-seconds, so we map them to HTTP 400 while\n * any other failure maps to 500 (which the workflow retry policy backs off\n * and re-attempts). Keep this list in sync with the `retry` predicate in\n * `packages/gcp-cloud-run/terraform/workflow.yaml`.\n */\nconst NON_RETRYABLE_ERROR_NAMES = new Set([\n // Handler-boundary guards.\n \"GCS_URI_NOT_ALLOWED\",\n \"PLAN_HASH_MISMATCH\",\n \"PLAN_ARTIFACT_DIGEST_MISMATCH\",\n \"PLAN_PROTOCOL_UNSUPPORTED\",\n \"PLAN_V2_INTEGRITY_UNRECOVERABLE\",\n // Producer error class names (`.name`) + their string code aliases \u2014 the\n // class sets `.name` to the class name but wraps a `code`; cover both so a\n // raw-code throw is caught too. Mirrors the AWS state machine's\n // non-retryable list.\n \"FormatNotSupportedInDistributedError\",\n \"PlanTooLargeError\",\n \"PlanProtocolUnsupportedError\",\n \"PlanV2IntegrityError\",\n \"RenderChunkValidationError\",\n \"FFMPEG_VERSION_MISMATCH\",\n \"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED\",\n \"PLAN_TOO_LARGE\",\n \"BROWSER_GPU_NOT_SOFTWARE\",\n \"FONT_FETCH_FAILED\",\n \"ChromeBinaryUnavailableError\",\n]);\n\n/**\n * Build the Hono app. A single `POST /` endpoint dispatches on the body's\n * `Action` field \u2014 the workflow points every step (plan, each renderChunk,\n * assemble) at the same URL and varies only the body. `GET /healthz` backs\n * the Cloud Run startup/liveness probe.\n *\n * `deps` is threaded through so tests can drive the real HTTP surface with\n * an injected Storage double + mocked primitives.\n */\nexport function createApp(deps?: HandlerDeps): Hono {\n const app = new Hono();\n\n app.get(\"/healthz\", (c) => c.json({ status: \"ok\" }));\n\n // fallow-ignore-next-line complexity\n app.post(\"/\", async (c) => {\n let body: CloudRunEvent;\n try {\n body = (await c.req.json()) as CloudRunEvent;\n } catch {\n return c.json({ error: \"BAD_REQUEST\", message: \"request body must be JSON\" }, 400);\n }\n try {\n const result = await dispatch(body, deps);\n return c.json(result, 200);\n } catch (err) {\n const name = err instanceof Error ? err.name : undefined;\n const message = err instanceof Error ? err.message : String(err);\n const status = name && NON_RETRYABLE_ERROR_NAMES.has(name) ? 400 : 500;\n // Surface `error` (the name) as the discriminator the workflow's\n // retry predicate keys off, plus `message` for human triage.\n return c.json({ error: name ?? \"RenderError\", message }, status);\n }\n });\n\n return app;\n}\n\n/** Start the HTTP server. Cloud Run injects `PORT` (default 8080). */\nexport function startServer(): void {\n const port = Number(process.env.PORT ?? 8080);\n const app = createApp();\n serve({ fetch: app.fetch, port }, (info) => {\n logEvent({ event: \"server_listening\", port: info.port });\n });\n}\n\n// Boot when executed directly (the Dockerfile runs `node dist/server.js`),\n// but not when imported by tests or the SDK.\nif (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {\n startServer();\n}\n", "/**\n * Cloud Run Chrome resolver.\n *\n * `renderChunk()` (the only primitive that needs a browser) launches Chrome\n * via the engine's `BrowserManager`. Because Cloud Run runs a container\n * image rather than a size-capped ZIP, the Chrome story is far simpler than\n * the Lambda adapter's: the `Dockerfile` installs `chrome-headless-shell`\n * (the same BeginFrame-capable build the K8s deploy uses) into the image at\n * a known path and exports `HYPERFRAMES_CHROME_PATH`. There is no runtime\n * decompression-into-/tmp step and no 250 MB packaging ceiling to fight.\n *\n * Resolution order:\n * 1. `PRODUCER_HEADLESS_SHELL_PATH` \u2014 the engine's own override. If a\n * caller (or the Docker image) already set it, honour it untouched.\n * 2. `HYPERFRAMES_CHROME_PATH` \u2014 set by the Dockerfile to the installed\n * `chrome-headless-shell` binary.\n * 3. A small list of conventional install paths, as a last resort for\n * images built outside our Dockerfile.\n *\n * Throws {@link ChromeBinaryUnavailableError} when nothing resolves, so a\n * misconfigured image fails loudly at the first chunk rather than emitting\n * a confusing puppeteer-core \"executablePath must be specified\" assertion.\n */\n\nimport { existsSync } from \"node:fs\";\n\n/**\n * Thrown when the Chrome binary resolver can't produce a usable path. The\n * class name is the workflow's non-retryable error discriminator.\n */\nexport class ChromeBinaryUnavailableError extends Error {\n // Read indirectly via the error envelope / Error.prototype.toString.\n // fallow-ignore-next-line unused-class-member\n override readonly name = \"ChromeBinaryUnavailableError\";\n readonly resolvedPath: string | null;\n constructor(resolvedPath: string | null, hint: string) {\n super(`[chromium] Chrome binary unavailable: ${hint}`);\n this.resolvedPath = resolvedPath;\n }\n}\n\n/**\n * Conventional locations a `chrome-headless-shell` (or full Chrome) binary\n * may live at in a Debian/Ubuntu-based container. Checked only after the\n * two env-var overrides miss.\n */\nconst FALLBACK_CHROME_PATHS = [\n \"/opt/chrome/chrome-headless-shell\",\n \"/usr/bin/chrome-headless-shell\",\n \"/usr/bin/google-chrome\",\n \"/usr/bin/google-chrome-stable\",\n \"/usr/bin/chromium\",\n \"/usr/bin/chromium-browser\",\n];\n\n/**\n * Resolve the absolute path to a Chrome binary suitable for BeginFrame.\n * Pure (no env mutation) so callers decide whether to export the result\n * into `PRODUCER_HEADLESS_SHELL_PATH`.\n */\n// fallow-ignore-next-line complexity\nexport function resolveChromeExecutablePath(): string {\n const fromEngineOverride = process.env.PRODUCER_HEADLESS_SHELL_PATH?.trim();\n if (fromEngineOverride) {\n if (!existsSync(fromEngineOverride)) {\n throw new ChromeBinaryUnavailableError(\n fromEngineOverride,\n `PRODUCER_HEADLESS_SHELL_PATH=${JSON.stringify(fromEngineOverride)} does not exist on disk.`,\n );\n }\n return fromEngineOverride;\n }\n\n const fromImage = process.env.HYPERFRAMES_CHROME_PATH?.trim();\n if (fromImage) {\n if (!existsSync(fromImage)) {\n throw new ChromeBinaryUnavailableError(\n fromImage,\n `HYPERFRAMES_CHROME_PATH=${JSON.stringify(fromImage)} does not exist on disk.`,\n );\n }\n return fromImage;\n }\n\n for (const candidate of FALLBACK_CHROME_PATHS) {\n if (existsSync(candidate)) return candidate;\n }\n\n throw new ChromeBinaryUnavailableError(\n null,\n \"no Chrome binary found. Set HYPERFRAMES_CHROME_PATH (the Dockerfile does this) or \" +\n \"PRODUCER_HEADLESS_SHELL_PATH to the absolute path of a chrome-headless-shell binary. \" +\n `Searched: ${FALLBACK_CHROME_PATHS.join(\", \")}.`,\n );\n}\n", "/**\n * Map a distributed `format` to the file extension the assembled output\n * should carry on disk + in GCS. Shared by `src/server.ts` (chunk +\n * assemble output paths) and `src/sdk/renderToCloudRun.ts` (final\n * output key construction) so the two sides agree on what an mp4\n * looks like vs a png-sequence.\n */\n\nimport type { DistributedFormat } from \"@hyperframes/producer/distributed\";\n\nexport type { DistributedFormat } from \"@hyperframes/producer/distributed\";\n\n// Closed-enum lookup table. TS enforces exhaustiveness via the\n// `Record<DistributedFormat, string>` annotation \u2014 adding a format to\n// `DistributedFormat` without adding the matching key here fails to\n// typecheck, which is the same exhaustiveness guarantee a switch +\n// `_exhaustive: never` arm provides but at lower complexity.\nconst FORMAT_EXTENSIONS: Record<DistributedFormat, string> = {\n mp4: \".mp4\",\n mov: \".mov\",\n webm: \".webm\",\n \"png-sequence\": \"\",\n};\n\nexport function formatExtension(format: DistributedFormat): string {\n return FORMAT_EXTENSIONS[format];\n}\n", "/**\n * Thin GCS transport for the Cloud Run handler.\n *\n * The OSS distributed primitives are pure functions over local file paths;\n * the handler bridges GCS \u2194 the container's writable `/tmp` filesystem on\n * each request. Functions here are intentionally narrow: parse a URI,\n * download an object to a local path, upload a path, tar-pack a planDir,\n * tar-extract a planDir back out.\n *\n * Tar (not zip) for planDir transit:\n * - planDirs contain symlinks (the extract stage materializes them but\n * the compiled/ subtree may include linked assets); tar preserves them,\n * zip does not.\n * - We use the `tar` npm package (pure JS over `node:zlib`) so the\n * archive format doesn't depend on a system `tar`/`unzip` being present\n * in the container image.\n *\n * Apart from the `gs://` scheme and the `@google-cloud/storage` client this\n * is the same shape as `@hyperframes/aws-lambda`'s `s3Transport.ts`.\n */\n\nimport {\n createReadStream,\n createWriteStream,\n existsSync,\n mkdirSync,\n rmSync,\n statSync,\n} from \"node:fs\";\nimport { createHash } from \"node:crypto\";\nimport { dirname } from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { Storage } from \"@google-cloud/storage\";\nimport * as tar from \"tar\";\n\n/** Parsed `gs://bucket/key` URI. */\nexport interface GcsLocation {\n bucket: string;\n key: string;\n}\n\n/** Parse `gs://bucket/key/path` \u2192 `{ bucket, key }`. Throws on malformed input. */\n// fallow-ignore-next-line complexity\nexport function parseGcsUri(uri: string): GcsLocation {\n if (!uri.startsWith(\"gs://\")) {\n throw new Error(`[gcsTransport] expected gs:// URI, got: ${JSON.stringify(uri)}`);\n }\n const rest = uri.slice(\"gs://\".length);\n const slash = rest.indexOf(\"/\");\n if (slash === -1) {\n throw new Error(`[gcsTransport] missing key in gs URI: ${JSON.stringify(uri)}`);\n }\n const bucket = rest.slice(0, slash);\n const key = rest.slice(slash + 1);\n if (!bucket || !key) {\n throw new Error(`[gcsTransport] empty bucket or key in gs URI: ${JSON.stringify(uri)}`);\n }\n return { bucket, key };\n}\n\n/** Build `gs://bucket/key` from a location. */\nexport function formatGcsUri(loc: GcsLocation): string {\n return `gs://${loc.bucket}/${loc.key}`;\n}\n\n/** Stream a GCS object to a local file path. */\nexport async function downloadGcsObjectToFile(\n storage: Storage,\n uri: string,\n destPath: string,\n): Promise<void> {\n const { bucket, key } = parseGcsUri(uri);\n mkdirSync(dirname(destPath), { recursive: true });\n const file = storage.bucket(bucket).file(key);\n // `createReadStream` streams the object body; piping into a write stream\n // keeps memory flat for large plan tarballs / chunk files rather than\n // buffering the whole object the way `file.download()` would.\n await pipeline(file.createReadStream(), createWriteStream(destPath));\n}\n\n/** Download and verify an immutable plan-v2 artifact before materialization. */\nexport async function downloadGcsObjectToFileVerified(\n storage: Storage,\n uri: string,\n destPath: string,\n expectedSha256: string,\n): Promise<void> {\n assertSha256(expectedSha256);\n await downloadGcsObjectToFile(storage, uri, destPath);\n const actual = await sha256File(destPath);\n if (actual !== expectedSha256) {\n rmSync(destPath, { force: true });\n const error = new Error(\n `[gcsTransport] PLAN_ARTIFACT_DIGEST_MISMATCH: ${uri} expected ${expectedSha256}, got ${actual}`,\n );\n error.name = \"PLAN_ARTIFACT_DIGEST_MISMATCH\";\n throw error;\n }\n}\n\n/**\n * Upload a local file's contents to a GCS URI using a resumable upload.\n * GCS objects have no practical size ceiling for the artifacts this adapter\n * handles (plan tarballs \u2264 2 GB, chunks \u2264 a few hundred MB), so a single\n * upload call works for every case.\n */\nexport async function uploadFileToGcs(\n storage: Storage,\n localPath: string,\n uri: string,\n contentType?: string,\n): Promise<void> {\n if (!existsSync(localPath)) {\n throw new Error(`[gcsTransport] upload source missing: ${localPath}`);\n }\n const { bucket, key } = parseGcsUri(uri);\n await storage.bucket(bucket).upload(localPath, {\n destination: key,\n // `resumable: false` (simple upload) is faster for the small-to-medium\n // objects this adapter moves and avoids the extra round-trip a resumable\n // session start costs; GCS recommends resumable only past ~8 MB but our\n // chunks are reliably above that, so let the client pick by default.\n contentType,\n });\n}\n\n/**\n * Upload one content-addressed plan-v2 artifact exactly once.\n *\n * The zero-generation precondition makes creation atomic. Existing objects\n * are reused only when their immutable digest metadata and byte length agree;\n * a conflict is never overwritten because another render may already consume\n * that object.\n */\nexport async function uploadContentAddressedFileToGcs(\n storage: Storage,\n localPath: string,\n uri: string,\n expectedSha256: string,\n contentType?: string,\n): Promise<\"uploaded\" | \"reused\"> {\n assertSha256(expectedSha256);\n if (!existsSync(localPath)) {\n throw new Error(`[gcsTransport] upload source missing: ${localPath}`);\n }\n const actualSha256 = await sha256File(localPath);\n if (actualSha256 !== expectedSha256) {\n throwDigestMismatch(\n `local artifact ${localPath} expected ${expectedSha256}, got ${actualSha256}`,\n );\n }\n\n const { bucket, key } = parseGcsUri(uri);\n const bucketHandle = storage.bucket(bucket);\n const file = bucketHandle.file(key);\n const size = statSync(localPath).size;\n if (await isReusableContentAddressedObject(file, uri, size, expectedSha256)) {\n return \"reused\";\n }\n\n try {\n await bucketHandle.upload(localPath, {\n destination: key,\n contentType,\n metadata: { metadata: { sha256: expectedSha256 } },\n preconditionOpts: { ifGenerationMatch: 0 },\n });\n return \"uploaded\";\n } catch (error) {\n // A concurrent planner may win the create-only race. Reuse only after\n // verifying that the winning object is exactly the immutable CAS value.\n if (\n isGcsPreconditionFailed(error) &&\n (await isReusableContentAddressedObject(file, uri, size, expectedSha256))\n ) {\n return \"reused\";\n }\n throw error;\n }\n}\n\ninterface GcsFileLike {\n exists(): Promise<[boolean, ...unknown[]]>;\n getMetadata(): Promise<\n [\n {\n size?: string | number;\n metadata?: Record<string, string | number | boolean | null>;\n },\n ...unknown[],\n ]\n >;\n}\n\nasync function isReusableContentAddressedObject(\n file: GcsFileLike,\n uri: string,\n expectedSize: number,\n expectedSha256: string,\n): Promise<boolean> {\n const [exists] = await file.exists();\n if (!exists) return false;\n const [metadata] = await file.getMetadata();\n if (Number(metadata.size) === expectedSize && metadata.metadata?.sha256 === expectedSha256) {\n return true;\n }\n throwDigestMismatch(\n `immutable object ${uri} already exists with different digest metadata or size`,\n );\n}\n\nexport async function sha256File(path: string): Promise<string> {\n const hash = createHash(\"sha256\");\n for await (const chunk of createReadStream(path)) {\n hash.update(chunk as Buffer);\n }\n return hash.digest(\"hex\");\n}\n\nfunction assertSha256(value: string): void {\n if (!/^[a-f0-9]{64}$/.test(value)) {\n throw new Error(\n `[gcsTransport] expected lowercase SHA-256 digest, got ${JSON.stringify(value)}`,\n );\n }\n}\n\nfunction throwDigestMismatch(detail: string): never {\n const error = new Error(`[gcsTransport] PLAN_ARTIFACT_DIGEST_MISMATCH: ${detail}`);\n error.name = \"PLAN_ARTIFACT_DIGEST_MISMATCH\";\n throw error;\n}\n\nfunction isGcsPreconditionFailed(error: unknown): boolean {\n if (!error || typeof error !== \"object\") return false;\n return (error as { code?: unknown }).code === 412;\n}\n\n/**\n * Pack a directory into a `.tar.gz` at `destTarball`. Uses the `tar` npm\n * package (pure JS over `node:zlib`) rather than spawning a system tar\n * binary so the archive format is independent of the container's userland.\n */\nexport async function tarDirectory(sourceDir: string, destTarball: string): Promise<void> {\n if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) {\n throw new Error(`[gcsTransport] tar source must be an existing directory: ${sourceDir}`);\n }\n mkdirSync(dirname(destTarball), { recursive: true });\n await tar.create({ gzip: true, file: destTarball, cwd: sourceDir }, [\".\"]);\n}\n\n/**\n * Extract a `.tar.gz` produced by {@link tarDirectory} into `destDir`.\n * The directory is created (or cleared) before extraction so a retried\n * request doesn't observe stale files from a prior run on the same warm\n * container instance.\n */\nexport async function untarDirectory(tarballPath: string, destDir: string): Promise<void> {\n if (!existsSync(tarballPath)) {\n throw new Error(`[gcsTransport] tarball missing: ${tarballPath}`);\n }\n // Wipe target so a warm container instance's prior planDir doesn't bleed\n // into the new request. Cloud Run re-uses the instance filesystem across\n // requests served by the same instance.\n if (existsSync(destDir)) {\n rmSync(destDir, { recursive: true, force: true });\n }\n mkdirSync(destDir, { recursive: true });\n await tar.extract({ file: tarballPath, cwd: destDir });\n}\n"],
5
+ "mappings": ";AAkBA,SAAS,cAAAA,aAAY,aAAAC,YAAW,aAAa,cAAc,UAAAC,SAAQ,YAAAC,iBAAgB;AACnF,SAAS,cAAc;AACvB,SAAS,UAAU,SAAS,YAAY;AACxC,SAAS,qBAAqB;AAC9B,SAAS,aAAa;AACtB,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAKA;AAAA,EACA;AAAA,OACK;;;AChBP,SAAS,kBAAkB;AAMpB,IAAM,+BAAN,cAA2C,MAAM;AAAA;AAAA;AAAA,EAGpC,OAAO;AAAA,EAChB;AAAA,EACT,YAAY,cAA6B,MAAc;AACrD,UAAM,yCAAyC,IAAI,EAAE;AACrD,SAAK,eAAe;AAAA,EACtB;AACF;AAOA,IAAM,wBAAwB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQO,SAAS,8BAAsC;AACpD,QAAM,qBAAqB,QAAQ,IAAI,8BAA8B,KAAK;AAC1E,MAAI,oBAAoB;AACtB,QAAI,CAAC,WAAW,kBAAkB,GAAG;AACnC,YAAM,IAAI;AAAA,QACR;AAAA,QACA,gCAAgC,KAAK,UAAU,kBAAkB,CAAC;AAAA,MACpE;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,QAAQ,IAAI,yBAAyB,KAAK;AAC5D,MAAI,WAAW;AACb,QAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,QACA,2BAA2B,KAAK,UAAU,SAAS,CAAC;AAAA,MACtD;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,aAAW,aAAa,uBAAuB;AAC7C,QAAI,WAAW,SAAS,EAAG,QAAO;AAAA,EACpC;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,IACA,oLAEe,sBAAsB,KAAK,IAAI,CAAC;AAAA,EACjD;AACF;;;AC7EA,IAAM,oBAAuD;AAAA,EAC3D,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,gBAAgB;AAClB;AAEO,SAAS,gBAAgB,QAAmC;AACjE,SAAO,kBAAkB,MAAM;AACjC;;;ACLA;AAAA,EACE;AAAA,EACA;AAAA,EACA,cAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,YAAY,SAAS;AAUd,SAAS,YAAY,KAA0B;AACpD,MAAI,CAAC,IAAI,WAAW,OAAO,GAAG;AAC5B,UAAM,IAAI,MAAM,2CAA2C,KAAK,UAAU,GAAG,CAAC,EAAE;AAAA,EAClF;AACA,QAAM,OAAO,IAAI,MAAM,QAAQ,MAAM;AACrC,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,UAAU,IAAI;AAChB,UAAM,IAAI,MAAM,yCAAyC,KAAK,UAAU,GAAG,CAAC,EAAE;AAAA,EAChF;AACA,QAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,QAAM,MAAM,KAAK,MAAM,QAAQ,CAAC;AAChC,MAAI,CAAC,UAAU,CAAC,KAAK;AACnB,UAAM,IAAI,MAAM,iDAAiD,KAAK,UAAU,GAAG,CAAC,EAAE;AAAA,EACxF;AACA,SAAO,EAAE,QAAQ,IAAI;AACvB;AAQA,eAAsB,wBACpB,SACA,KACA,UACe;AACf,QAAM,EAAE,QAAQ,IAAI,IAAI,YAAY,GAAG;AACvC,YAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,QAAM,OAAO,QAAQ,OAAO,MAAM,EAAE,KAAK,GAAG;AAI5C,QAAM,SAAS,KAAK,iBAAiB,GAAG,kBAAkB,QAAQ,CAAC;AACrE;AAGA,eAAsB,gCACpB,SACA,KACA,UACA,gBACe;AACf,eAAa,cAAc;AAC3B,QAAM,wBAAwB,SAAS,KAAK,QAAQ;AACpD,QAAM,SAAS,MAAM,WAAW,QAAQ;AACxC,MAAI,WAAW,gBAAgB;AAC7B,WAAO,UAAU,EAAE,OAAO,KAAK,CAAC;AAChC,UAAM,QAAQ,IAAI;AAAA,MAChB,iDAAiD,GAAG,aAAa,cAAc,SAAS,MAAM;AAAA,IAChG;AACA,UAAM,OAAO;AACb,UAAM;AAAA,EACR;AACF;AAQA,eAAsB,gBACpB,SACA,WACA,KACA,aACe;AACf,MAAI,CAACC,YAAW,SAAS,GAAG;AAC1B,UAAM,IAAI,MAAM,yCAAyC,SAAS,EAAE;AAAA,EACtE;AACA,QAAM,EAAE,QAAQ,IAAI,IAAI,YAAY,GAAG;AACvC,QAAM,QAAQ,OAAO,MAAM,EAAE,OAAO,WAAW;AAAA,IAC7C,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,IAKb;AAAA,EACF,CAAC;AACH;AAUA,eAAsB,gCACpB,SACA,WACA,KACA,gBACA,aACgC;AAChC,eAAa,cAAc;AAC3B,MAAI,CAACA,YAAW,SAAS,GAAG;AAC1B,UAAM,IAAI,MAAM,yCAAyC,SAAS,EAAE;AAAA,EACtE;AACA,QAAM,eAAe,MAAM,WAAW,SAAS;AAC/C,MAAI,iBAAiB,gBAAgB;AACnC;AAAA,MACE,kBAAkB,SAAS,aAAa,cAAc,SAAS,YAAY;AAAA,IAC7E;AAAA,EACF;AAEA,QAAM,EAAE,QAAQ,IAAI,IAAI,YAAY,GAAG;AACvC,QAAM,eAAe,QAAQ,OAAO,MAAM;AAC1C,QAAM,OAAO,aAAa,KAAK,GAAG;AAClC,QAAM,OAAO,SAAS,SAAS,EAAE;AACjC,MAAI,MAAM,iCAAiC,MAAM,KAAK,MAAM,cAAc,GAAG;AAC3E,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,aAAa,OAAO,WAAW;AAAA,MACnC,aAAa;AAAA,MACb;AAAA,MACA,UAAU,EAAE,UAAU,EAAE,QAAQ,eAAe,EAAE;AAAA,MACjD,kBAAkB,EAAE,mBAAmB,EAAE;AAAA,IAC3C,CAAC;AACD,WAAO;AAAA,EACT,SAAS,OAAO;AAGd,QACE,wBAAwB,KAAK,KAC5B,MAAM,iCAAiC,MAAM,KAAK,MAAM,cAAc,GACvE;AACA,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACF;AAeA,eAAe,iCACb,MACA,KACA,cACA,gBACkB;AAClB,QAAM,CAAC,MAAM,IAAI,MAAM,KAAK,OAAO;AACnC,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,CAAC,QAAQ,IAAI,MAAM,KAAK,YAAY;AAC1C,MAAI,OAAO,SAAS,IAAI,MAAM,gBAAgB,SAAS,UAAU,WAAW,gBAAgB;AAC1F,WAAO;AAAA,EACT;AACA;AAAA,IACE,oBAAoB,GAAG;AAAA,EACzB;AACF;AAEA,eAAsB,WAAW,MAA+B;AAC9D,QAAM,OAAO,WAAW,QAAQ;AAChC,mBAAiB,SAAS,iBAAiB,IAAI,GAAG;AAChD,SAAK,OAAO,KAAe;AAAA,EAC7B;AACA,SAAO,KAAK,OAAO,KAAK;AAC1B;AAEA,SAAS,aAAa,OAAqB;AACzC,MAAI,CAAC,iBAAiB,KAAK,KAAK,GAAG;AACjC,UAAM,IAAI;AAAA,MACR,yDAAyD,KAAK,UAAU,KAAK,CAAC;AAAA,IAChF;AAAA,EACF;AACF;AAEA,SAAS,oBAAoB,QAAuB;AAClD,QAAM,QAAQ,IAAI,MAAM,iDAAiD,MAAM,EAAE;AACjF,QAAM,OAAO;AACb,QAAM;AACR;AAEA,SAAS,wBAAwB,OAAyB;AACxD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,SAAQ,MAA6B,SAAS;AAChD;AAOA,eAAsB,aAAa,WAAmB,aAAoC;AACxF,MAAI,CAACA,YAAW,SAAS,KAAK,CAAC,SAAS,SAAS,EAAE,YAAY,GAAG;AAChE,UAAM,IAAI,MAAM,4DAA4D,SAAS,EAAE;AAAA,EACzF;AACA,YAAU,QAAQ,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;AACnD,QAAU,WAAO,EAAE,MAAM,MAAM,MAAM,aAAa,KAAK,UAAU,GAAG,CAAC,GAAG,CAAC;AAC3E;AAQA,eAAsB,eAAe,aAAqB,SAAgC;AACxF,MAAI,CAACA,YAAW,WAAW,GAAG;AAC5B,UAAM,IAAI,MAAM,mCAAmC,WAAW,EAAE;AAAA,EAClE;AAIA,MAAIA,YAAW,OAAO,GAAG;AACvB,WAAO,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAClD;AACA,YAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACtC,QAAU,YAAQ,EAAE,MAAM,aAAa,KAAK,QAAQ,CAAC;AACvD;;;AHvMA,IAAI,gBAAgC;AACpC,SAAS,aAAsB;AAC7B,MAAI,cAAe,QAAO;AAC1B,kBAAgB,IAAI,QAAQ;AAC5B,SAAO;AACT;AA2BA,eAAsB,SAAS,OAAsB,MAA6C;AAChG,QAAM,YAAY,YAAY,KAAK;AACnC,4BAA0B,SAAS;AACnC,uBAAqB,SAAS;AAC9B,WAAS,EAAE,OAAO,iBAAiB,QAAQ,UAAU,QAAQ,OAAO,eAAe,SAAS,EAAE,CAAC;AAC/F,MAAI;AACF,YAAQ,UAAU,QAAQ;AAAA,MACxB,KAAK;AACH,eAAO,MAAM,WAAW,WAAW,IAAI;AAAA,MACzC,KAAK;AACH,eAAO,MAAM,kBAAkB,WAAW,IAAI;AAAA,MAChD,KAAK;AACH,eAAO,MAAM,eAAe,WAAW,IAAI;AAAA,MAC7C,SAAS;AAGP,cAAM,cAAqB;AAC3B,cAAM,IAAI;AAAA,UACR,6BAA6B,KAAK;AAAA,YAC/B,YAAoC;AAAA,UACvC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,+BAA2B,GAAG;AAC9B,aAAS;AAAA,MACP,OAAO;AAAA,MACP,QAAQ,UAAU;AAAA,MAClB,OAAO,eAAe,SAAS;AAAA,MAC/B,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MACxD,MAAM,eAAe,QAAQ,IAAI,OAAO;AAAA,IAC1C,CAAC;AACD,UAAM;AAAA,EACR;AACF;AAKA,SAAS,0BAA0B,OAA2D;AAC5F,QAAM,MAAM;AACZ,QAAM,WAAW,IAAI;AACrB,MAAI,aAAa,UAAa,aAAa,QAAQ,aAAa,MAAM;AACpE,UAAM,QAAQ,IAAI;AAAA,MAChB,sCAAsC,KAAK,UAAU,QAAQ,CAAC;AAAA,IAChE;AACA,UAAM,OAAO;AACb,UAAM;AAAA,EACR;AACA,MAAI,MAAM,WAAW,OAAQ;AAE7B,QAAM,eAAe,OAAO,IAAI,eAAe;AAC/C,QAAM,gBAAgB,OAAO,IAAI,yBAAyB;AAC1D,QAAM,cAAc,OAAO,IAAI,4BAA4B;AAC3D,QAAM,QACJ,aAAa,OACT,CAAC,gBAAgB,iBAAiB,cAClC,gBAAgB,CAAC,iBAAiB,CAAC;AACzC,MAAI,CAAC,OAAO;AACV,UAAM,QAAQ,IAAI;AAAA,MAChB,aAAa,aAAa,OAAO,OAAO,IAAI,IAAI,MAAM,MAAM;AAAA,IAC9D;AACA,UAAM,OAAO;AACb,UAAM;AAAA,EACR;AACA,MAAI,aAAa,QAAQ,MAAM,WAAW,cAAc,MAAM,gBAAgB,MAAM;AAClF,UAAM,QAAQ,IAAI,MAAM,oEAAoE;AAC5F,UAAM,OAAO;AACb,UAAM;AAAA,EACR;AACF;AAKA,SAAS,2BAA2B,OAAsB;AACxD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,QAAM,YAAY;AAClB,MACE,UAAU,SAAS,+BACnB,UAAU,SAAS,oBACnB,UAAU,SAAS,mCACnB;AACA,cAAU,OAAO,UAAU;AAAA,EAC7B;AACF;AAIA,IAAM,qBAAqB;AAGpB,SAAS,YAAY,OAAoE;AAC9F,MAAI,SAAwB;AAC5B,WAAS,IAAI,GAAG,IAAI,oBAAoB,KAAK;AAC3C,QAAI,UAAU,OAAO,WAAW,UAAU;AACxC,YAAM,MAAM;AACZ,UAAI,OAAO,IAAI,WAAW,YAAY,iBAAiB,IAAI,MAAM,GAAG;AAClE,eAAO;AAAA,MACT;AACA,UAAI,aAAa,KAAK;AACpB,iBAAS,IAAI;AACb;AAAA,MACF;AACA,UAAI,WAAW,KAAK;AAClB,iBAAS,IAAI;AACb;AAAA,MACF;AAAA,IACF;AACA;AAAA,EACF;AACA,QAAM,IAAI;AAAA,IACR,sDAAsD,kBAAkB;AAAA,EAC1E;AACF;AAEA,SAAS,iBAAiB,OAAwC;AAChE,SAAO,UAAU,UAAU,UAAU,iBAAiB,UAAU;AAClE;AAQA,SAAS,SAAS,SAAwC;AACxD,UAAQ,IAAI,KAAK,UAAU,OAAO,CAAC;AACrC;AASA,SAAS,eACP,OACyB;AACzB,UAAQ,MAAM,QAAQ;AAAA,IACpB,KAAK;AACH,aAAO;AAAA,QACL,eAAe,MAAM;AAAA,QACrB,qBAAqB,MAAM;AAAA,QAC3B,cAAc,MAAM,gBAAgB;AAAA,QACpC,QAAQ,MAAM,OAAO;AAAA,QACrB,KAAK,MAAM,OAAO;AAAA,MACpB;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,cAAc,MAAM,gBAAgB;AAAA,QACpC,GAAI,MAAM,iBAAiB,OACvB,EAAE,sBAAsB,MAAM,qBAAqB,IACnD,EAAE,YAAY,MAAM,WAAW;AAAA,QACnC,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,MAChB;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,cAAc,MAAM,gBAAgB;AAAA,QACpC,GAAI,MAAM,iBAAiB,OACvB,EAAE,sBAAsB,MAAM,qBAAqB,IACnD,EAAE,YAAY,MAAM,WAAW;AAAA,QACnC,YAAY,MAAM,aAAa;AAAA,QAC/B,UAAU,MAAM,gBAAgB;AAAA,QAChC,cAAc,MAAM;AAAA,QACpB,QAAQ,MAAM;AAAA,MAChB;AAAA,EACJ;AACF;AAQA,SAAS,YAAY,MAA0B;AAC7C,MAAI,MAAM,qBAAsB;AAChC,MAAI,QAAQ,IAAI,6BAA8B;AAC9C,UAAQ,IAAI,+BAA+B,4BAA4B;AACzE;AAKA,eAAe,WAAW,OAAkB,MAA6C;AACvF,MAAI,MAAM,iBAAiB,MAAM;AAC/B,WAAO,aAAa,OAAO,IAAI;AAAA,EACjC;AACA,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,UAAU,MAAM,WAAW,WAAW;AAC5C,QAAM,YAAY,MAAM,YAAY,QAAQ;AAK5C,cAAY,IAAI;AAEhB,QAAM,OAAO,YAAY,KAAK,MAAM,WAAW,OAAO,GAAG,aAAa,CAAC;AACvE,QAAM,iBAAiB,KAAK,MAAM,gBAAgB;AAClD,QAAM,aAAa,KAAK,MAAM,SAAS;AACvC,QAAM,UAAU,KAAK,MAAM,MAAM;AAEjC,MAAI;AACF,UAAM,wBAAwB,SAAS,MAAM,eAAe,cAAc;AAC1E,UAAM,eAAe,gBAAgB,UAAU;AAE/C,UAAM,SAAkC;AAAA,MACtC,GAAG,MAAM;AAAA,IACX;AACA,UAAM,SAAqB,MAAM,UAAU,YAAY,QAAQ,OAAO;AAUtE,UAAM,UAAU,KAAK,MAAM,aAAa;AACxC,UAAM,aAAa,SAAS,OAAO;AACnC,UAAM,aAAa,GAAG,kBAAkB,MAAM,mBAAmB,CAAC;AAClE,UAAM,YAAY,KAAK,SAAS,WAAW;AAC3C,UAAM,WAAWC,YAAW,SAAS,KAAKC,UAAS,SAAS,EAAE,OAAO;AACrE,UAAM,gBAAgB,SAAS,SAAS,YAAY,kBAAkB;AAEtE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,UAAU,OAAO;AAAA,MACjB,YAAY,OAAO;AAAA,MACnB,aAAa,OAAO;AAAA,MACpB,KAAK,OAAO;AAAA,MACZ,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO;AAAA,MACf,UAAU;AAAA,MACV,aAAa;AAAA,MACb,eAAe,OAAO;AAAA,MACtB,iBAAiB,OAAO;AAAA,MACxB,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AAAA,EACF,UAAE;AACA,eAAW,IAAI;AAAA,EACjB;AACF;AAOA,eAAe,aACb,OACA,MAC0D;AAC1D,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,UAAU,MAAM,WAAW,WAAW;AAC5C,QAAM,YAAY,MAAM,YAAY,UAAU;AAC9C,cAAY,IAAI;AAEhB,QAAM,OAAO,YAAY,KAAK,MAAM,WAAW,OAAO,GAAG,gBAAgB,CAAC;AAC1E,QAAM,iBAAiB,KAAK,MAAM,gBAAgB;AAClD,QAAM,aAAa,KAAK,MAAM,SAAS;AACvC,QAAM,YAAY,KAAK,MAAM,SAAS;AACtC,MAAI;AACF,UAAM,wBAAwB,SAAS,MAAM,eAAe,cAAc;AAC1E,UAAM,eAAe,gBAAgB,UAAU;AAC/C,UAAM,SAAuB,MAAM,UAAU,YAAY,EAAE,GAAG,MAAM,OAAO,GAAG,SAAS;AACvF,UAAM,WAAW,mBAAmB,SAAS;AAC7C,QAAI,SAAS,aAAa,OAAO,UAAU;AACzC,4BAAsB,OAAO,UAAU,SAAS,QAAQ;AAAA,IAC1D;AAEA,UAAM,eAAe,GAAG,kBAAkB,MAAM,mBAAmB,CAAC;AACpE,UAAM,iBAAiB,GAAG,YAAY;AACtC,UAAM,kBAAkB;AAAA,MACtB,GAAG,IAAI,IAAI,SAAS,UAAU,IAAI,CAAC,aAAa,CAAC,SAAS,QAAQ,QAAQ,CAAC,CAAC,EAAE,OAAO;AAAA,IACvF;AACA,UAAM,cAAc,iBAAiB,IAAI,OAAO,aAAa;AAC3D,YAAM;AAAA,QACJ;AAAA,QACA,eAAe,WAAW,SAAS,MAAM;AAAA,QACzC,cAAc,gBAAgB,SAAS,MAAM;AAAA,QAC7C,SAAS;AAAA,MACX;AAAA,IACF,CAAC;AAED,UAAM,cAAc,GAAG,YAAY;AACnC,UAAM;AAAA,MACJ;AAAA,MACA,OAAO;AAAA,MACP;AAAA,MACA,MAAM,WAAW,OAAO,YAAY;AAAA,MACpC;AAAA,IACF;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,sBAAsB;AAAA,MACtB,yBAAyB;AAAA,MACzB,UAAU,OAAO;AAAA,MACjB,YAAY,OAAO;AAAA,MACnB,aAAa,OAAO;AAAA,MACpB,KAAK,OAAO;AAAA,MACZ,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO;AAAA,MACf,UAAU,SAAS,UAAU,KAAK,CAAC,aAAa,SAAS,SAAS,WAAW;AAAA,MAC7E,aAAa;AAAA,MACb,eAAe,OAAO;AAAA,MACtB,iBAAiB,OAAO;AAAA,MACxB,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AAAA,EACF,UAAE;AACA,eAAW,IAAI;AAAA,EACjB;AACF;AAKA,eAAe,kBACb,OACA,MACgC;AAChC,MAAI,MAAM,iBAAiB,MAAM;AAC/B,WAAO,oBAAoB,OAAO,IAAI;AAAA,EACxC;AACA,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,UAAU,MAAM,WAAW,WAAW;AAC5C,QAAM,YAAY,MAAM,YAAY,eAAe;AAEnD,cAAY,IAAI;AAEhB,QAAM,OAAO,YAAY,KAAK,MAAM,WAAW,OAAO,GAAG,cAAc,CAAC;AACxE,QAAM,UAAU,KAAK,MAAM,aAAa;AACxC,QAAM,UAAU,KAAK,MAAM,MAAM;AAEjC,MAAI;AACF,UAAM,wBAAwB,SAAS,MAAM,YAAY,OAAO;AAChE,UAAM,eAAe,SAAS,OAAO;AAOrC,mBAAe,SAAS,MAAM,QAAQ;AAEtC,UAAM,kBAAkB;AAAA,MACtB;AAAA,MACA,MAAM,WAAW,iBACb,SAAS,IAAI,MAAM,UAAU,CAAC,KAC9B,SAAS,IAAI,MAAM,UAAU,CAAC,GAAG,gBAAgB,MAAM,MAAM,CAAC;AAAA,IACpE;AAEA,UAAM,SAAsB,MAAM,UAAU,SAAS,MAAM,YAAY,eAAe;AAEtF,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,YAAY,MAAM;AAAA,MAClB,QAAQ,OAAO;AAAA,MACf,eAAe,OAAO;AAAA,MACtB,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AAAA,EACF,UAAE;AACA,eAAW,IAAI;AAAA,EACjB;AACF;AAIA,eAAe,oBACb,OACA,MACgC;AAChC,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,UAAU,MAAM,WAAW,WAAW;AAC5C,QAAM,YAAY,MAAM,YAAY,eAAe;AACnD,cAAY,IAAI;AAEhB,QAAM,OAAO,YAAY,KAAK,MAAM,WAAW,OAAO,GAAG,iBAAiB,CAAC;AAC3E,MAAI;AACF,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA;AAAA,MACA,EAAE,MAAM,SAAS,YAAY,MAAM,WAAW;AAAA,MAC9C;AAAA,IACF;AACA,UAAM,kBAAkB;AAAA,MACtB;AAAA,MACA,MAAM,WAAW,iBACb,SAAS,IAAI,MAAM,UAAU,CAAC,KAC9B,SAAS,IAAI,MAAM,UAAU,CAAC,GAAG,gBAAgB,MAAM,MAAM,CAAC;AAAA,IACpE;AACA,UAAM,SAAS,MAAM,UAAU,SAAS,MAAM,YAAY,eAAe;AACzE,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AACA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,YAAY,MAAM;AAAA,MAClB,QAAQ,OAAO;AAAA,MACf,eAAe,OAAO;AAAA,MACtB,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AAAA,EACF,UAAE;AACA,eAAW,IAAI;AAAA,EACjB;AACF;AAEA,eAAe,kBACb,SACA,QACA,QACA,YACiB;AACjB,QAAM,UAAU,kBAAkB,MAAM;AACxC,MAAI,OAAO,eAAe,QAAQ;AAChC,UAAM,MAAM,QAAQ,OAAO,UAAU;AACrC,UAAMC,OAAM,GAAG,OAAO,WAAW,IAAI,UAAU,CAAC,GAAG,GAAG;AACtD,UAAM,gBAAgB,SAAS,OAAO,YAAYA,IAAG;AACrD,WAAOA;AAAA,EACT;AAIA,QAAM,UAAU,GAAG,OAAO,UAAU;AACpC,QAAM,aAAa,OAAO,YAAY,OAAO;AAC7C,QAAM,MAAM,GAAG,OAAO,WAAW,IAAI,UAAU,CAAC;AAChD,QAAM,gBAAgB,SAAS,SAAS,KAAK,kBAAkB;AAC/D,SAAO;AACT;AAKA,eAAe,eACb,OACA,MAC6B;AAC7B,MAAI,MAAM,iBAAiB,MAAM;AAC/B,WAAO,iBAAiB,OAAO,IAAI;AAAA,EACrC;AACA,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,UAAU,MAAM,WAAW,WAAW;AAC5C,QAAM,YAAY,MAAM,YAAY,YAAY;AAEhD,QAAM,OAAO,YAAY,KAAK,MAAM,WAAW,OAAO,GAAG,iBAAiB,CAAC;AAC3E,QAAM,UAAU,KAAK,MAAM,aAAa;AACxC,QAAM,UAAU,KAAK,MAAM,MAAM;AAEjC,MAAI;AACF,UAAM,wBAAwB,SAAS,MAAM,YAAY,OAAO;AAChE,UAAM,eAAe,SAAS,OAAO;AAErC,UAAM,aAAa,MAAM,qBAAqB,SAAS,MAAM,cAAc,MAAM,MAAM,MAAM;AAM7F,QAAI,YAA2B;AAC/B,UAAM,YAAY,KAAK,SAAS,WAAW;AAC3C,QAAIF,YAAW,SAAS,KAAKC,UAAS,SAAS,EAAE,OAAO,GAAG;AACzD,kBAAY;AAAA,IACd,WAAW,MAAM,aAAa;AAC5B,kBAAY;AACZ,YAAM,wBAAwB,SAAS,MAAM,aAAa,SAAS;AAAA,IACrE;AAEA,UAAM,cACJ,MAAM,WAAW,iBACb,KAAK,MAAM,eAAe,IAC1B,KAAK,MAAM,SAAS,gBAAgB,MAAM,MAAM,CAAC,EAAE;AAEzD,UAAM,SAAyB,MAAM,UAAU,SAAS,YAAY,WAAW,aAAa;AAAA,MAC1F,KAAK,MAAM,QAAQ;AAAA,IACrB,CAAC;AAED,QAAI,MAAM,WAAW,gBAAgB;AACnC,YAAM,UAAU,GAAG,WAAW;AAC9B,YAAM,aAAa,aAAa,OAAO;AACvC,YAAM,gBAAgB,SAAS,SAAS,MAAM,cAAc,kBAAkB;AAAA,IAChF,OAAO;AACL,YAAM,gBAAgB,SAAS,aAAa,MAAM,YAAY;AAAA,IAChE;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,cAAc,MAAM;AAAA,MACpB,eAAe,OAAO;AAAA,MACtB,UAAU,OAAO;AAAA,MACjB,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AAAA,EACF,UAAE;AACA,eAAW,IAAI;AAAA,EACjB;AACF;AAOA,eAAe,iBACb,OACA,MAC6B;AAC7B,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,UAAU,MAAM,WAAW,WAAW;AAC5C,QAAM,YAAY,MAAM,YAAY,YAAY;AAChD,QAAM,OAAO,YAAY,KAAK,MAAM,WAAW,OAAO,GAAG,oBAAoB,CAAC;AAC9E,MAAI;AACF,UAAM,UAAU,MAAM,6BAA6B,SAAS,OAAO,EAAE,MAAM,YAAY,GAAG,IAAI;AAC9F,UAAM,YAAYD,YAAW,KAAK,SAAS,WAAW,CAAC,IAAI,KAAK,SAAS,WAAW,IAAI;AACxF,UAAM,aAAa,MAAM,qBAAqB,SAAS,MAAM,cAAc,MAAM,MAAM,MAAM;AAC7F,UAAM,cACJ,MAAM,WAAW,iBACb,KAAK,MAAM,eAAe,IAC1B,KAAK,MAAM,SAAS,gBAAgB,MAAM,MAAM,CAAC,EAAE;AACzD,UAAM,SAAS,MAAM,UAAU,SAAS,YAAY,WAAW,aAAa;AAAA,MAC1E,KAAK,MAAM,QAAQ;AAAA,IACrB,CAAC;AACD,QAAI,MAAM,WAAW,gBAAgB;AACnC,YAAM,UAAU,GAAG,WAAW;AAC9B,YAAM,aAAa,aAAa,OAAO;AACvC,YAAM,gBAAgB,SAAS,SAAS,MAAM,cAAc,kBAAkB;AAAA,IAChF,OAAO;AACL,YAAM,gBAAgB,SAAS,aAAa,MAAM,YAAY;AAAA,IAChE;AACA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,cAAc,MAAM;AAAA,MACpB,eAAe,OAAO;AAAA,MACtB,UAAU,OAAO;AAAA,MACjB,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AAAA,EACF,UAAE;AACA,eAAW,IAAI;AAAA,EACjB;AACF;AAEA,eAAe,6BACb,SACA,OAKA,QACA,MACiB;AACjB,QAAM,eAAe,KAAK,MAAM,SAAS;AACzC,EAAAG,WAAU,cAAc,EAAE,WAAW,KAAK,CAAC;AAC3C,QAAM;AAAA,IACJ;AAAA,IACA,MAAM;AAAA,IACN,KAAK,cAAc,WAAW;AAAA,EAChC;AACA,QAAM,WAAW,mBAAmB,YAAY;AAChD,MAAI,SAAS,aAAa,MAAM,UAAU;AACxC,0BAAsB,MAAM,UAAU,SAAS,QAAQ;AAAA,EACzD;AACA,QAAM,YAAY,6BAA6B,UAAU,MAAM;AAC/D,QAAM,kBAAkB;AAAA,IACtB,GAAG,IAAI,IAAI,UAAU,IAAI,CAAC,aAAa,CAAC,SAAS,QAAQ,QAAQ,CAAC,CAAC,EAAE,OAAO;AAAA,EAC9E;AACA,QAAM,cAAc,iBAAiB,IAAI,OAAO,aAAa;AAC3D,UAAM,uBAAuB,SAAS,MAAM,yBAAyB,cAAc,QAAQ;AAAA,EAC7F,CAAC;AACD,QAAM,UAAU,KAAK,MAAM,MAAM;AACjC,0BAAwB,cAAc,QAAQ,OAAO;AACrD,SAAO;AACT;AAEA,eAAe,uBACb,SACA,gBACA,WACA,UACe;AACf,QAAM;AAAA,IACJ;AAAA,IACA,cAAc,gBAAgB,SAAS,MAAM;AAAA,IAC7C,eAAe,WAAW,SAAS,MAAM;AAAA,IACzC,SAAS;AAAA,EACX;AACF;AAEA,SAAS,eAAe,WAAmB,QAAwB;AACjE,SAAO,KAAK,WAAW,aAAa,UAAU,OAAO,MAAM,GAAG,CAAC,GAAG,MAAM;AAC1E;AAEA,SAAS,cAAc,QAAgB,QAAwB;AAC7D,SAAO,GAAG,kBAAkB,MAAM,CAAC,IAAI,OAAO,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;AACrE;AAEA,SAAS,sBAAsB,UAAkB,QAAuB;AACtE,QAAM,QAAQ,IAAI;AAAA,IAChB,sCAAsC,QAAQ,uCAAuC,MAAM;AAAA,EAC7F;AACA,QAAM,OAAO;AACb,QAAM;AACR;AAEA,eAAe,cACb,QACA,aACA,IACe;AACf,MAAI,SAAS;AACb,iBAAe,SAAwB;AACrC,WAAO,SAAS,OAAO,QAAQ;AAC7B,YAAM,QAAQ;AACd,YAAM,GAAG,OAAO,KAAK,CAAE;AAAA,IACzB;AAAA,EACF;AACA,QAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,aAAa,OAAO,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,CAAC;AAChG;AAEA,eAAe,qBACb,SACA,MACA,SACA,QACmB;AACnB,QAAM,YAAY,KAAK,SAAS,QAAQ;AACxC,EAAAA,WAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAMxC,QAAM,QAAkB,IAAI,MAAc,KAAK,MAAM;AACrD,QAAM,QAAQ;AAAA,IACZ,KAAK,IAAI,OAAO,KAAK,MAAM;AACzB,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,gCAAgC,CAAC,WAAW;AAAA,MAC9D;AACA,YAAM,EAAE,IAAI,IAAI,YAAY,GAAG;AAC/B,YAAM,YAAY,KAAK,WAAW,SAAS,GAAG,CAAC;AAC/C,YAAM,wBAAwB,SAAS,KAAK,SAAS;AACrD,UAAI,WAAW,gBAAgB;AAC7B,cAAM,UAAU,KAAK,WAAW,UAAU,IAAI,CAAC,CAAC,EAAE;AAClD,cAAM,eAAe,WAAW,OAAO;AACvC,cAAM,CAAC,IAAI;AAAA,MACb,OAAO;AACL,cAAM,CAAC,IAAI;AAAA,MACb;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAOA,SAAS,gBAAgB,OAA+D;AACtF,UAAQ,MAAM,QAAQ;AAAA,IACpB,KAAK;AACH,aAAO,CAAC,MAAM,eAAe,MAAM,mBAAmB;AAAA,IACxD,KAAK;AACH,aAAO,MAAM,iBAAiB,OAC1B,CAAC,MAAM,sBAAsB,MAAM,yBAAyB,MAAM,oBAAoB,IACtF,CAAC,MAAM,YAAY,MAAM,oBAAoB;AAAA,IACnD,KAAK;AACH,aAAO;AAAA,QACL,GAAI,MAAM,iBAAiB,OACvB,CAAC,MAAM,sBAAsB,MAAM,uBAAuB,IAC1D,CAAC,MAAM,UAAU;AAAA,QACrB,GAAG,MAAM;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR,EAAE,OAAO,CAAC,MAAmB,KAAK,IAAI;AAAA,EAC1C;AACF;AAGA,IAAI,0BAA0B;AAe9B,SAAS,qBAAqB,OAA2D;AACvF,QAAM,gBAAgB,QAAQ,IAAI,2BAA2B,KAAK;AAClE,MAAI,kBAAkB,IAAK;AAC3B,MAAI,CAAC,eAAe;AAClB,QAAI,CAAC,yBAAyB;AAC5B,gCAA0B;AAC1B,eAAS;AAAA,QACP,OAAO;AAAA,QACP,OAAO;AAAA,QACP,SACE;AAAA,MAEJ,CAAC;AAAA,IACH;AACA;AAAA,EACF;AAEA,aAAW,OAAO,gBAAgB,KAAK,GAAG;AACxC,UAAM,EAAE,OAAO,IAAI,YAAY,GAAG;AAClC,QAAI,WAAW,eAAe;AAC5B,YAAM,MAAM,IAAI;AAAA,QACd,sCAAsC,KAAK,UAAU,GAAG,CAAC,oBAAoB,MAAM,eAAe,aAAa;AAAA,MACjH;AACA,UAAI,OAAO;AACX,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,SAAS,IAAI,GAAmB;AAC9B,SAAO,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG;AACrC;AAEA,SAAS,kBAAkB,QAAwB;AACjD,SAAO,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,IAAI;AACtD;AAEA,SAAS,WAAW,KAAmB;AACrC,MAAI;AAIF,IAAAC,QAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC9C,QAAQ;AAAA,EAER;AACF;AAWA,SAAS,eAAe,SAAiB,UAAwB;AAC/D,QAAM,eAAe,KAAK,SAAS,WAAW;AAC9C,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;AAAA,EACzD,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,UAAM,QAAQ,IAAI,MAAM,sCAAsC,YAAY,KAAK,GAAG,EAAE;AACpF,UAAM,OAAO;AACb,UAAM;AAAA,EACR;AACA,QAAM,SAAS,OAAO;AACtB,MAAI,OAAO,WAAW,YAAY,WAAW,UAAU;AACrD,UAAM,QAAQ,IAAI;AAAA,MAChB,sCAAsC,QAAQ,qCAAqC,OAAO,MAAM,CAAC;AAAA,IACnG;AACA,UAAM,OAAO;AACb,UAAM;AAAA,EACR;AACF;AAYA,IAAM,4BAA4B,oBAAI,IAAI;AAAA;AAAA,EAExC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAWM,SAAS,UAAU,MAA0B;AAClD,QAAM,MAAM,IAAI,KAAK;AAErB,MAAI,IAAI,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,CAAC,CAAC;AAGnD,MAAI,KAAK,KAAK,OAAO,MAAM;AACzB,QAAI;AACJ,QAAI;AACF,aAAQ,MAAM,EAAE,IAAI,KAAK;AAAA,IAC3B,QAAQ;AACN,aAAO,EAAE,KAAK,EAAE,OAAO,eAAe,SAAS,4BAA4B,GAAG,GAAG;AAAA,IACnF;AACA,QAAI;AACF,YAAM,SAAS,MAAM,SAAS,MAAM,IAAI;AACxC,aAAO,EAAE,KAAK,QAAQ,GAAG;AAAA,IAC3B,SAAS,KAAK;AACZ,YAAM,OAAO,eAAe,QAAQ,IAAI,OAAO;AAC/C,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,YAAM,SAAS,QAAQ,0BAA0B,IAAI,IAAI,IAAI,MAAM;AAGnE,aAAO,EAAE,KAAK,EAAE,OAAO,QAAQ,eAAe,QAAQ,GAAG,MAAM;AAAA,IACjE;AAAA,EACF,CAAC;AAED,SAAO;AACT;AAGO,SAAS,cAAoB;AAClC,QAAM,OAAO,OAAO,QAAQ,IAAI,QAAQ,IAAI;AAC5C,QAAM,MAAM,UAAU;AACtB,QAAM,EAAE,OAAO,IAAI,OAAO,KAAK,GAAG,CAAC,SAAS;AAC1C,aAAS,EAAE,OAAO,oBAAoB,MAAM,KAAK,KAAK,CAAC;AAAA,EACzD,CAAC;AACH;AAIA,IAAI,QAAQ,KAAK,CAAC,KAAK,cAAc,YAAY,GAAG,MAAM,QAAQ,KAAK,CAAC,GAAG;AACzE,cAAY;AACd;",
6
6
  "names": ["existsSync", "mkdirSync", "rmSync", "statSync", "existsSync", "existsSync", "existsSync", "statSync", "uri", "mkdirSync", "rmSync"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/gcp-cloud-run",
3
- "version": "0.7.70",
3
+ "version": "0.7.72",
4
4
  "description": "Google Cloud Run + Workflows adapter for HyperFrames distributed rendering — request handler, client-side SDK, and Terraform module.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,14 +45,15 @@
45
45
  "hono": "^4.6.0",
46
46
  "puppeteer-core": "^25.2.1",
47
47
  "tar": "^7.4.3",
48
- "@hyperframes/producer": "^0.7.70"
48
+ "@hyperframes/producer": "^0.7.72"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^25.0.10",
52
52
  "@types/tar": "^6.1.13",
53
53
  "esbuild": "^0.25.12",
54
54
  "tsx": "^4.21.0",
55
- "typescript": "^5.7.2"
55
+ "typescript": "^5.7.2",
56
+ "yaml": "^2.9.0"
56
57
  },
57
58
  "engines": {
58
59
  "node": ">=22"
package/terraform/main.tf CHANGED
@@ -133,6 +133,11 @@ resource "google_workflows_workflow" "render" {
133
133
  region = var.region
134
134
  service_account = google_service_account.workflow_sa.id
135
135
  source_contents = file(local.workflow_source)
136
+ # Do not publish an executable workflow until its identity has permission to
137
+ # reach Cloud Run. IAM propagation remains eventually consistent, so the
138
+ # workflow also retries the edge's transient 403 response with bounded
139
+ # backoff.
140
+ depends_on = [google_cloud_run_v2_service_iam_member.workflow_invokes_run]
136
141
  # Allow `terraform destroy` to remove the workflow without a manual step;
137
142
  # the definition is reproducible from this module.
138
143
  deletion_protection = false
@@ -3,6 +3,16 @@ output "render_bucket_name" {
3
3
  value = google_storage_bucket.render.name
4
4
  }
5
5
 
6
+ output "project_name" {
7
+ description = "Resource prefix used by this deployment."
8
+ value = var.project_name
9
+ }
10
+
11
+ output "render_service_name" {
12
+ description = "Cloud Run service name."
13
+ value = google_cloud_run_v2_service.render.name
14
+ }
15
+
6
16
  output "service_url" {
7
17
  description = "HTTPS URL of the Cloud Run render service. Pass as renderToCloudRun({ serviceUrl })."
8
18
  value = google_cloud_run_v2_service.render.uri
@@ -0,0 +1,71 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "bun:test";
4
+
5
+ const smokePath = join(import.meta.dir, "../../../examples/gcp-cloud-run/scripts/smoke.sh");
6
+ const smoke = readFileSync(smokePath, "utf-8");
7
+ const dockerfile = readFileSync(join(import.meta.dir, "../Dockerfile"), "utf-8");
8
+
9
+ describe("GCP smoke ownership and protocol safety", () => {
10
+ it("defaults to v1 and requires an explicit v2 protocol argument", () => {
11
+ expect(smoke).toContain('PROTOCOLS="${PROTOCOLS:-v1}"');
12
+ expect(smoke).toContain("--protocols)");
13
+ expect(smoke).toContain("PlanProtocol: $protocol");
14
+ expect(smoke).toContain("decodedFramesEqual");
15
+ expect(smoke).toContain("decodedAudioEqual");
16
+ expect(smoke).toContain("normalizedMetadataEqual");
17
+ });
18
+
19
+ it("derives a length-safe owner prefix and isolates Terraform state", () => {
20
+ expect(smoke).toContain('OWNER_HASH="$(printf');
21
+ expect(smoke).toContain("RUN_NONCE=");
22
+ expect(smoke).toContain('STACK_NAME="hf-smoke-$OWNER_HASH"');
23
+ expect(smoke).toContain('TF_WORK_DIR="$ARTIFACT_DIR/terraform"');
24
+ expect(smoke).toContain('TF_DATA_DIR="$ARTIFACT_DIR/terraform-data"');
25
+ expect(smoke).toContain("export TF_DATA_DIR");
26
+ expect(smoke).toContain('-var "project_name=$STACK_NAME"');
27
+ expect(smoke).toContain('[ "$STACK_NAME" != "hyperframes" ]');
28
+ });
29
+
30
+ it("tracks owned registry resources and verifies stack deletion", () => {
31
+ expect(smoke).toContain("CREATED_IMAGE=0");
32
+ expect(smoke).toContain("CREATED_REPO=0");
33
+ expect(smoke).toContain('if [ "$CREATED_REPO" -eq 1 ]');
34
+ expect(smoke).toContain('if [ "$CREATED_IMAGE" -eq 1 ]');
35
+
36
+ for (const resource of [
37
+ "cloud-run-service",
38
+ "workflow",
39
+ "render-bucket",
40
+ "artifact-image",
41
+ "artifact-repository",
42
+ "cloud-build-staging-bucket",
43
+ ]) {
44
+ expect(smoke).toContain(`verify_absent "${resource}"`);
45
+ }
46
+ expect(smoke).toContain('verify_service_account_absent "run-service-account"');
47
+ expect(smoke).toContain('verify_service_account_absent "workflow-service-account"');
48
+ expect(smoke).toContain('verify_absent "preflight-cloud-run-service"');
49
+ expect(smoke).toContain('verify_absent "preflight-artifact-image"');
50
+ expect(smoke).toContain("$STACK_NAME-render");
51
+ expect(smoke).toContain("--ignore-file");
52
+ expect(smoke).toContain("--gcs-source-staging-dir");
53
+ expect(smoke).toContain("!scripts/package-subpaths.mjs");
54
+ expect(dockerfile).toContain("COPY scripts/package-subpaths.mjs scripts/package-subpaths.mjs");
55
+ expect(smoke).not.toContain("gcloud services enable");
56
+ expect(smoke).toContain("gcloud services list");
57
+ expect(smoke).toContain("--enabled");
58
+ expect(smoke).not.toContain("gcloud services describe");
59
+ expect(smoke).toContain("cannot find");
60
+ expect(smoke).toContain("gcloud iam service-accounts list");
61
+ expect(smoke).toContain('--filter "email:$email"');
62
+ });
63
+
64
+ it("does not swallow Terraform cleanup failures", () => {
65
+ const cleanupStart = smoke.indexOf("cleanup() {");
66
+ const cleanupEnd = smoke.indexOf("\ntrap cleanup EXIT", cleanupStart);
67
+ const cleanup = smoke.slice(cleanupStart, cleanupEnd);
68
+ expect(cleanup).not.toContain("|| true");
69
+ expect(cleanup).toContain("exit 7");
70
+ });
71
+ });
@@ -13,6 +13,15 @@ variable "project_name" {
13
13
  type = string
14
14
  description = "Name prefix applied to the service / workflow / bucket / service accounts."
15
15
  default = "hyperframes"
16
+
17
+ validation {
18
+ condition = (
19
+ length(var.project_name) >= 3 &&
20
+ length(var.project_name) <= 23 &&
21
+ can(regex("^[a-z][a-z0-9-]*[a-z0-9]$", var.project_name))
22
+ )
23
+ error_message = "project_name must be 3-23 lowercase letters, digits, or hyphens, begin with a letter, and end with a letter or digit so derived service-account IDs remain valid."
24
+ }
16
25
  }
17
26
 
18
27
  variable "image" {
@@ -0,0 +1,148 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "bun:test";
4
+ import { parse } from "yaml";
5
+
6
+ type Step = Record<string, unknown>;
7
+
8
+ const source = readFileSync(join(import.meta.dir, "workflow.yaml"), "utf-8");
9
+ // Cloud Workflows expressions are valid to Google's parser but `${...}`
10
+ // inside YAML flow collections is not valid generic YAML. Quote expressions
11
+ // for structural parsing while preserving their text for contract assertions.
12
+ const parseableSource = source.replace(/\$\{([^}]*)\}/g, (_match, expression: string) =>
13
+ JSON.stringify(`\${${expression}}`),
14
+ );
15
+ const workflow = parse(parseableSource) as {
16
+ main: {
17
+ steps: Step[];
18
+ };
19
+ retryable: {
20
+ steps: Step[];
21
+ };
22
+ };
23
+
24
+ function namedStep(name: string, steps = workflow.main.steps): Record<string, unknown> {
25
+ for (const step of steps) {
26
+ if (name in step) return step[name] as Record<string, unknown>;
27
+ }
28
+ throw new Error(`missing workflow step ${name}`);
29
+ }
30
+
31
+ function requestBody(stepName: string): Record<string, unknown> {
32
+ const step = namedStep(stepName);
33
+ const attempt = step.try as {
34
+ args: {
35
+ body: Record<string, unknown>;
36
+ };
37
+ };
38
+ return attempt.args.body;
39
+ }
40
+
41
+ function requestAuth(stepName: string): Record<string, unknown> {
42
+ const step = namedStep(stepName);
43
+ const attempt = step.try as {
44
+ args: {
45
+ auth: Record<string, unknown>;
46
+ };
47
+ };
48
+ return attempt.args.auth;
49
+ }
50
+
51
+ function chunkRequestBody(stepName: string): Record<string, unknown> {
52
+ const renderChunks = namedStep("renderChunks");
53
+ const parallel = renderChunks.parallel as {
54
+ for: {
55
+ steps: Step[];
56
+ };
57
+ };
58
+ const step = namedStep(stepName, parallel.for.steps);
59
+ const attempt = step.try as {
60
+ args: {
61
+ body: Record<string, unknown>;
62
+ };
63
+ };
64
+ return attempt.args.body;
65
+ }
66
+
67
+ describe("Cloud Workflows plan protocol routing", () => {
68
+ it("pins OIDC tokens to the Cloud Run root and retries IAM propagation", () => {
69
+ for (const stepName of ["planV1", "planV2", "assembleV1", "assembleV2"]) {
70
+ expect(requestAuth(stepName)).toEqual({
71
+ type: "OIDC",
72
+ audience: "${serviceUrl}",
73
+ });
74
+ }
75
+ expect(JSON.stringify(workflow.retryable)).toContain("e.code == 403");
76
+ expect(source.match(/max_retries: 6/g)).toHaveLength(2);
77
+ expect(source.match(/max_retries: 4/g)).toHaveLength(4);
78
+ });
79
+
80
+ it("keeps v1 as the default and rejects unknown protocols before plan", () => {
81
+ expect(source).toContain('default(map.get(args, "PlanProtocol"), "v1")');
82
+ expect(namedStep("selectPlanProtocol")).toMatchObject({
83
+ next: "unsupportedPlanProtocol",
84
+ });
85
+ expect(namedStep("unsupportedPlanProtocol")).toMatchObject({
86
+ raise: {
87
+ code: "PLAN_PROTOCOL_UNSUPPORTED",
88
+ },
89
+ });
90
+ });
91
+
92
+ it("uses disjoint v1 and v2 plan request/response contracts", () => {
93
+ expect(requestBody("planV1")).toMatchObject({
94
+ Action: "plan",
95
+ PlanProtocol: "v1",
96
+ });
97
+ expect(requestBody("planV2")).toMatchObject({
98
+ Action: "plan",
99
+ PlanProtocol: "v2",
100
+ });
101
+ const validation = JSON.stringify(namedStep("validatePlanResult"));
102
+ expect(validation).toContain("PlanGcsUri");
103
+ expect(validation).toContain("PlanV2ManifestGcsUri");
104
+ expect(validation).toContain("PlanV2ArtifactGcsPrefix");
105
+ expect(source).toContain('not("PlanGcsUri" in planResult)');
106
+ expect(source).toContain(
107
+ '(not("PlanProtocol" in planResult) or planResult.PlanProtocol == "v1")',
108
+ );
109
+ });
110
+
111
+ it("never mixes v1 and v2 chunk locators", () => {
112
+ const v1 = chunkRequestBody("renderOneChunkV1");
113
+ expect(v1).toMatchObject({
114
+ Action: "renderChunk",
115
+ PlanProtocol: "v1",
116
+ });
117
+ expect(v1).toHaveProperty("PlanGcsUri");
118
+ expect(v1).not.toHaveProperty("PlanV2ManifestGcsUri");
119
+ expect(v1).not.toHaveProperty("PlanV2ArtifactGcsPrefix");
120
+
121
+ const v2 = chunkRequestBody("renderOneChunkV2");
122
+ expect(v2).toMatchObject({
123
+ Action: "renderChunk",
124
+ PlanProtocol: "v2",
125
+ });
126
+ expect(v2).not.toHaveProperty("PlanGcsUri");
127
+ expect(v2).toHaveProperty("PlanV2ManifestGcsUri");
128
+ expect(v2).toHaveProperty("PlanV2ArtifactGcsPrefix");
129
+ expect(v2).toHaveProperty("PlanHash");
130
+ });
131
+
132
+ it("never mixes v1 and v2 assembler locators", () => {
133
+ const v1 = requestBody("assembleV1");
134
+ expect(v1).toHaveProperty("PlanGcsUri");
135
+ expect(v1).not.toHaveProperty("PlanV2ManifestGcsUri");
136
+ expect(v1).not.toHaveProperty("PlanV2ArtifactGcsPrefix");
137
+
138
+ const v2 = requestBody("assembleV2");
139
+ expect(v2).not.toHaveProperty("PlanGcsUri");
140
+ expect(v2).toHaveProperty("PlanV2ManifestGcsUri");
141
+ expect(v2).toHaveProperty("PlanV2ArtifactGcsPrefix");
142
+ expect(v2).toHaveProperty("PlanHash");
143
+ expect(v2).toMatchObject({
144
+ PlanProtocol: "v2",
145
+ AudioGcsUri: null,
146
+ });
147
+ });
148
+ });