diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc
index 54f9fb333..4703fa525 100644
--- a/.fallowrc.jsonc
+++ b/.fallowrc.jsonc
@@ -3,6 +3,7 @@
"entry": [
"packages/producer/src/**/*.test.ts",
"packages/aws-lambda/src/**/*.test.ts",
+ "packages/gcp-cloud-run/src/**/*.test.ts",
"packages/producer/src/regression-harness.ts",
"packages/producer/src/regression-harness-distributed.test.ts",
"packages/producer/src/regression-harness-lambda-local.ts",
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index dc17b8d01..459d8c276 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -131,6 +131,7 @@ jobs:
publish_pkg "@hyperframes/shader-transitions" "@hyperframes/shader-transitions"
publish_pkg "@hyperframes/studio" "@hyperframes/studio"
publish_pkg "@hyperframes/aws-lambda" "@hyperframes/aws-lambda"
+ publish_pkg "@hyperframes/gcp-cloud-run" "@hyperframes/gcp-cloud-run"
# CLI is @hyperframes/cli in the monorepo but published as unscoped "hyperframes" on npm.
# Rewrite the name in package.json before publishing, then use npm publish directly
diff --git a/.gitignore b/.gitignore
index 037b814d3..ba86bfbdf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,6 +75,10 @@ examples/*
!examples/aws-lambda/**
!examples/k8s-jobs
!examples/k8s-jobs/**
+!examples/gcp-cloud-run
+!examples/gcp-cloud-run/**
+# …but never the local smoke run's build/render artifacts.
+examples/gcp-cloud-run/scripts/gcp-smoke-artifacts/
packages/studio/data/
.desloppify/
diff --git a/.prettierignore b/.prettierignore
index d70887435..9fba17dc3 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,3 +2,6 @@ docs/
DOCS_GUIDELINES.md
packages/producer/tests/
*.generated.ts
+
+# Cloud Workflows GCL — uses ${...} expressions that are not standard YAML.
+packages/gcp-cloud-run/terraform/workflow.yaml
diff --git a/Dockerfile.test b/Dockerfile.test
index d6e83f9c2..45b7d89e3 100644
--- a/Dockerfile.test
+++ b/Dockerfile.test
@@ -79,6 +79,7 @@ COPY packages/cli/package.json packages/cli/package.json
COPY packages/studio/package.json packages/studio/package.json
COPY packages/shader-transitions/package.json packages/shader-transitions/package.json
COPY packages/aws-lambda/package.json packages/aws-lambda/package.json
+COPY packages/gcp-cloud-run/package.json packages/gcp-cloud-run/package.json
RUN bun install --frozen-lockfile
# Copy source
diff --git a/bun.lock b/bun.lock
index 67a6dd8e6..add7eec86 100644
--- a/bun.lock
+++ b/bun.lock
@@ -22,7 +22,7 @@
},
"packages/aws-lambda": {
"name": "@hyperframes/aws-lambda",
- "version": "0.6.69",
+ "version": "0.6.79",
"dependencies": {
"@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/client-sfn": "^3.700.0",
@@ -54,7 +54,7 @@
},
"packages/cli": {
"name": "@hyperframes/cli",
- "version": "0.6.69",
+ "version": "0.6.79",
"bin": {
"hyperframes": "./dist/cli.js",
},
@@ -81,6 +81,7 @@
"@hyperframes/aws-lambda": "workspace:*",
"@hyperframes/core": "workspace:*",
"@hyperframes/engine": "workspace:*",
+ "@hyperframes/gcp-cloud-run": "workspace:*",
"@hyperframes/producer": "workspace:*",
"@hyperframes/studio": "workspace:*",
"@types/adm-zip": "^0.5.7",
@@ -100,7 +101,7 @@
},
"packages/core": {
"name": "@hyperframes/core",
- "version": "0.6.69",
+ "version": "0.6.79",
"dependencies": {
"@babel/parser": "^7.27.0",
"@chenglou/pretext": "^0.0.5",
@@ -129,7 +130,7 @@
},
"packages/engine": {
"name": "@hyperframes/engine",
- "version": "0.6.69",
+ "version": "0.6.79",
"dependencies": {
"@hono/node-server": "^1.13.0",
"@hyperframes/core": "workspace:^",
@@ -145,9 +146,29 @@
"vitest": "^3.2.4",
},
},
+ "packages/gcp-cloud-run": {
+ "name": "@hyperframes/gcp-cloud-run",
+ "version": "0.6.79",
+ "dependencies": {
+ "@google-cloud/storage": "^7.14.0",
+ "@google-cloud/workflows": "^4.2.0",
+ "@hono/node-server": "^1.13.0",
+ "@hyperframes/producer": "workspace:^",
+ "hono": "^4.6.0",
+ "puppeteer-core": "^24.39.1",
+ "tar": "^7.4.3",
+ },
+ "devDependencies": {
+ "@types/node": "^25.0.10",
+ "@types/tar": "^6.1.13",
+ "esbuild": "^0.25.12",
+ "tsx": "^4.21.0",
+ "typescript": "^5.7.2",
+ },
+ },
"packages/player": {
"name": "@hyperframes/player",
- "version": "0.6.69",
+ "version": "0.6.79",
"devDependencies": {
"@types/bun": "^1.1.0",
"gsap": "^3.12.5",
@@ -159,7 +180,7 @@
},
"packages/producer": {
"name": "@hyperframes/producer",
- "version": "0.6.69",
+ "version": "0.6.79",
"dependencies": {
"@fontsource/archivo-black": "^5.2.8",
"@fontsource/eb-garamond": "^5.2.7",
@@ -199,7 +220,7 @@
},
"packages/shader-transitions": {
"name": "@hyperframes/shader-transitions",
- "version": "0.6.69",
+ "version": "0.6.79",
"dependencies": {
"html2canvas": "^1.4.1",
},
@@ -211,7 +232,7 @@
},
"packages/studio": {
"name": "@hyperframes/studio",
- "version": "0.6.69",
+ "version": "0.6.79",
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
@@ -576,8 +597,22 @@
"@fontsource/space-mono": ["@fontsource/space-mono@5.2.9", "", {}, "sha512-b61faFOHEISQ/pD25G+cfGY9o/WW6lRv6hBQQfpWvEJ4y1V+S4gmth95EVyBE2VL3qDYHeVQ8nBzrplzdXTDDg=="],
+ "@google-cloud/paginator": ["@google-cloud/paginator@5.0.2", "", { "dependencies": { "arrify": "^2.0.0", "extend": "^3.0.2" } }, "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg=="],
+
+ "@google-cloud/projectify": ["@google-cloud/projectify@4.0.0", "", {}, "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA=="],
+
+ "@google-cloud/promisify": ["@google-cloud/promisify@4.0.0", "", {}, "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g=="],
+
+ "@google-cloud/storage": ["@google-cloud/storage@7.19.0", "", { "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", "@google-cloud/promisify": "<4.1.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", "fast-xml-parser": "^5.3.4", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", "mime": "^3.0.0", "p-limit": "^3.0.1", "retry-request": "^7.0.0", "teeny-request": "^9.0.0", "uuid": "^8.0.0" } }, "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ=="],
+
+ "@google-cloud/workflows": ["@google-cloud/workflows@4.4.0", "", { "dependencies": { "google-gax": "^5.0.0" } }, "sha512-euazZS+KeByU1ECWpZhAn8FKBKg56jEm70mQkWq1sZwyxYNO9Ke952h8yh19W2bODuJRQrBIhOwj5YjY5L0Tdw=="],
+
"@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="],
+ "@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="],
+
+ "@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
+
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@hyperframes/aws-lambda": ["@hyperframes/aws-lambda@workspace:packages/aws-lambda"],
@@ -588,6 +623,8 @@
"@hyperframes/engine": ["@hyperframes/engine@workspace:packages/engine"],
+ "@hyperframes/gcp-cloud-run": ["@hyperframes/gcp-cloud-run@workspace:packages/gcp-cloud-run"],
+
"@hyperframes/player": ["@hyperframes/player@workspace:packages/player"],
"@hyperframes/producer": ["@hyperframes/producer@workspace:packages/producer"],
@@ -662,6 +699,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+ "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
+
"@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="],
"@lezer/css": ["@lezer/css@1.3.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg=="],
@@ -948,6 +987,8 @@
"@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="],
+ "@tootallnate/once": ["@tootallnate/once@2.0.1", "", {}, "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ=="],
+
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
@@ -966,6 +1007,8 @@
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
+ "@types/caseless": ["@types/caseless@0.12.5", "", {}, "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="],
+
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
@@ -988,6 +1031,8 @@
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+ "@types/request": ["@types/request@2.48.13", "", { "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", "form-data": "^2.5.5" } }, "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg=="],
+
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
"@types/tar": ["@types/tar@6.1.13", "", { "dependencies": { "@types/node": "*", "minipass": "^4.0.0" } }, "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw=="],
@@ -1020,6 +1065,8 @@
"@webgpu/types": ["@webgpu/types@0.1.70", "", {}, "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA=="],
+ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
+
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="],
@@ -1042,6 +1089,8 @@
"array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="],
+ "arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="],
+
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
@@ -1050,6 +1099,10 @@
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
+ "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
"autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="],
"aws-cdk-lib": ["aws-cdk-lib@2.254.0", "", { "dependencies": { "@aws-cdk/asset-awscli-v1": "2.2.273", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", "@aws-cdk/cloud-assembly-api": "^2.2.3", "@aws-cdk/cloud-assembly-schema": "^53.21.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.3.3", "ignore": "^5.3.2", "jsonschema": "^1.5.0", "mime-types": "^2.1.35", "minimatch": "^10.2.3", "punycode": "^2.3.1", "semver": "^7.7.4", "table": "^6.9.0", "yaml": "1.10.3" }, "peerDependencies": { "constructs": "^10.5.0" } }, "sha512-O7fn6fu1FXb0BgoO/+WeiBXZ16H/q6z4fOndjdG62c9bPU7Z/UeW5gz7qBiwLm4ERvTObobLCqv7wjd8bp+v3A=="],
@@ -1110,6 +1163,8 @@
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
@@ -1142,6 +1197,8 @@
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
@@ -1212,6 +1269,8 @@
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devtools-protocol": ["devtools-protocol@0.0.1608973", "", {}, "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ=="],
@@ -1232,6 +1291,10 @@
"dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="],
+
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
@@ -1254,6 +1317,10 @@
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
"es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
@@ -1272,6 +1339,8 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
+
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
@@ -1322,6 +1391,8 @@
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
+ "form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
+
"formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
@@ -1334,7 +1405,7 @@
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
- "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
+ "gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
@@ -1342,6 +1413,10 @@
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
@@ -1364,6 +1439,8 @@
"google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
+ "google-gax": ["google-gax@5.0.7", "", { "dependencies": { "@grpc/grpc-js": "^1.12.6", "@grpc/proto-loader": "^0.8.0", "duplexify": "^4.1.3", "google-auth-library": "10.5.0", "google-logging-utils": "1.1.3", "node-fetch": "^3.3.2", "object-hash": "^3.0.0", "proto3-json-serializer": "3.0.4", "protobufjs": "^7.5.4", "retry-request": "^8.0.2", "rimraf": "^5.0.1" } }, "sha512-EhiqaWWJ+9h7sCcKJTsoo6tMcjokVHhWsbSuWCnZJT4vIBP3y4mAoFLnt9SzgkVZeq24ZsFaArr06nnYYku2yA=="],
+
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
@@ -1372,18 +1449,26 @@
"gsap": ["gsap@3.15.0", "", {}, "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A=="],
+ "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
+
"happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"hono": ["hono@4.12.18", "", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
+ "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
+
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
@@ -1432,6 +1517,8 @@
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+ "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
+
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -1508,6 +1595,8 @@
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
+ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
+
"lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
@@ -1524,6 +1613,8 @@
"matcher": ["matcher@4.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ=="],
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"mediabunny": ["mediabunny@1.45.3", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-GUCPYjR+5olLM7DRmupCXCmZkkSrKHVl1gyW2RztpObqLfrix19kWGf/9WgWzDW0g49DvfGXpl+zfArCx5HDMQ=="],
@@ -1534,6 +1625,8 @@
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+ "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
+
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
@@ -1560,7 +1653,7 @@
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
- "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="],
@@ -1590,6 +1683,8 @@
"oxlint": ["oxlint@1.64.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.64.0", "@oxlint/binding-android-arm64": "1.64.0", "@oxlint/binding-darwin-arm64": "1.64.0", "@oxlint/binding-darwin-x64": "1.64.0", "@oxlint/binding-freebsd-x64": "1.64.0", "@oxlint/binding-linux-arm-gnueabihf": "1.64.0", "@oxlint/binding-linux-arm-musleabihf": "1.64.0", "@oxlint/binding-linux-arm64-gnu": "1.64.0", "@oxlint/binding-linux-arm64-musl": "1.64.0", "@oxlint/binding-linux-ppc64-gnu": "1.64.0", "@oxlint/binding-linux-riscv64-gnu": "1.64.0", "@oxlint/binding-linux-riscv64-musl": "1.64.0", "@oxlint/binding-linux-s390x-gnu": "1.64.0", "@oxlint/binding-linux-x64-gnu": "1.64.0", "@oxlint/binding-linux-x64-musl": "1.64.0", "@oxlint/binding-openharmony-arm64": "1.64.0", "@oxlint/binding-win32-arm64-msvc": "1.64.0", "@oxlint/binding-win32-ia32-msvc": "1.64.0", "@oxlint/binding-win32-x64-msvc": "1.64.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-Star3SNpWPeWFPw7kRXIhXUSn6fdiAl25q15CQzH/9WaOtG6e9CWTc25vNZOCr4PE1yEP1GtKJKIKglhj3OmEQ=="],
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
@@ -1650,6 +1745,8 @@
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
+ "proto3-json-serializer": ["proto3-json-serializer@3.0.4", "", { "dependencies": { "protobufjs": "^7.4.0" } }, "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw=="],
+
"protobufjs": ["protobufjs@7.5.7", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-NGnrxS/nLKUo5nkbVQxlC71sB4hdfImdYIbFeSCidxtwATx0AHRPcANSLd0q5Bb2BkoSWo2iisQhGg5/r+ihbA=="],
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
@@ -1694,8 +1791,12 @@
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
+ "retry-request": ["retry-request@7.0.2", "", { "dependencies": { "@types/request": "^2.48.8", "extend": "^3.0.2", "teeny-request": "^9.0.0" } }, "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w=="],
+
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
+ "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
+
"rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
@@ -1748,6 +1849,10 @@
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
+ "stream-events": ["stream-events@1.0.5", "", { "dependencies": { "stubs": "^3.0.0" } }, "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg=="],
+
+ "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="],
+
"streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -1766,6 +1871,8 @@
"strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
+ "stubs": ["stubs@3.0.0", "", {}, "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw=="],
+
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
@@ -1786,6 +1893,8 @@
"tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="],
+ "teeny-request": ["teeny-request@9.0.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.9", "stream-events": "^1.0.5", "uuid": "^9.0.0" } }, "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g=="],
+
"teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
"test-exclude": ["test-exclude@7.0.2", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="],
@@ -1864,6 +1973,8 @@
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
+ "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
+
"vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
@@ -1918,6 +2029,8 @@
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="],
@@ -1938,6 +2051,8 @@
"@codemirror/lint/@codemirror/view": ["@codemirror/view@6.42.1", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-ToN3oFc0nsxNUYVF5P0ztLgbC4UPPjPtA9aKYhkOKQaZASpOUo6ISXyQLP66ctVwlDc+j6Jv0uK5IFALkiXztg=="],
+ "@google-cloud/storage/google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
+
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
@@ -1968,12 +2083,24 @@
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+ "gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
+
+ "gcp-metadata/gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
+
"get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+ "google-auth-library/gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
+
+ "google-gax/google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
+
+ "google-gax/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+
+ "google-gax/retry-request": ["retry-request@8.0.3", "", { "dependencies": { "extend": "^3.0.2", "teeny-request": "^10.0.0" } }, "sha512-qqoc4kkGgP9cmQDWELlOpAmfgJOg0Yi7MT82ZjiPWu451ayju4itwomjM4/dBEliify8C1b3tSaeCOldugtwPQ=="],
+
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="],
@@ -1990,6 +2117,8 @@
"minizlib/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+ "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+
"pac-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"pac-proxy-agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
@@ -2018,6 +2147,10 @@
"tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+ "teeny-request/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
+
+ "teeny-request/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
+
"tsup/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"tsup/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
@@ -2030,6 +2163,8 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+ "@google-cloud/storage/google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
+
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
@@ -2038,8 +2173,26 @@
"gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+ "gcp-metadata/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+
"glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
+ "google-auth-library/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "google-auth-library/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+
+ "google-gax/google-auth-library/gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
+
+ "google-gax/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
+
+ "google-gax/retry-request/teeny-request": ["teeny-request@10.1.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "stream-events": "^1.0.5" } }, "sha512-5yDliI1uWkYPo7W+Zvrxg6YmoWuj5iC5EydewqrRTvc68nyMTZhlPPlLg6cptUGfbQAb+N9XDPDPzF6N081lug=="],
+
+ "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+
+ "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
@@ -2148,8 +2301,22 @@
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
+ "@google-cloud/storage/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
+
+ "gcp-metadata/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ "google-auth-library/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "google-gax/google-auth-library/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "google-gax/retry-request/teeny-request/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
"tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+ "google-gax/google-auth-library/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "google-gax/retry-request/teeny-request/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
}
}
diff --git a/docs/deploy/gcp-cloud-run.mdx b/docs/deploy/gcp-cloud-run.mdx
new file mode 100644
index 000000000..41c5c0072
--- /dev/null
+++ b/docs/deploy/gcp-cloud-run.mdx
@@ -0,0 +1,99 @@
+---
+title: Google Cloud Run
+description: "Deploy distributed HyperFrames rendering to Google Cloud Run + Cloud Workflows, and drive renders from a laptop or CI."
+---
+
+HyperFrames ships a Google Cloud deployment that mirrors the [AWS Lambda](/deploy/aws-lambda) one: a single Cloud Run service fronts a Cloud Workflows definition that fans renders out across many parallel chunk workers, with intermediate artifacts in Google Cloud Storage. The render primitives are identical — only the storage, compute, and orchestration adapters differ.
+
+It's the right choice for teams already running their backend and storage on Google Cloud who want distributed HyperFrames rendering without adding AWS infrastructure.
+
+## Architecture
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│ Cloud Workflows definition │
+│ Plan → parallel(for chunk) RenderChunk → Assemble │
+└──────────────────────────────────────────────────────────────────┘
+ │ OIDC-authenticated http.post per step
+ ▼
+┌──────────────────────────────────────────────────────────────────┐
+│ One Cloud Run service (packages/gcp-cloud-run/Dockerfile) │
+│ dist/server.js │
+│ ├─ Action="plan" → @hyperframes/producer/distributed │
+│ ├─ Action="renderChunk" → @hyperframes/producer/distributed │
+│ └─ Action="assemble" → @hyperframes/producer/distributed │
+└──────────────────────────────────────────────────────────────────┘
+ │ GCS download / upload
+ ▼
+ Google Cloud Storage bucket
+```
+
+Each workflow step `POST`s to the same Cloud Run URL with a different `Action`. The handler downloads its inputs from GCS into the container's filesystem, runs the matching OSS primitive, uploads the output back to GCS, and returns a small JSON result. The workflow accumulates every step's result and returns `{ Plan, Chunks, Assemble }`.
+
+## Why Cloud Run is simpler than Lambda here
+
+Cloud Run runs a container image, so the Chrome story collapses to a `Dockerfile` line. There's no 250 MB ZIP ceiling, no `@sparticuz/chromium` runtime decompression, and no packaging probe — the image installs the same pinned `chrome-headless-shell` build the production renderer uses. Cloud Run gen2 also gives more headroom than Lambda: up to a 60-minute request timeout and 32 GiB of memory.
+
+## Deploy
+
+The Terraform module at `packages/gcp-cloud-run/terraform` provisions the GCS bucket, the Cloud Run service, the Cloud Workflows definition, two least-privilege service accounts, and a runaway-request alert.
+
+```bash
+# 1. Build + push the render image.
+gcloud builds submit . \
+ --tag us-central1-docker.pkg.dev/PROJECT/hyperframes/hyperframes-render:v1
+
+# 2. Apply the module.
+cd node_modules/@hyperframes/gcp-cloud-run/terraform
+terraform init
+terraform apply \
+ -var project_id=PROJECT \
+ -var region=us-central1 \
+ -var image=us-central1-docker.pkg.dev/PROJECT/hyperframes/hyperframes-render:v1
+```
+
+Terraform outputs `render_bucket_name`, `service_url`, `workflow_name`, and `region`. Pass those into the SDK.
+
+
+The target GCP project must have **billing enabled** — Cloud Run, Cloud Workflows, Artifact Registry, and Cloud Build are all billed services.
+
+
+## Render
+
+```ts
+import {
+ renderToCloudRun,
+ getRenderProgress,
+} from "@hyperframes/gcp-cloud-run/sdk";
+
+const handle = await renderToCloudRun({
+ projectDir: "./my-composition",
+ config: { fps: 30, width: 1920, height: 1080, format: "mp4" },
+ bucketName: "hyperframes-render-my-project",
+ projectId: "my-project",
+ location: "us-central1",
+ workflowId: "hyperframes-render",
+ serviceUrl: "https://hyperframes-render-abc.us-central1.run.app",
+});
+
+let progress = await getRenderProgress({ executionName: handle.executionName });
+while (progress.status === "running") {
+ await new Promise((r) => setTimeout(r, 5000));
+ progress = await getRenderProgress({ executionName: handle.executionName });
+}
+console.log(progress.status, progress.outputFile, progress.costs.displayCost);
+```
+
+Templates with [variables](/concepts/variables) work the same way — declare `data-composition-variables` on the composition and pass `config.variables`. The Cloud Workflows execution argument is capped at 512 KiB, so pass media as URL references the composition resolves at render time rather than inlining base64.
+
+## End-to-end smoke
+
+`examples/gcp-cloud-run/scripts/smoke.sh` builds the image, applies the Terraform module, renders a fixture composition through the workflow at one or more chunk sizes, PSNR-compares each output against the in-process baseline, and tears the stack down.
+
+```bash
+examples/gcp-cloud-run/scripts/smoke.sh --project my-project --region us-central1
+```
+
+## Supported formats
+
+Same as the distributed pipeline everywhere: `mp4` (H.264 / H.265), `mov` (ProRes), `webm` (VP9), and `png-sequence`. HDR mp4 is not supported in distributed mode.
diff --git a/docs/docs.json b/docs/docs.json
index 0de7e282e..3982833c1 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -101,6 +101,7 @@
"pages": [
"guides/deploy",
"deploy/aws-lambda",
+ "deploy/gcp-cloud-run",
"deploy/templates-on-lambda",
"deploy/migrating-to-hyperframes-lambda"
]
diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx
index 8607ada9d..e2d3bc023 100644
--- a/docs/packages/cli.mdx
+++ b/docs/packages/cli.mdx
@@ -1207,6 +1207,55 @@ The actions list is deliberately broad (`Resource: "*"`) because CloudFormation
`hyperframes lambda` keeps per-stack metadata under `/.hyperframes/lambda-stack-.json` so the verbs don't need to call `describe-stacks` every time. Commit the file to a repo or `.gitignore` it depending on your workflow — it contains the bucket name, state-machine ARN, and region, none of which are secrets but all of which are AWS-account-identifying.
+## hyperframes cloudrun
+
+The Google Cloud counterpart to `hyperframes lambda`. Deploys HyperFrames distributed rendering to Cloud Run + Cloud Workflows and drives renders from your laptop or CI. Wraps the `@hyperframes/gcp-cloud-run` SDK plus `terraform` (the module shipped with the package) and `gcloud` / Cloud Build for the image.
+
+```bash
+hyperframes cloudrun deploy --project my-gcp-project
+hyperframes cloudrun render ./my-project --width 1920 --height 1080 --wait
+hyperframes cloudrun destroy --project my-gcp-project # when you're done
+```
+
+#### `cloudrun deploy`
+
+Enables the required APIs, builds + pushes the render image via Cloud Build (unless you pass `--image`), then `terraform apply`s the module that provisions the GCS bucket, Cloud Run service, Cloud Workflows definition, two service accounts, and a runaway-request alert. Caches the resulting bucket / service URL / workflow id so later verbs don't need them re-passed.
+
+```bash
+hyperframes cloudrun deploy --project my-gcp-project --region us-central1
+hyperframes cloudrun deploy --project my-gcp-project --image us-central1-docker.pkg.dev/my-gcp-project/hyperframes/hyperframes-render:v1
+```
+
+Flags: `--project` (required), `--region` (default `us-central1`), `--image` (skip the build), `--repo` (Artifact Registry repo, default `hyperframes`). Machine sizing / scaling: `--cpu` (1/2/4/8, default 4), `--memory` (e.g. `32Gi`, default `16Gi`), `--max-instances` (render fan-out ceiling, default 100), `--timeout` (per-request seconds, max 3600). Omitted sizing flags keep the module defaults; for anything finer, apply the Terraform module directly.
+
+#### `cloudrun sites create `
+
+Tar + upload a project to GCS once and reuse it across renders. `--site-id` overrides the content hash. Prints the `gs://` URI.
+
+#### `cloudrun render `
+
+Start a distributed render. `--width` / `--height` are required; `--fps` (24/30/60), `--format`, `--codec`, `--quality`, `--chunk-size`, `--max-parallel-chunks`, and `--output-resolution` (deviceScaleFactor supersampling, e.g. `4k`) mirror the local render flags. Pass composition variables with `--variables '{"title":"Hi"}'` or `--variables-file alice.json`; add `--strict-variables` to fail on a key that's undeclared or mistyped vs the composition's `data-composition-variables`. `--wait` polls until the render finishes and prints the output URI + cost; without it the command returns an execution name.
+
+#### `cloudrun render-batch `
+
+Fan out N personalised renders from a JSONL batch file (`--batch users.jsonl`, one `{"outputKey":"...","variables":{...}}` per line). Deploys the site once and starts an execution per entry, capped at `--max-concurrent` (default 50). `--dry-run` prints the resolved manifest without starting anything. Shares the render flags above.
+
+#### `cloudrun progress `
+
+Print progress + cost for an in-flight or finished render. Coarse `running` progress; exact frame counts + cost on success.
+
+#### `cloudrun destroy`
+
+`terraform destroy` the stack (force-destroys the render bucket). Reads the cached project/region, or pass `--project` / `--region`.
+
+### When to pick `cloudrun` vs `lambda`
+
+Same trade-off as `lambda`, on Google Cloud instead of AWS. Pick `cloudrun` when your backend + storage already live on GCP. The render primitives are identical; only the storage (GCS), compute (Cloud Run), and orchestration (Cloud Workflows) adapters differ.
+
+### State file
+
+`hyperframes cloudrun` caches the deployed stack's coordinates under `~/.hyperframes/cloudrun-state.json` (project id, region, bucket, service URL, workflow id) so `render` / `progress` / `destroy` don't need them re-passed. None are secrets, but all are GCP-project-identifying.
+
## hyperframes.json
`hyperframes init` writes a `hyperframes.json` file at the root of every new project. `hyperframes add` reads it to know which registry to pull items from and where to drop them. Edit the file (or delete it to fall back to defaults) to reshape your project layout or point at a custom registry.
diff --git a/examples/gcp-cloud-run/README.md b/examples/gcp-cloud-run/README.md
new file mode 100644
index 000000000..f7be4eda9
--- /dev/null
+++ b/examples/gcp-cloud-run/README.md
@@ -0,0 +1,51 @@
+# Google Cloud Run example
+
+End-to-end deployment + smoke for [`@hyperframes/gcp-cloud-run`](../../packages/gcp-cloud-run) — the Cloud Run + Cloud Workflows adapter for HyperFrames distributed rendering.
+
+## Layout
+
+```
+scripts/smoke.sh Real-GCP smoke: build → deploy → render → PSNR → destroy
+sample-events/ Example request bodies for the Cloud Run handler
+ (plan.json, render-chunk.json, assemble.json)
+```
+
+The Terraform module and the Cloud Workflows definition that the smoke deploys live with the package, at `packages/gcp-cloud-run/terraform/` (including `workflow.yaml`).
+
+## Prerequisites
+
+- `gcloud` authenticated, with a project that has **billing enabled**
+- `terraform` (≥ 1.5), `docker`, `ffmpeg`, `jq` on PATH
+
+## Run the smoke
+
+```bash
+# Renders the mp4-h264-sdr fixture through the workflow and PSNR-compares it
+# against the in-process baseline, then tears the stack down.
+./scripts/smoke.sh --project YOUR_GCP_PROJECT --region us-central1
+
+# Keep the stack up to poke at it:
+./scripts/smoke.sh --project YOUR_GCP_PROJECT --keep-stack
+
+# Render at several chunk sizes to see the fan-out scaling:
+./scripts/smoke.sh --project YOUR_GCP_PROJECT --chunk-sizes 30,15,10
+```
+
+Outputs land in `scripts/gcp-smoke-artifacts/`: `results.json`
+(`chunkSize × wallClockMs × psnrAvgDb`), the rendered MP4s, and each
+workflow execution's describe output.
+
+## Test the handler locally
+
+The sample events exercise the same body shape Cloud Workflows sends. With the
+container running locally (`PORT=8080`) and credentials that can reach a GCS
+bucket, you can drive a single action:
+
+```bash
+curl -sX POST localhost:8080/ \
+ -H 'content-type: application/json' \
+ --data @sample-events/plan.json | jq .
+```
+
+Replace the `PROJECT` placeholder bucket names and `REPLACE_WITH_PLAN_HASH`
+with real values from a prior `plan` response.
diff --git a/examples/gcp-cloud-run/sample-events/assemble.json b/examples/gcp-cloud-run/sample-events/assemble.json
new file mode 100644
index 000000000..115a538bd
--- /dev/null
+++ b/examples/gcp-cloud-run/sample-events/assemble.json
@@ -0,0 +1,11 @@
+{
+ "Action": "assemble",
+ "PlanGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/plan.tar.gz",
+ "ChunkGcsUris": [
+ "gs://hyperframes-render-PROJECT/renders/hf-render-demo/chunks/0000.mp4",
+ "gs://hyperframes-render-PROJECT/renders/hf-render-demo/chunks/0001.mp4"
+ ],
+ "AudioGcsUri": null,
+ "OutputGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/output.mp4",
+ "Format": "mp4"
+}
diff --git a/examples/gcp-cloud-run/sample-events/plan.json b/examples/gcp-cloud-run/sample-events/plan.json
new file mode 100644
index 000000000..667988b41
--- /dev/null
+++ b/examples/gcp-cloud-run/sample-events/plan.json
@@ -0,0 +1,6 @@
+{
+ "Action": "plan",
+ "ProjectGcsUri": "gs://hyperframes-render-PROJECT/sites/abc123/project.tar.gz",
+ "PlanOutputGcsPrefix": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/",
+ "Config": { "fps": 30, "width": 1920, "height": 1080, "format": "mp4" }
+}
diff --git a/examples/gcp-cloud-run/sample-events/render-chunk.json b/examples/gcp-cloud-run/sample-events/render-chunk.json
new file mode 100644
index 000000000..b98ad63ad
--- /dev/null
+++ b/examples/gcp-cloud-run/sample-events/render-chunk.json
@@ -0,0 +1,8 @@
+{
+ "Action": "renderChunk",
+ "PlanGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/plan.tar.gz",
+ "PlanHash": "REPLACE_WITH_PLAN_HASH",
+ "ChunkIndex": 0,
+ "ChunkOutputGcsPrefix": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/",
+ "Format": "mp4"
+}
diff --git a/examples/gcp-cloud-run/scripts/smoke.sh b/examples/gcp-cloud-run/scripts/smoke.sh
new file mode 100755
index 000000000..4cd4dbf1d
--- /dev/null
+++ b/examples/gcp-cloud-run/scripts/smoke.sh
@@ -0,0 +1,239 @@
+#!/usr/bin/env bash
+# Real-GCP smoke + benchmark for the HyperFrames Cloud Run adapter.
+#
+# Run from a workstation with `gcloud` credentials. Builds the render
+# container, pushes it to Artifact Registry, applies the Terraform module at
+# packages/gcp-cloud-run/terraform to your project, renders a fixture
+# composition through the Cloud Workflows definition, PSNR-compares the
+# output against the in-process baseline, and tears the stack down.
+#
+# Usage:
+# ./smoke.sh --project
+# ./smoke.sh --project p --fixture mp4-h264-sdr --chunk-sizes 15,30
+# ./smoke.sh --project p --keep-stack
+#
+# Required tools on PATH:
+# - gcloud (authenticated; the target project must have billing enabled)
+# - terraform (>= 1.5)
+# - docker
+# - ffmpeg (PSNR computation)
+# - jq
+#
+# Inputs (flags or env vars):
+# --project (required; or $GCP_PROJECT)
+# --region (default: us-central1)
+# --fixture (default: mp4-h264-sdr — under packages/producer/tests/distributed/)
+# --chunk-sizes (default: from the fixture meta; CSV of chunkSize overrides)
+# --psnr-threshold (default: 35)
+# --repo (Artifact Registry repo name, default: hyperframes)
+# --keep-stack (skip `terraform destroy` at the end)
+# --skip-build (reuse the last-pushed image tag in ./gcp-smoke-artifacts/image.txt)
+#
+# Outputs:
+# ./gcp-smoke-artifacts/results.json (chunkSize x wallClockMs x psnrAvgDb)
+# ./gcp-smoke-artifacts/renders/c-output.mp4
+# ./gcp-smoke-artifacts/renders/c-execution.json
+#
+# Exit codes:
+# 0 all good 1 arg/pre-flight 2 build/push 3 terraform apply
+# 4 a render failed 5 PSNR below threshold
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
+TF_DIR="$REPO_ROOT/packages/gcp-cloud-run/terraform"
+
+# ── Defaults ──────────────────────────────────────────────────────────────
+PROJECT="${GCP_PROJECT:-}"
+REGION="${GCP_REGION:-us-central1}"
+FIXTURE="${FIXTURE:-mp4-h264-sdr}"
+CHUNK_SIZES="${CHUNK_SIZES:-}"
+PSNR_THRESHOLD="${PSNR_THRESHOLD:-35}"
+AR_REPO="${AR_REPO:-hyperframes}"
+KEEP_STACK=0
+SKIP_BUILD=0
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --project) PROJECT="$2"; shift 2 ;;
+ --region) REGION="$2"; shift 2 ;;
+ --fixture) FIXTURE="$2"; shift 2 ;;
+ --chunk-sizes) CHUNK_SIZES="$2"; shift 2 ;;
+ --psnr-threshold) PSNR_THRESHOLD="$2"; shift 2 ;;
+ --repo) AR_REPO="$2"; shift 2 ;;
+ --keep-stack) KEEP_STACK=1; shift ;;
+ --skip-build) SKIP_BUILD=1; shift ;;
+ -h|--help) sed -n '2,40p' "$0"; exit 0 ;;
+ *) echo "Unknown arg: $1" >&2; exit 1 ;;
+ esac
+done
+
+[ -n "$PROJECT" ] || { echo "ERROR: --project (or \$GCP_PROJECT) is required" >&2; exit 1; }
+for tool in gcloud terraform docker ffmpeg jq; do
+ command -v "$tool" >/dev/null || { echo "ERROR: $tool not on PATH" >&2; exit 1; }
+done
+
+FIXTURE_DIR="$REPO_ROOT/packages/producer/tests/distributed/$FIXTURE"
+FIXTURE_META="$FIXTURE_DIR/meta.json"
+BASELINE_MP4="$FIXTURE_DIR/output/output.mp4"
+[ -d "$FIXTURE_DIR/src" ] || { echo "ERROR: fixture src missing: $FIXTURE_DIR/src" >&2; exit 1; }
+[ -f "$BASELINE_MP4" ] || { echo "ERROR: baseline mp4 missing: $BASELINE_MP4" >&2; exit 1; }
+
+ARTIFACT_DIR="$SCRIPT_DIR/gcp-smoke-artifacts"
+mkdir -p "$ARTIFACT_DIR/renders"
+
+echo "→ Project: $PROJECT Region: $REGION Fixture: $FIXTURE"
+
+# ── 1. Enable APIs ──────────────────────────────────────────────────────────
+echo "→ Enabling required APIs (idempotent)"
+gcloud services enable \
+ run.googleapis.com workflows.googleapis.com workflowexecutions.googleapis.com \
+ artifactregistry.googleapis.com cloudbuild.googleapis.com monitoring.googleapis.com \
+ --project "$PROJECT" >/dev/null
+
+# ── 2. Build + push the render image ────────────────────────────────────────
+IMAGE_TXT="$ARTIFACT_DIR/image.txt"
+if [ "$SKIP_BUILD" -eq 1 ] && [ -f "$IMAGE_TXT" ]; then
+ IMAGE="$(cat "$IMAGE_TXT")"
+ echo "→ Reusing image $IMAGE"
+else
+ gcloud artifacts repositories describe "$AR_REPO" --location "$REGION" --project "$PROJECT" >/dev/null 2>&1 || \
+ gcloud artifacts repositories create "$AR_REPO" --repository-format docker \
+ --location "$REGION" --project "$PROJECT" >/dev/null
+ TAG="$(date +%Y%m%d-%H%M%S)"
+ IMAGE="$REGION-docker.pkg.dev/$PROJECT/$AR_REPO/hyperframes-render:$TAG"
+ echo "→ Building + pushing $IMAGE via Cloud Build"
+ # The Dockerfile lives at packages/gcp-cloud-run/Dockerfile, not the repo
+ # root, so we drive the build with an inline cloudbuild config rather than
+ # `--tag` (which assumes a root Dockerfile).
+ CB_CONFIG="$ARTIFACT_DIR/cloudbuild.yaml"
+ cat > "$CB_CONFIG" <&2; exit 2; }
+ echo "$IMAGE" > "$IMAGE_TXT"
+fi
+
+# ── 3. terraform apply ──────────────────────────────────────────────────────
+# The google provider authenticates via Application Default Credentials. If
+# ADC isn't configured (common on a box set up with only `gcloud auth login`),
+# fall back to a short-lived access token from the active gcloud account.
+if ! gcloud auth application-default print-access-token >/dev/null 2>&1; then
+ echo "→ ADC not configured; using a gcloud access token for Terraform"
+ export GOOGLE_OAUTH_ACCESS_TOKEN="$(gcloud auth print-access-token)"
+ export GOOGLE_PROJECT="$PROJECT"
+fi
+echo "→ terraform apply"
+terraform -chdir="$TF_DIR" init -input=false >/dev/null
+terraform -chdir="$TF_DIR" apply -input=false -auto-approve \
+ -var "project_id=$PROJECT" -var "region=$REGION" -var "image=$IMAGE" \
+ || { echo "ERROR: terraform apply failed" >&2; exit 3; }
+
+BUCKET="$(terraform -chdir="$TF_DIR" output -raw render_bucket_name)"
+SERVICE_URL="$(terraform -chdir="$TF_DIR" output -raw service_url)"
+WORKFLOW="$(terraform -chdir="$TF_DIR" output -raw workflow_name)"
+echo " bucket=$BUCKET service=$SERVICE_URL workflow=$WORKFLOW"
+
+cleanup() {
+ if [ "$KEEP_STACK" -eq 0 ]; then
+ echo "→ terraform destroy"
+ # Apply force_destroy=true into state FIRST. Terraform reads the bucket's
+ # force_destroy from prior state during the destroy step, so a destroy
+ # alone can't flip it; a quick apply updates the attribute, then destroy
+ # can empty + remove the (scratch) bucket.
+ terraform -chdir="$TF_DIR" apply -input=false -auto-approve \
+ -var "project_id=$PROJECT" -var "region=$REGION" -var "image=$IMAGE" \
+ -var "bucket_force_destroy=true" >/dev/null 2>&1 || true
+ terraform -chdir="$TF_DIR" destroy -input=false -auto-approve \
+ -var "project_id=$PROJECT" -var "region=$REGION" -var "image=$IMAGE" \
+ -var "bucket_force_destroy=true" || true
+ else
+ echo "→ --keep-stack set; leaving the stack up. Destroy with:"
+ echo " terraform -chdir=$TF_DIR destroy -var project_id=$PROJECT -var region=$REGION -var image=$IMAGE -var bucket_force_destroy=true"
+ fi
+}
+trap cleanup EXIT
+
+# ── 4. Upload the fixture as a project tarball ──────────────────────────────
+SITE_TAR="$ARTIFACT_DIR/project.tar.gz"
+tar -czf "$SITE_TAR" -C "$FIXTURE_DIR/src" .
+PROJECT_GCS="gs://$BUCKET/sites/$FIXTURE/project.tar.gz"
+gcloud storage cp "$SITE_TAR" "$PROJECT_GCS" --project "$PROJECT" >/dev/null
+echo "→ Uploaded fixture to $PROJECT_GCS"
+
+BASE_FPS=$(jq -r '.renderConfig.fps // 30' "$FIXTURE_META")
+META_CHUNK=$(jq -r '.renderConfig.chunkSize // empty' "$FIXTURE_META")
+[ -n "$CHUNK_SIZES" ] || CHUNK_SIZES="${META_CHUNK:-15}"
+
+echo "[]" > "$ARTIFACT_DIR/results.json"
+OVERALL_RC=0
+
+IFS=',' read -ra SIZES <<< "$CHUNK_SIZES"
+for CS in "${SIZES[@]}"; do
+ RENDER_ID="hf-smoke-c${CS}-$(date +%s)"
+ OUT_GCS="gs://$BUCKET/renders/$RENDER_ID/output.mp4"
+ ARG=$(jq -n \
+ --arg svc "$SERVICE_URL" \
+ --arg proj "$PROJECT_GCS" \
+ --arg prefix "gs://$BUCKET/renders/$RENDER_ID/" \
+ --arg out "$OUT_GCS" \
+ --argjson fps "$BASE_FPS" \
+ --argjson cs "$CS" \
+ '{ServiceUrl:$svc, ProjectGcsUri:$proj, PlanOutputGcsPrefix:$prefix, OutputGcsUri:$out,
+ Config:{fps:$fps, width:640, height:360, format:"mp4", chunkSize:$cs}}')
+
+ echo "→ Render chunkSize=$CS (renderId=$RENDER_ID)"
+ START_MS=$(date +%s%3N)
+ EXEC=$(gcloud workflows execute "$WORKFLOW" --location "$REGION" --project "$PROJECT" \
+ --data "$ARG" --format='value(name)')
+ # Poll until terminal.
+ STATE="ACTIVE"
+ while [ "$STATE" = "ACTIVE" ] || [ "$STATE" = "QUEUED" ]; do
+ sleep 5
+ STATE=$(gcloud workflows executions describe "$EXEC" --location "$REGION" \
+ --project "$PROJECT" --format='value(state)')
+ done
+ END_MS=$(date +%s%3N)
+ WALL=$((END_MS - START_MS))
+
+ gcloud workflows executions describe "$EXEC" --location "$REGION" --project "$PROJECT" \
+ --format=json > "$ARTIFACT_DIR/renders/c$CS-execution.json"
+
+ if [ "$STATE" != "SUCCEEDED" ]; then
+ echo " ✗ execution state=$STATE"
+ jq -r '.error.payload // empty' "$ARTIFACT_DIR/renders/c$CS-execution.json" | head -c 800
+ OVERALL_RC=4
+ continue
+ fi
+
+ OUT_LOCAL="$ARTIFACT_DIR/renders/c$CS-output.mp4"
+ gcloud storage cp "$OUT_GCS" "$OUT_LOCAL" --project "$PROJECT" >/dev/null
+
+ # PSNR vs the in-process baseline.
+ PSNR_LOG="$ARTIFACT_DIR/renders/c$CS-psnr.log"
+ ffmpeg -y -i "$OUT_LOCAL" -i "$BASELINE_MP4" \
+ -lavfi "psnr=stats_file=$PSNR_LOG" -f null - 2>/dev/null || true
+ PSNR_AVG=$(awk -F'psnr_avg:' '/psnr_avg:/{split($2,a," "); s+=a[1]; n++} END{if(n>0) printf "%.2f", s/n; else print "0"}' "$PSNR_LOG" 2>/dev/null || echo "0")
+
+ echo " ✓ state=SUCCEEDED wall=${WALL}ms psnr_avg=${PSNR_AVG}dB"
+ jq --argjson cs "$CS" --argjson wall "$WALL" --arg psnr "$PSNR_AVG" \
+ '. += [{chunkSize:$cs, wallClockMs:$wall, psnrAvgDb:($psnr|tonumber)}]' \
+ "$ARTIFACT_DIR/results.json" > "$ARTIFACT_DIR/results.json.tmp" && \
+ mv "$ARTIFACT_DIR/results.json.tmp" "$ARTIFACT_DIR/results.json"
+
+ if awk "BEGIN{exit !($PSNR_AVG < $PSNR_THRESHOLD)}"; then
+ echo " ✗ PSNR ${PSNR_AVG}dB below threshold ${PSNR_THRESHOLD}dB"
+ OVERALL_RC=5
+ fi
+done
+
+echo "→ Results:"; cat "$ARTIFACT_DIR/results.json" | jq .
+exit $OVERALL_RC
diff --git a/package.json b/package.json
index 60e032a88..62383845a 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"type": "module",
"scripts": {
"dev": "bun run studio",
- "build": "bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda}' build && bun run --filter @hyperframes/cli build",
+ "build": "bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run}' build && bun run --filter @hyperframes/cli build",
"build:producer": "bun run --filter @hyperframes/producer build",
"studio": "bun run --filter @hyperframes/studio dev",
"build:hyperframes-runtime": "bun run --filter @hyperframes/core build:hyperframes-runtime",
diff --git a/packages/aws-lambda/src/events.ts b/packages/aws-lambda/src/events.ts
index f4c5f0b47..738ef0188 100644
--- a/packages/aws-lambda/src/events.ts
+++ b/packages/aws-lambda/src/events.ts
@@ -16,7 +16,12 @@
* results per §2.4).
*/
-import type { DistributedFormat, DistributedRenderConfig } from "@hyperframes/producer/distributed";
+import type {
+ DistributedFormat,
+ SerializableDistributedRenderConfig,
+} from "@hyperframes/producer/distributed";
+
+export type { SerializableDistributedRenderConfig } from "@hyperframes/producer/distributed";
/** Discriminator for the three roles the one Lambda image fulfills. */
export type LambdaAction = "plan" | "renderChunk" | "assemble";
@@ -93,17 +98,6 @@ export interface AssembleEvent {
Cfr?: boolean;
}
-/**
- * `DistributedRenderConfig` minus the runtime-only fields (`logger`,
- * `abortSignal`, `producerConfig`). The Step Functions event JSON cannot
- * carry function references; the handler reconstitutes the runtime fields
- * from Lambda environment + the AbortController it owns.
- */
-export type SerializableDistributedRenderConfig = Omit<
- DistributedRenderConfig,
- "logger" | "abortSignal" | "producerConfig"
->;
-
// ── Result types — kept small to fit Step Functions history budgets ─────────
/** Result of a `plan` invocation. Carries enough to size the Map(N) state. */
diff --git a/packages/aws-lambda/src/sdk/deploySite.ts b/packages/aws-lambda/src/sdk/deploySite.ts
index 7cc5af09d..c325a0b6a 100644
--- a/packages/aws-lambda/src/sdk/deploySite.ts
+++ b/packages/aws-lambda/src/sdk/deploySite.ts
@@ -12,12 +12,11 @@
* produce the same `siteId` and `HeadObject`-short-circuit the upload.
*/
-import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
-import { createHash } from "node:crypto";
+import { mkdtempSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
-import { join, relative } from "node:path";
+import { join } from "node:path";
import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";
-import { PLAN_PROJECT_DIR_SKIP_SEGMENTS } from "@hyperframes/producer/distributed";
+import { hashProjectDir } from "@hyperframes/producer/distributed";
import { formatS3Uri, tarDirectory, uploadFileToS3 } from "../s3Transport.js";
/** Options for {@link deploySite}. */
@@ -110,41 +109,6 @@ export async function deploySite(opts: DeploySiteOptions): Promise {
}
}
-/**
- * SHA-256 over every regular file under `projectDir` (sorted by relative
- * path) → 16-character hex prefix. The prefix is the `siteId`.
- *
- * The hash includes the relative path plus every byte of each file, so a
- * same-bytes rename still yields a fresh id. We trim to 16 chars because
- * the full 64 isn't useful in an S3 key for legibility.
- *
- * Reads are synchronous: project trees are typically tens of MB at most
- * (HTML/CSS/JS plus a few composition assets), so the simpler shape wins
- * over a streaming pipeline.
- */
-function hashProjectDir(projectDir: string): string {
- const hash = createHash("sha256");
- const files: string[] = [];
- function walk(dir: string, isRoot: boolean): void {
- for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
- a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
- )) {
- if (isRoot && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(entry.name)) continue;
- const full = join(dir, entry.name);
- if (entry.isDirectory()) walk(full, false);
- else if (entry.isFile()) files.push(full);
- }
- }
- walk(projectDir, true);
- for (const file of files) {
- const rel = relative(projectDir, file).replaceAll("\\", "/");
- hash.update(rel);
- hash.update("\0");
- hash.update(readFileSync(file));
- }
- return hash.digest("hex").slice(0, 16);
-}
-
async function headObject(
s3: S3Client,
bucket: string,
diff --git a/packages/aws-lambda/src/sdk/validateConfig.ts b/packages/aws-lambda/src/sdk/validateConfig.ts
index 59dd0d9a9..934e2a470 100644
--- a/packages/aws-lambda/src/sdk/validateConfig.ts
+++ b/packages/aws-lambda/src/sdk/validateConfig.ts
@@ -1,194 +1,32 @@
/**
- * Client-side validation of `SerializableDistributedRenderConfig` so the
- * SDK fails on shape errors with a typed `InvalidConfigError` *before* a
- * Step Functions execution starts.
+ * Client-side validation for the AWS Lambda adapter.
*
- * The producer's `plan` stage validates the same fields server-side, but a
- * caller staring at "ExecutionFailed: BROWSER_GPU_NOT_SOFTWARE" five
- * minutes after StartExecution has to dig through Step Functions history
- * to learn that the renderToLambda call passed an unsupported format.
- * Catching the obvious mistakes locally turns that wait into a synchronous
- * throw.
- *
- * The check is deliberately narrow — it covers the *shape* errors any
- * caller could have surfaced with `tsc` if they passed a literal, plus
- * the `force-hdr` rejection (HDR mp4 isn't supported in distributed
- * mode). webm was previously rejected here too; v0.7+ supports it via
- * closed-GOP concat-copy. Anything deeper (font availability, plan
- * size cap, GPU mode at runtime) needs the actual planner.
+ * The cloud-agnostic config-shape validation (`validateDistributedRenderConfig`,
+ * `validateVariablesPayload`, `InvalidConfigError`) lives in
+ * `@hyperframes/producer/distributed` and is shared with the other adapters.
+ * This module re-exports those and adds the one piece specific to Step
+ * Functions: the 256 KiB Standard-workflow execution-input size cap.
*/
-import type { DistributedFormat } from "../formatExtension.js";
-import type { SerializableDistributedRenderConfig } from "../events.js";
+import { InvalidConfigError } from "@hyperframes/producer/distributed";
-/** Thrown for any client-side `SerializableDistributedRenderConfig` violation. */
-export class InvalidConfigError extends Error {
- // Read via Error.prototype.toString; fallow can't see it.
- // fallow-ignore-next-line unused-class-member
- override readonly name = "InvalidConfigError";
- /** Dotted JSON-pointer-ish path to the offending field, e.g. `config.fps`. */
- readonly field: string;
- constructor(field: string, message: string) {
- super(`[validateConfig] ${field}: ${message}`);
- this.field = field;
- }
-}
-
-const ALLOWED_FPS = [24, 30, 60] as const;
-const ALLOWED_FORMATS = [
- "mp4",
- "mov",
- "png-sequence",
- "webm",
-] as const satisfies readonly DistributedFormat[];
-const ALLOWED_CODECS = ["h264", "h265"] as const;
-const ALLOWED_QUALITIES = ["draft", "standard", "high"] as const;
-const ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "none"] as const;
-const ALLOWED_HDR_MODES = ["auto", "force-sdr"] as const;
-
-const MAX_DIMENSION = 7680;
-const MIN_DIMENSION = 16;
-const MAX_CHUNK_SIZE = 3600;
-const MAX_PARALLEL_CHUNKS_CEILING = 256;
+export {
+ InvalidConfigError,
+ validateDistributedRenderConfig,
+ validateVariablesPayload,
+} from "@hyperframes/producer/distributed";
/**
- * Throw an `InvalidConfigError` if `config` is not a valid
- * `SerializableDistributedRenderConfig`. Returns the same reference on
- * success so the call site reads:
- *
- * const validated = validateDistributedRenderConfig(input);
- */
-export function validateDistributedRenderConfig(
- config: SerializableDistributedRenderConfig,
-): SerializableDistributedRenderConfig {
- if (config === null || typeof config !== "object") {
- throw new InvalidConfigError("config", "must be an object");
- }
-
- if (!ALLOWED_FPS.includes(config.fps as 24 | 30 | 60)) {
- throw new InvalidConfigError(
- "config.fps",
- `must be one of ${ALLOWED_FPS.join(", ")}; got ${String(config.fps)}`,
- );
- }
-
- validateIntDimension("config.width", config.width);
- validateIntDimension("config.height", config.height);
-
- if (!ALLOWED_FORMATS.includes(config.format)) {
- throw new InvalidConfigError(
- "config.format",
- `must be one of ${ALLOWED_FORMATS.join(", ")}; got ${String(config.format)}`,
- );
- }
-
- if (config.codec !== undefined) {
- if (config.format !== "mp4") {
- throw new InvalidConfigError(
- "config.codec",
- `is only valid with format="mp4"; got format=${String(config.format)}`,
- );
- }
- if (!ALLOWED_CODECS.includes(config.codec)) {
- throw new InvalidConfigError(
- "config.codec",
- `must be one of ${ALLOWED_CODECS.join(", ")}; got ${String(config.codec)}`,
- );
- }
- }
-
- if (config.quality !== undefined && !ALLOWED_QUALITIES.includes(config.quality)) {
- throw new InvalidConfigError(
- "config.quality",
- `must be one of ${ALLOWED_QUALITIES.join(", ")}; got ${String(config.quality)}`,
- );
- }
-
- if (config.crf !== undefined && config.bitrate !== undefined) {
- throw new InvalidConfigError("config.crf", "is mutually exclusive with config.bitrate");
- }
- if (
- config.crf !== undefined &&
- (!Number.isInteger(config.crf) || config.crf < 0 || config.crf > 51)
- ) {
- throw new InvalidConfigError("config.crf", `must be an integer in [0, 51]; got ${config.crf}`);
- }
- if (config.bitrate !== undefined && !/^\d+(\.\d+)?[kKmM]?$/.test(config.bitrate)) {
- throw new InvalidConfigError(
- "config.bitrate",
- `must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`,
- );
- }
-
- if (config.chunkSize !== undefined) {
- if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
- throw new InvalidConfigError(
- "config.chunkSize",
- `must be a positive integer; got ${config.chunkSize}`,
- );
- }
- if (config.chunkSize > MAX_CHUNK_SIZE) {
- throw new InvalidConfigError(
- "config.chunkSize",
- // Lambda 15-min cap leaves no useful headroom past ~3600 frames
- // at 4 fps capture-equivalent throughput; rejecting up front
- // avoids a 14-minute Plan-state retry storm.
- `must be ≤ ${MAX_CHUNK_SIZE} (Lambda 15-min cap); got ${config.chunkSize}`,
- );
- }
- }
-
- if (config.maxParallelChunks !== undefined) {
- if (!Number.isInteger(config.maxParallelChunks) || config.maxParallelChunks < 1) {
- throw new InvalidConfigError(
- "config.maxParallelChunks",
- `must be a positive integer; got ${config.maxParallelChunks}`,
- );
- }
- if (config.maxParallelChunks > MAX_PARALLEL_CHUNKS_CEILING) {
- throw new InvalidConfigError(
- "config.maxParallelChunks",
- `must be ≤ ${MAX_PARALLEL_CHUNKS_CEILING}; got ${config.maxParallelChunks}`,
- );
- }
- }
-
- if (config.runtimeCap !== undefined && !ALLOWED_RUNTIME_CAPS.includes(config.runtimeCap)) {
- throw new InvalidConfigError(
- "config.runtimeCap",
- `must be one of ${ALLOWED_RUNTIME_CAPS.join(", ")}; got ${String(config.runtimeCap)}`,
- );
- }
-
- if (config.hdrMode !== undefined && !ALLOWED_HDR_MODES.includes(config.hdrMode)) {
- // `force-hdr` is rejected here on top of the producer's plan-stage
- // rejection — it makes the typical typo (`"force-hdr"` from a copy-
- // paste of in-process config) surface synchronously instead of as a
- // typed Step Functions failure two minutes in.
- throw new InvalidConfigError(
- "config.hdrMode",
- `distributed mode supports only ${ALLOWED_HDR_MODES.join(", ")}; got ${String(config.hdrMode)}`,
- );
- }
-
- if (config.variables !== undefined) {
- validateVariablesPayload(config.variables);
- }
-
- return config;
-}
-
-/**
- * Hard cap on Step Functions Standard workflow execution input — 256 KiB
- * per the AWS limits page. Express workflows cap at 32 KiB; the render
- * stack runs Standard for execution-history visibility, so the larger
- * limit applies. The cap is on the entire serialized input, not just the
- * variables, because users hit it at the wire boundary regardless of
- * which field caused the bloat.
+ * Hard cap on Step Functions Standard workflow execution input — 256 KiB per
+ * the AWS limits page. Express workflows cap at 32 KiB; the render stack runs
+ * Standard for execution-history visibility, so the larger limit applies. The
+ * cap is on the entire serialized input, not just the variables, because
+ * users hit it at the wire boundary regardless of which field caused the
+ * bloat.
*
* Specific to Step Functions Standard. Other workflow runtimes (Temporal,
- * Express SFN, raw Lambda invoke) have different caps; this constant
- * shouldn't be reused for those without confirming the limit.
+ * Express SFN, Cloud Workflows, raw Lambda invoke) have different caps; don't
+ * reuse this constant for those without confirming the limit.
*/
export const MAX_STEP_FUNCTIONS_INPUT_BYTES = 256 * 1024;
@@ -197,10 +35,10 @@ const LARGE_VARIABLES_DOCS_URL =
"https://hyperframes.heygen.com/deploy/templates-on-lambda#working-with-large-variables";
/**
- * Validate that the serialized Step Functions execution input fits inside
- * the 256 KiB Standard-workflow cap. Measured in UTF-8 bytes (the format
- * Step Functions uses on the wire) — JS strings count UTF-16 code units,
- * which under-reports for any multi-byte character.
+ * Validate that the serialized Step Functions execution input fits inside the
+ * 256 KiB Standard-workflow cap. Measured in UTF-8 bytes (the format Step
+ * Functions uses on the wire) — JS strings count UTF-16 code units, which
+ * under-reports for any multi-byte character.
*
* Throws {@link InvalidConfigError} with a clear message naming the actual
* byte count, the cap, and a pointer to the "working with large variables"
@@ -214,17 +52,12 @@ export function validateStepFunctionsInputSize(input: unknown): void {
try {
serialized = JSON.stringify(input);
} catch (err) {
- // JSON.stringify throws on circular refs and BigInt. The variables
- // walker catches both inside `config.variables`, but a non-variables
- // field could hit the same case in a future field addition.
throw new InvalidConfigError(
"config",
`Step Functions execution input is not JSON-serializable: ${err instanceof Error ? err.message : String(err)}`,
);
}
if (serialized === undefined) {
- // JSON.stringify returns undefined for non-serializable roots
- // (functions, Symbols at the top level).
throw new InvalidConfigError(
"config",
"Step Functions execution input is not JSON-serializable (JSON.stringify returned undefined). " +
@@ -244,112 +77,3 @@ export function validateStepFunctionsInputSize(input: unknown): void {
);
}
}
-
-/**
- * Validate that `variables` is a plain JSON-safe object — no functions,
- * Symbols, `undefined` leaves, BigInts, non-finite numbers, or non-plain
- * objects (Dates, Maps, Sets, class instances). Rejected values would
- * either round-trip incorrectly through Step Functions (`undefined` is
- * silently dropped by `JSON.stringify`) or throw at the wire boundary
- * (`bigint`), so we surface the offending path synchronously.
- *
- * The check is purely structural — semantic constraints (e.g. "is this
- * variable declared in `data-composition-variables`?") belong to the CLI
- * layer where the project's HTML is on disk.
- */
-export function validateVariablesPayload(value: unknown): void {
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
- throw new InvalidConfigError(
- "config.variables",
- `must be a plain JSON object (got ${describeValue(value)})`,
- );
- }
- walkVariables(value, "config.variables", new WeakSet());
-}
-
-/** Per-typeof rejection messages for JSON-unsafe leaves. */
-const LEAF_REJECTIONS: Partial> = {
- // `JSON.stringify` silently drops `undefined` leaves — caller would never
- // notice their value isn't actually being sent.
- undefined:
- "undefined leaves are silently dropped by JSON.stringify — use null if you mean an absent value",
- function: "functions are not JSON-serializable",
- symbol: "Symbols are not JSON-serializable",
- bigint: "BigInt values throw at JSON.stringify — encode as a string if you need 64-bit integers",
-};
-
-// fallow-ignore-next-line complexity
-function walkVariables(value: unknown, path: string, seen: WeakSet