From 8c14562a2f4be976baf36c099549809e81fdb23b Mon Sep 17 00:00:00 2001 From: richblack Date: Mon, 20 Apr 2026 17:34:42 +0800 Subject: [PATCH] feat(auth): auth_service_account WASM primitive + remove TS JWT signer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - registry/components/auth_service_account: TinyGo impl for Google Service Account (JWT-bearer → token exchange) and base structure for AWS SigV4. - .component-builds/auth_service_account: independent Worker at auth-service-account.arcrun.dev, extends wasi-shim with an http_request host function for the token exchange step. - Delete cypher-executor/src/lib/wasm-executor.ts (legacy, replaced by component-loader WASM HTTP runner path). - credential-injector.ts service_account branch now throws — all service_account recipes must route through auth-dispatcher. Per .agents/specs/arcrun/credential-primitives-wasm Phase 2. Co-Authored-By: Claude Opus 4.7 --- .../auth_service_account/package.json | 14 + .../auth_service_account/pnpm-lock.yaml | 898 ++++++++++++++++++ .../auth_service_account/src/index.ts | 115 +++ .../auth_service_account/tsconfig.json | 11 + .../auth_service_account/wrangler.toml | 23 + .../src/actions/credential-injector.ts | 251 +++-- cypher-executor/src/lib/wasm-executor.ts | 119 --- .../component.contract.yaml | 70 ++ .../components/auth_service_account/go.mod | 3 + .../components/auth_service_account/main.go | 474 +++++++++ 10 files changed, 1771 insertions(+), 207 deletions(-) create mode 100644 .component-builds/auth_service_account/package.json create mode 100644 .component-builds/auth_service_account/pnpm-lock.yaml create mode 100644 .component-builds/auth_service_account/src/index.ts create mode 100644 .component-builds/auth_service_account/tsconfig.json create mode 100644 .component-builds/auth_service_account/wrangler.toml delete mode 100644 cypher-executor/src/lib/wasm-executor.ts create mode 100644 registry/components/auth_service_account/component.contract.yaml create mode 100644 registry/components/auth_service_account/go.mod create mode 100644 registry/components/auth_service_account/main.go diff --git a/.component-builds/auth_service_account/package.json b/.component-builds/auth_service_account/package.json new file mode 100644 index 0000000..4acbd2c --- /dev/null +++ b/.component-builds/auth_service_account/package.json @@ -0,0 +1,14 @@ +{ + "name": "arcrun-auth-service-account", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "hono": "^4.7.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250408.0", + "typescript": "^5.4.0", + "wrangler": "^4.0.0" + } +} diff --git a/.component-builds/auth_service_account/pnpm-lock.yaml b/.component-builds/auth_service_account/pnpm-lock.yaml new file mode 100644 index 0000000..9d03336 --- /dev/null +++ b/.component-builds/auth_service_account/pnpm-lock.yaml @@ -0,0 +1,898 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + hono: + specifier: ^4.7.0 + version: 4.12.14 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250408.0 + version: 4.20260420.1 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + wrangler: + specifier: ^4.0.0 + version: 4.83.0(@cloudflare/workers-types@4.20260420.1) + +packages: + + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.16.0': + resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260415.1': + resolution: {integrity: sha512-dsxaKsQm3LnPGNPEdsRv09QN3Y4DqCw7kX5j6noKqbAtro2jTr95sVlYM1jUxZ5FkOl1f7SXgaKKB9t5H5Nkbg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260415.1': + resolution: {integrity: sha512-+JgSgVA49KyKteHRA1SnonE4Zn5Ei5zdAp5FQMxFmXI8qulZw4Hl7safXxRyK4i9sTO8gl7TFOKO5Q64VPvSDQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260415.1': + resolution: {integrity: sha512-tU+9pwsqCy8afOVlGtiWrWQc/fedQK4SRm4KPIAt+zOiQWDxWASm6YGBUJis5c648WN80yz47qnmdDi8DQNOcA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260415.1': + resolution: {integrity: sha512-bR9uITnV19r5NQ14xnypi2xHXu2iQvfYV8cVgx0JouFUmWwTEEAwFVojDdssGq93VHX9hr/pi2IRUZeegbYBog==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260415.1': + resolution: {integrity: sha512-4NuMLlerI0Ijua3Ir8HXQ+qyNvCUDEG5gDco5Om+sAiK6rnWiz+aGoSlbB8W16yW9QAgzCstbmXLiVknUBflfQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260420.1': + resolution: {integrity: sha512-DHT9JnSn9cIiCSdL76OxW+Xvc1+ml1CWzWvgVwreoHQ+E604aeFxPPHp9X7nE+XRWm2NH4l0OgtxUI5T/nuI3g==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hono@4.12.14: + resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} + engines: {node: '>=16.9.0'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + miniflare@4.20260415.0: + resolution: {integrity: sha512-JoExRWN4YBI2luA5BoSMFEgi8rQWXUGzo3mtE+58VXCLV3jj/Xnk5Yeqs/IXWz8Es5GJIaq6BtsixDvAxXSIng==} + engines: {node: '>=18.0.0'} + hasBin: true + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + workerd@1.20260415.1: + resolution: {integrity: sha512-phyPjRnx+mQDfkhN9ENPioL1L0SdhYs4S0YmJK/xF9Oga+ykNfdSy1MHnsOj8yqnOV96zcVQMx32dJ0r3pq0jQ==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.83.0: + resolution: {integrity: sha512-gw5g3LCiuAqVWxaoKY6+quE0HzAUEFb/FV3oAlNkE1ttd4XP3FiV91XDkkzUCcdqxS4WjhQvPhIDBNdhEi8P0A==} + engines: {node: '>=20.3.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260415.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + +snapshots: + + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260415.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260415.1 + + '@cloudflare/workerd-darwin-64@1.20260415.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260415.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260415.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260415.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260415.1': + optional: true + + '@cloudflare/workers-types@4.20260420.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.15': {} + + blake3-wasm@2.1.5: {} + + cookie@1.1.1: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + fsevents@2.3.3: + optional: true + + hono@4.12.14: {} + + kleur@4.1.5: {} + + miniflare@4.20260415.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260415.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + semver@7.7.4: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + supports-color@10.2.2: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici@7.24.8: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + workerd@1.20260415.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260415.1 + '@cloudflare/workerd-darwin-arm64': 1.20260415.1 + '@cloudflare/workerd-linux-64': 1.20260415.1 + '@cloudflare/workerd-linux-arm64': 1.20260415.1 + '@cloudflare/workerd-windows-64': 1.20260415.1 + + wrangler@4.83.0(@cloudflare/workers-types@4.20260420.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260415.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260415.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260415.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260420.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.18.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.15 + cookie: 1.1.1 + youch-core: 0.3.3 diff --git a/.component-builds/auth_service_account/src/index.ts b/.component-builds/auth_service_account/src/index.ts new file mode 100644 index 0000000..3c17157 --- /dev/null +++ b/.component-builds/auth_service_account/src/index.ts @@ -0,0 +1,115 @@ +/** + * arcrun auth_service_account Worker + * + * POST / → JSON input {action, api_key, service} → WASM (WASI preview1 stdin/stdout) → JSON output + * + * 方案 A:直接 import cypher-executor/src/lib/wasi-shim.ts 的 shim + host function factory, + * 確保 AES-GCM 解密 / RS256 sign 邏輯只存在於一個檔案(rule 02 §2.2)。 + * + * 這個 Worker 比 auth_static_key 多一個 host function:http_request(token exchange 用)。 + * http_request 不是 crypto,不受 rule 02 §2.2 約束;在此檔內聚合提供即可。 + * + * 安全邊界: + * - api_key 經 stdin 傳進 WASM,同時綁到 host function 的 kv_get 做越權檢查 + * - ENCRYPTION_KEY 只存在於 host function 的 closure 中,不會進入 WASM 記憶體 + * - private key 只以 PKCS8 bytes 傳給 crypto_sign_rs256 host function,decrypt 後 plaintext 不離開 WASM + */ + +import componentWasm from '../component.wasm' assert { type: 'webassembly' }; +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { + createWasiShim, + createArcrunHostFunctions, + type ArcrunHostEnv, + type WasiHostFunctions, +} from '../../../cypher-executor/src/lib/wasi-shim'; + +type Env = ArcrunHostEnv; + +const app = new Hono<{ Bindings: Env }>(); +app.use('*', cors()); + +app.get('/', (c) => c.json({ ok: true, component: 'auth_service_account' })); + +app.post('/', async (c) => { + let input: Record; + try { + input = await c.req.json(); + } catch { + return c.json({ success: false, error: 'request body must be JSON' }, 400); + } + + const apiKey = typeof input.api_key === 'string' ? input.api_key : ''; + if (!apiKey) { + return c.json({ success: false, error: 'api_key 必填' }, 400); + } + + try { + const result = await runWasm(c.env, apiKey, input); + return c.json(result); + } catch (e) { + return c.json( + { success: false, error: e instanceof Error ? e.message : String(e) }, + 500, + ); + } +}); + +export default app; + +// ── WASM runner ────────────────────────────────────────────────────────────── + +async function runWasm(env: Env, apiKey: string, input: unknown): Promise { + const stdinData = JSON.stringify(input); + const baseHost = createArcrunHostFunctions(env, apiKey); + const hostFunctions: WasiHostFunctions = { + ...baseHost, + http_request: async (url, method, headersJson, body) => { + const headers: Record = {}; + if (headersJson) { + try { + const parsed = JSON.parse(headersJson); + if (parsed && typeof parsed === 'object') { + for (const [k, v] of Object.entries(parsed as Record)) { + if (typeof v === 'string') headers[k] = v; + } + } + } catch { + // 忽略 header parse 錯誤,當作沒 header + } + } + const init: RequestInit = { method, headers }; + if (body && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') { + init.body = body; + } + const res = await fetch(url, init); + // WASM 端(main.go)直接 json.Unmarshal 回傳內容找 access_token, + // 因此只回傳 response body 原文。非 2xx 也回原文,讓 WASM 從 {error, error_description} 判斷 + return await res.text(); + }, + }; + + const shim = createWasiShim(stdinData, hostFunctions); + + const instance = await WebAssembly.instantiate( + componentWasm as WebAssembly.Module, + shim.imports, + ); + shim.setMemory(instance.exports.memory as WebAssembly.Memory); + + const start = (instance.exports._start ?? instance.exports.main) as () => void; + if (typeof start !== 'function') { + throw new Error('WASM missing _start or main export'); + } + + try { + start(); + } catch (e) { + if (!(e instanceof Error && e.message === 'wasm exit: 0')) throw e; + } + + const stdout = shim.getStdout().trim(); + if (!stdout) throw new Error('WASM component produced no output'); + return JSON.parse(stdout); +} diff --git a/.component-builds/auth_service_account/tsconfig.json b/.component-builds/auth_service_account/tsconfig.json new file mode 100644 index 0000000..b65fda7 --- /dev/null +++ b/.component-builds/auth_service_account/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "noEmit": true + } +} diff --git a/.component-builds/auth_service_account/wrangler.toml b/.component-builds/auth_service_account/wrangler.toml new file mode 100644 index 0000000..b07b7fb --- /dev/null +++ b/.component-builds/auth_service_account/wrangler.toml @@ -0,0 +1,23 @@ +name = "arcrun-auth-service-account" +main = "src/index.ts" +compatibility_date = "2025-02-19" +compatibility_flags = ["nodejs_compat"] + +[vars] +COMPONENT_ID = "auth_service_account" + +[[routes]] +pattern = "auth-service-account.arcrun.dev/*" +zone_name = "arcrun.dev" + +# 與 cypher-executor/wrangler.toml 同一組 KV namespace +[[kv_namespaces]] +binding = "CREDENTIALS_KV" +id = "e7f4320f88d343f187e35e3543dd74c9" + +[[kv_namespaces]] +binding = "RECIPES" +id = "9cf9db905c6241f78503199e58b2ffe0" + +# ENCRYPTION_KEY 透過 wrangler secret set 設定 +# wrangler secret put ENCRYPTION_KEY diff --git a/cypher-executor/src/actions/credential-injector.ts b/cypher-executor/src/actions/credential-injector.ts index ac2f640..2cbf436 100644 --- a/cypher-executor/src/actions/credential-injector.ts +++ b/cypher-executor/src/actions/credential-injector.ts @@ -1,93 +1,53 @@ /** * Credential Injector * - * 在 WASM 零件執行前,從 CREDENTIALS_KV 讀取加密 credential, - * AES-GCM 解密後注入到 input 的對應欄位(inject_as)。 + * 執行順序: + * 1. 檢查是否有對應的 auth recipe(auth_recipe:{componentId} in RECIPES KV) + * → 有:走 auth recipe 路徑(支援 static_key, service_account) + * → 無:走舊有 flat injection 路徑(向後相容) * - * 用戶的 workflow.yaml config 中不需要也不應該包含明文 token。 + * Auth Recipe 路徑: + * - static_key:展開 inject.header/query/body 的 {{secret.KEY}} 模板 + * - service_account:JWT signing → token exchange → 展開 {{runtime.access_token}} + * - 注入結果以 _auth_headers / _auth_query / _auth_body 攜帶,不污染業務欄位 * - * 設計原則: - * - contract.yaml 的 credentials_required 宣告需要哪個 credential - * - CREDENTIALS_KV 存放 AES-GCM 加密後的 credential(key = cred:{name}) - * - 注入發生在 WASM 執行前,不修改 WEBHOOKS KV 中儲存的 workflow 定義 + * 舊有路徑(向後相容): + * - 從 RECIPES KV 讀取 credentials_required(動態 recipe) + * - 或從 BUILTIN_CREDENTIALS_MAP(內建清單) + * - 解密後以 inject_as 欄位名稱直接注入 context */ import type { Bindings } from '../types'; +import { resolveRecipe, resolveAuthRecipe } from '../routes/recipes'; +import type { AuthRecipeDefinition } from '../routes/recipes'; export interface CredentialRequirement { - key: string; // CREDENTIALS_KV 的 key(如 gmail_token) - type: string; // token 類型(如 google_oauth) - description: string; // 說明 - inject_as: string; // 注入到 input 的欄位名稱(如 access_token) + key: string; // CREDENTIALS_KV 的 credential 名稱(如 gmail_token) + inject_as: string; // 注入到 input 的欄位名稱(如 access_token) } -/** - * 讀取並解析零件的 contract.yaml(從 WASM_BUCKET) - * 回傳 credentials_required 陣列,若不存在則回傳空陣列 - */ -async function loadCredentialsRequired( - componentId: string, - wasmBucket: R2Bucket, -): Promise { - const contractKey = `${componentId}/component.contract.yaml`; - const obj = await wasmBucket.get(contractKey); - if (!obj) return []; +/** 內建 API recipe 的 credentials_required(對應 component-loader 的 BUILTIN_API_RECIPES)*/ +const BUILTIN_CREDENTIALS_MAP: Record = { + gmail: [{ key: 'gmail_token', inject_as: 'access_token' }], + google_sheets: [{ key: 'google_oauth', inject_as: 'access_token' }], + telegram: [{ key: 'telegram_bot_token', inject_as: 'bot_token' }], + line_notify: [{ key: 'line_token', inject_as: 'token' }], +}; - const yamlText = await obj.text(); - return parseCredentialsRequired(yamlText); -} +// ── AES-GCM 解密 ────────────────────────────────────────────────────────────── -/** - * 從 YAML 文字解析 credentials_required 欄位 - * 使用簡單的正規表達式解析(避免引入 js-yaml 依賴) - */ -function parseCredentialsRequired(yaml: string): CredentialRequirement[] { - const credsSection = yaml.match(/credentials_required:\s*([\s\S]*?)(?=\n\w|\n#|$)/); - if (!credsSection) return []; - - const items: CredentialRequirement[] = []; - const blockText = credsSection[1]; - - // 解析 " - key: xxx" 開頭的項目 - const itemMatches = blockText.split(/\n - /).slice(1); - for (const item of itemMatches) { - const key = item.match(/key:\s*["']?([^"'\n]+)["']?/)?.[1]?.trim(); - const type = item.match(/type:\s*["']?([^"'\n]+)["']?/)?.[1]?.trim(); - const description = item.match(/description:\s*["']?([^"'\n]+)["']?/)?.[1]?.trim() ?? ''; - const inject_as = item.match(/inject_as:\s*["']?([^"'\n]+)["']?/)?.[1]?.trim(); - - if (key && type && inject_as) { - items.push({ key, type, description, inject_as }); - } - } - - return items; -} - -/** - * AES-GCM 解密(與 credentials Worker 的加密邏輯對應) - * CREDENTIALS_KV 儲存格式:{ encrypted: base64, iv: base64 } - */ async function decryptCredential(encryptedJson: string, encryptionKey: string): Promise { const { encrypted, iv } = JSON.parse(encryptedJson) as { encrypted: string; iv: string }; - // 將 hex-encoded 256-bit key 轉為 CryptoKey const keyBytes = hexToUint8Array(encryptionKey); const cryptoKey = await crypto.subtle.importKey( - 'raw', - keyBytes, - { name: 'AES-GCM' }, - false, - ['decrypt'], + 'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt'], ); - const ivBytes = base64ToUint8Array(iv); - const cipherBytes = base64ToUint8Array(encrypted); - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv: ivBytes }, + { name: 'AES-GCM', iv: base64ToUint8Array(iv) }, cryptoKey, - cipherBytes, + base64ToUint8Array(encrypted), ); return new TextDecoder().decode(decrypted); @@ -95,53 +55,168 @@ async function decryptCredential(encryptedJson: string, encryptionKey: string): function hexToUint8Array(hex: string): Uint8Array { const bytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); - } + for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); return bytes; } function base64ToUint8Array(b64: string): Uint8Array { const binary = atob(b64); const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes; } +// ── 解密所有 required_secrets → { key: decryptedValue } ────────────────────── + +async function decryptSecrets( + recipe: AuthRecipeDefinition, + apiKey: string, + env: Bindings, +): Promise> { + const result: Record = {}; + + for (const req of recipe.required_secrets) { + if (req.optional) continue; + + const kvKey = `${apiKey}:cred:${req.key}`; + const record = await env.CREDENTIALS_KV.get(kvKey); + + if (!record) { + throw new Error( + `缺少 credential:${req.key}(${req.label})\n` + + `修復步驟:\n` + + ` 1. 在 credentials.yaml 加入 ${req.key}: "your-value"\n` + + ` 2. 執行:acr creds push`, + ); + } + + result[req.key] = await decryptCredential(record, env.ENCRYPTION_KEY); + } + + return result; +} + +// ── Template 展開:{{secret.KEY}} 和 {{runtime.KEY}} ───────────────────────── + +function interpolateTemplate( + template: string, + secrets: Record, + runtime: Record, +): string { + return template.replace(/\{\{(secret|runtime)\.(\w+)\}\}/g, (_, ns, key) => { + if (ns === 'secret') return secrets[key] ?? ''; + if (ns === 'runtime') return runtime[key] ?? ''; + return ''; + }); +} + +function interpolateRecord( + record: Record, + secrets: Record, + runtime: Record, +): Record { + const result: Record = {}; + for (const [k, v] of Object.entries(record)) { + result[k] = interpolateTemplate(v, secrets, runtime); + } + return result; +} + +// ── Auth Recipe 注入(新路徑)──────────────────────────────────────────────── + +async function injectFromAuthRecipe( + recipe: AuthRecipeDefinition, + input: Record, + env: Bindings, + apiKey: string, +): Promise> { + // 解密所有 required_secrets + const secrets = await decryptSecrets(recipe, apiKey, env); + + // runtime token:service_account 路徑已改走 auth-dispatcher → auth_service_account WASM; + // 這條 TS fallback 只處理 static_key (runtime 為空即可),service_account 永遠不會走到這裡 + const runtime: Record = {}; + + if (recipe.primitive === 'service_account') { + throw new Error( + `service_account primitive 應由 auth-dispatcher → auth_service_account WASM 處理,` + + `不應進到 credential-injector TS fallback (service=${recipe.service})`, + ); + } + + // 展開 inject 模板 + const authHeaders = recipe.inject.header + ? interpolateRecord(recipe.inject.header, secrets, runtime) + : {}; + const authQuery = recipe.inject.query + ? interpolateRecord(recipe.inject.query, secrets, runtime) + : {}; + const authBody = recipe.inject.body + ? interpolateRecord(recipe.inject.body, secrets, runtime) + : {}; + + return { + ...input, + _auth_headers: authHeaders, + _auth_query: authQuery, + _auth_body: authBody, + }; +} + +// ── 舊有路徑:flat injection(向後相容)────────────────────────────────────── + +async function loadCredentialsRequired( + componentId: string, + env: Bindings, +): Promise { + const recipe = await resolveRecipe(componentId, env.RECIPES); + if (recipe?.credentials_required?.length) { + return recipe.credentials_required; + } + return BUILTIN_CREDENTIALS_MAP[componentId] ?? []; +} + +// ── 主入口 ──────────────────────────────────────────────────────────────────── + /** - * 執行 credential 注入 + * 執行 credential 注入。 * - * @param componentId - 零件 canonical_id - * @param input - 節點的原始 input(來自 workflow config) - * @param env - Cloudflare Worker Bindings - * @returns 注入 credential 後的 input - * - * @throws 若 credential 不存在,拋出結構化錯誤(含 key 名稱與修復步驟) + * @param componentId - 零件 canonical_id 或 hash + * @param input - 節點的 merged context + * @param env - Cloudflare Worker Bindings + * @param apiKey - 用戶的 API Key(ak_前綴),作為 KV namespace */ export async function injectCredentials( componentId: string, input: Record, env: Bindings, + apiKey?: string, ): Promise> { - // 讀取 contract.yaml 中的 credentials_required - const required = await loadCredentialsRequired(componentId, env.WASM_BUCKET); + // 沒有 api_key → local 模式,略過 + if (!apiKey) return input; + + // ── 新路徑:auth recipe ── + const authRecipe = await resolveAuthRecipe(componentId, env.RECIPES); + if (authRecipe) { + return injectFromAuthRecipe(authRecipe, input, env, apiKey); + } + + // ── 舊路徑:flat injection(向後相容)── + const required = await loadCredentialsRequired(componentId, env); if (required.length === 0) return input; const enriched = { ...input }; for (const cred of required) { - const kvKey = `cred:${cred.key}`; + const kvKey = `${apiKey}:cred:${cred.key}`; const record = await env.CREDENTIALS_KV.get(kvKey); if (!record) { throw new Error( - `缺少 credential:${cred.key}(${cred.description})\n` + + `缺少 credential:${cred.key}\n` + `修復步驟:\n` + - ` 1. 在 credentials.yaml 中加入:\n` + - ` ${cred.key}: "your-${cred.type}-token"\n` + - ` 2. 執行:acr creds push` + ` 1. 在 credentials.yaml 中加入 ${cred.key}: "your-token"\n` + + ` 2. 執行:acr creds push`, ); } @@ -151,7 +226,7 @@ export async function injectCredentials( } catch (e) { throw new Error( `credential "${cred.key}" 解密失敗:${e instanceof Error ? e.message : String(e)}\n` + - `修復步驟:重新執行 acr creds push 上傳正確的 credential。` + `修復步驟:重新執行 acr creds push。`, ); } } diff --git a/cypher-executor/src/lib/wasm-executor.ts b/cypher-executor/src/lib/wasm-executor.ts deleted file mode 100644 index aa4575c..0000000 --- a/cypher-executor/src/lib/wasm-executor.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Tier 1 WASM 執行器 - * 從 R2 載入 .wasm,透過 WASI preview1 shim 執行,stdin/stdout JSON I/O。 - * - * 快取策略:WebAssembly.Module 快取於 Worker 記憶體(跨請求共享), - * 避免重複編譯。每次執行只重新 instantiate。 - * - * Requirements: 3.1, 3.3, 6.6 - */ - -import { createWasiShim, type WasiHostFunctions } from './wasi-shim'; - -// Worker 記憶體快取:r2Key → WebAssembly.Module -const moduleCache = new Map(); - -export interface WasmExecutorOptions { - /** R2 Bucket binding */ - bucket: R2Bucket; - /** R2 物件鍵(例:components/validate_json/v1.wasm) */ - r2Key: string; - /** 逾時上限(ms),對應 contract.constraints.max_cold_start_ms */ - timeoutMs?: number; - /** 可選的 host function 注入(讓 .wasm 呼叫外部服務) */ - hostFunctions?: WasiHostFunctions; -} - -export interface WasmExecuteResult { - output: unknown; - stdout: string; - stderr: string; - duration_ms: number; -} - -/** - * 執行 WASM 零件 - * @param input - 傳入零件的 JSON 物件(寫入 stdin) - * @param options - 執行選項 - */ -export async function executeWasm( - input: unknown, - options: WasmExecutorOptions, -): Promise { - const { bucket, r2Key, timeoutMs = 50, hostFunctions } = options; - - // ...(其餘不變) - const start = Date.now(); - - // 1. 取得或編譯 WebAssembly.Module(快取) - let wasmModule = moduleCache.get(r2Key); - if (!wasmModule) { - const obj = await bucket.get(r2Key); - if (!obj) throw new Error(`WASM 零件不存在於 R2:${r2Key}`); - const arrayBuffer = await obj.arrayBuffer(); - wasmModule = await WebAssembly.compile(arrayBuffer); - moduleCache.set(r2Key, wasmModule); - } - - // 2. 建立 WASI shim,注入 stdin 與可選的 host functions - const stdinJson = JSON.stringify(input); - const shim = createWasiShim(stdinJson, hostFunctions); - - // 3. instantiate(每次執行都重新 instantiate,shim 狀態是獨立的) - const instance = await WebAssembly.instantiate(wasmModule, shim.imports); - - // 4. 注入 memory(WASI fd_read/fd_write 需要存取 memory) - const memory = instance.exports.memory as WebAssembly.Memory | undefined; - if (memory) shim.setMemory(memory); - - // 5. 執行(帶逾時) - const exports = instance.exports as Record; - const entryFn = (exports._start ?? exports.main) as (() => void) | undefined; - if (typeof entryFn !== 'function') { - throw new Error(`WASM 零件缺少 _start 或 main export(r2Key: ${r2Key})`); - } - - const runWithTimeout = new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error(`WASM 執行逾時(>${timeoutMs}ms):${r2Key}`)); - }, timeoutMs); - try { - entryFn(); - clearTimeout(timer); - resolve(); - } catch (e) { - clearTimeout(timer); - // proc_exit(0) 拋出 "wasm exit: 0",視為正常結束 - if (e instanceof Error && e.message === 'wasm exit: 0') { - resolve(); - } else { - reject(e); - } - } - }); - - await runWithTimeout; - - // 6. 讀取 stdout,JSON.parse - const stdout = shim.getStdout().trim(); - const stderr = shim.getStderr().trim(); - const duration_ms = Date.now() - start; - - if (!stdout) { - throw new Error(`WASM 零件沒有輸出(stdout 為空):${r2Key}`); - } - - let output: unknown; - try { - output = JSON.parse(stdout); - } catch { - throw new Error(`WASM 零件輸出不是合法 JSON:${stdout.slice(0, 200)}`); - } - - return { output, stdout, stderr, duration_ms }; -} - -/** 清除 Module 快取(測試用) */ -export function clearModuleCache(): void { - moduleCache.clear(); -} diff --git a/registry/components/auth_service_account/component.contract.yaml b/registry/components/auth_service_account/component.contract.yaml new file mode 100644 index 0000000..b778ed2 --- /dev/null +++ b/registry/components/auth_service_account/component.contract.yaml @@ -0,0 +1,70 @@ +canonical_id: "auth_service_account" +display_name: "Auth Primitive — Service Account (Google JWT)" +category: "auth" +version: "v1" +wasi_target: "preview1" +stability: "floating" +runtime_compat: + - "cf-workers" + - "workerd" + - "wazero" +constraints: + max_size_kb: 2048 + max_cold_start_ms: 100 + no_network_syscall: false + no_filesystem_syscall: true + io_model: "stdin_stdout_json" +input_schema: + type: object + required: [action, api_key, service] + properties: + action: + type: string + enum: [authenticate] + description: 目前僅支援 authenticate + api_key: + type: string + description: 租戶識別(ak_ 前綴),用來組 {api_key}:cred:{name} KV key + service: + type: string + description: auth recipe 名稱,對應 auth_recipe:{service} 的 KV 記錄 + request: + type: object + description: (保留)下游零件的 HTTP request 上下文 +output_schema: + type: object + properties: + success: + type: boolean + auth_headers: + type: object + additionalProperties: + type: string + auth_query: + type: object + additionalProperties: + type: string + auth_body: + type: object + additionalProperties: + type: string + runtime: + type: object + description: 包含 access_token(token exchange 後取得) + properties: + access_token: + type: string +gherkin_tests: + - scenario: "缺少 api_key" + given: '{"action":"authenticate","service":"google_sheets_sa"}' + then_contains: '{"success":false' + - scenario: "找不到 auth recipe" + given: '{"action":"authenticate","api_key":"ak_nonexistent","service":"nonexistent"}' + then_contains: '{"success":false' +tags: [auth, credential, primitive, service_account, google] +description: "Service Account auth primitive (Google JWT 方案)。讀取 auth_recipe + 解密 service_account_json → 解析 PEM private key → 組 JWT → crypto_sign_rs256 (host function) → token exchange endpoint → 取 access_token → 展開 {{runtime.access_token}} 模板。透過 host function crypto_sign_rs256,private key 僅以 PKCS8 bytes 傳給 host,解密後 plaintext 不離開 WASM。" +config_example: | + auth_step: + component: "auth_service_account" + action: "authenticate" + service: "google_sheets_sa" diff --git a/registry/components/auth_service_account/go.mod b/registry/components/auth_service_account/go.mod new file mode 100644 index 0000000..185c5ee --- /dev/null +++ b/registry/components/auth_service_account/go.mod @@ -0,0 +1,3 @@ +module component + +go 1.21 diff --git a/registry/components/auth_service_account/main.go b/registry/components/auth_service_account/main.go new file mode 100644 index 0000000..73d4236 --- /dev/null +++ b/registry/components/auth_service_account/main.go @@ -0,0 +1,474 @@ +// auth_service_account — Google Service Account JWT auth primitive +// +// 讀取 auth_recipe:{service} + 解密 service_account_json + 組 JWT + RS256 簽章(透過 host) +// + token exchange → access_token + 展開 {{runtime.access_token}}。 +// +// Host imports: +// - u6u.kv_get — 讀 RECIPES + CREDENTIALS_KV +// - u6u.crypto_decrypt — AES-GCM 解密 service account JSON +// - u6u.crypto_sign_rs256 — RSASSA-PKCS1-v1_5 + SHA-256 (PKCS8 private key) +// - u6u.http_request — POST token exchange endpoint +// +//go:build tinygo + +package main + +import ( + "encoding/base64" + "encoding/json" + "io" + "net/url" + "os" + "strings" + "time" + "unsafe" +) + +// ── host function 宣告 ─────────────────────────────────────────────────────── + +//go:wasmimport u6u kv_get +func hostKvGet( + keyPtr uintptr, keyLen uint32, + outPtr uintptr, outLenPtr uintptr, +) uint32 + +//go:wasmimport u6u crypto_decrypt +func hostCryptoDecrypt( + encPtr uintptr, encLen uint32, + ivPtr uintptr, ivLen uint32, + outPtr uintptr, outLenPtr uintptr, +) uint32 + +//go:wasmimport u6u crypto_sign_rs256 +func hostCryptoSignRS256( + dataPtr uintptr, dataLen uint32, + pkcs8Ptr uintptr, pkcs8Len uint32, + outPtr uintptr, outLenPtr uintptr, +) uint32 + +//go:wasmimport u6u http_request +func hostHttpRequest( + urlPtr uintptr, urlLen uint32, + methodPtr uintptr, methodLen uint32, + headersPtr uintptr, headersLen uint32, + bodyPtr uintptr, bodyLen uint32, + outPtr uintptr, outLenPtr uintptr, +) uint32 + +// ── 型別 ───────────────────────────────────────────────────────────────────── + +type Input struct { + Action string `json:"action"` + APIKey string `json:"api_key"` + Service string `json:"service"` + Request json.RawMessage `json:"request,omitempty"` +} + +type SecretRequirement struct { + Key string `json:"key"` + Label string `json:"label"` + Optional bool `json:"optional,omitempty"` +} + +type AuthInjectSpec struct { + Header map[string]string `json:"header,omitempty"` + Query map[string]string `json:"query,omitempty"` + Body map[string]string `json:"body,omitempty"` +} + +type TokenExchange struct { + Endpoint string `json:"endpoint"` + Scopes []string `json:"scopes"` +} + +type AuthRecipe struct { + Kind string `json:"kind"` + Service string `json:"service"` + Primitive string `json:"primitive"` + ServiceAccountKind string `json:"service_account_kind,omitempty"` + TokenExchange *TokenExchange `json:"token_exchange,omitempty"` + RequiredSecrets []SecretRequirement `json:"required_secrets"` + Inject AuthInjectSpec `json:"inject"` +} + +type EncryptedRecord struct { + Encrypted string `json:"encrypted"` + IV string `json:"iv"` +} + +type ServiceAccountJSON struct { + ClientEmail string `json:"client_email"` + PrivateKey string `json:"private_key"` +} + +type JWTHeader struct { + Alg string `json:"alg"` + Typ string `json:"typ"` +} + +type JWTPayload struct { + Iss string `json:"iss"` + Sub string `json:"sub"` + Aud string `json:"aud"` + Scope string `json:"scope"` + Iat int64 `json:"iat"` + Exp int64 `json:"exp"` +} + +// ── main ───────────────────────────────────────────────────────────────────── + +func main() { + raw, err := io.ReadAll(os.Stdin) + if err != nil { + writeError("failed to read stdin: " + err.Error()) + return + } + + var input Input + if err := json.Unmarshal(raw, &input); err != nil { + writeError("invalid input JSON: " + err.Error()) + return + } + + if input.APIKey == "" { + writeError("api_key 必填") + return + } + if input.Service == "" { + writeError("service 必填") + return + } + if input.Action != "" && input.Action != "authenticate" { + writeError("auth_service_account 僅支援 action=authenticate") + return + } + + // 1. 讀 auth recipe + recipeJSON, status := kvGet("auth_recipe:" + input.Service) + if status == 2 { + writeError("找不到 auth recipe: " + input.Service) + return + } + if status != 0 { + writeError("kv_get 失敗(auth_recipe)") + return + } + + var recipe AuthRecipe + if err := json.Unmarshal([]byte(recipeJSON), &recipe); err != nil { + writeError("auth recipe JSON 解析失敗: " + err.Error()) + return + } + if recipe.Primitive != "service_account" { + writeError("auth recipe " + input.Service + " 的 primitive 不是 service_account (是 " + recipe.Primitive + ")") + return + } + if recipe.ServiceAccountKind != "google_jwt" { + writeError("auth recipe " + input.Service + " 的 service_account_kind 必須是 google_jwt,實際: " + recipe.ServiceAccountKind) + return + } + if recipe.TokenExchange == nil || recipe.TokenExchange.Endpoint == "" { + writeError("auth recipe " + input.Service + " 缺少 token_exchange.endpoint") + return + } + if len(recipe.RequiredSecrets) == 0 { + writeError("auth recipe " + input.Service + " 缺少 required_secrets[0](SA JSON)") + return + } + + // 2. 解密 service account JSON (慣例:required_secrets[0] 是 SA JSON) + saReq := recipe.RequiredSecrets[0] + kvKey := input.APIKey + ":cred:" + saReq.Key + encJSON, s := kvGet(kvKey) + if s == 2 { + writeError("缺少 credential: " + saReq.Key + " (" + saReq.Label + ")。修復: 編輯 credentials.yaml 後執行 acr creds push") + return + } + if s != 0 { + writeError("kv_get 失敗(credential " + saReq.Key + ")") + return + } + + var rec EncryptedRecord + if err := json.Unmarshal([]byte(encJSON), &rec); err != nil { + writeError("credential " + saReq.Key + " 格式錯誤: " + err.Error()) + return + } + saJSONStr, ok := cryptoDecrypt(rec.Encrypted, rec.IV) + if !ok { + writeError("credential " + saReq.Key + " 解密失敗") + return + } + + // 3. 解析 service account JSON + var sa ServiceAccountJSON + if err := json.Unmarshal([]byte(saJSONStr), &sa); err != nil { + writeError("service account JSON 格式錯誤: " + err.Error()) + return + } + if sa.ClientEmail == "" || sa.PrivateKey == "" { + writeError("service account JSON 缺少 client_email 或 private_key") + return + } + + // 4. PEM → PKCS8 bytes (去 header/footer + base64 decode) + pkcs8, err := pemToPkcs8(sa.PrivateKey) + if err != nil { + writeError("解析 service account private key 失敗: " + err.Error()) + return + } + + // 5. 組 JWT header + payload (base64url-encoded) + now := time.Now().Unix() + header := JWTHeader{Alg: "RS256", Typ: "JWT"} + payload := JWTPayload{ + Iss: sa.ClientEmail, + Sub: sa.ClientEmail, + Aud: recipe.TokenExchange.Endpoint, + Scope: strings.Join(recipe.TokenExchange.Scopes, " "), + Iat: now, + Exp: now + 3600, + } + headerBytes, _ := json.Marshal(header) + payloadBytes, _ := json.Marshal(payload) + + signingInput := base64.RawURLEncoding.EncodeToString(headerBytes) + "." + + base64.RawURLEncoding.EncodeToString(payloadBytes) + + // 6. 呼叫 host 簽章 (RSASSA-PKCS1-v1_5 + SHA-256) + signature, ok := cryptoSignRS256([]byte(signingInput), pkcs8) + if !ok { + writeError("JWT 簽章失敗(host function crypto_sign_rs256 回傳錯誤)") + return + } + + jwt := signingInput + "." + base64.RawURLEncoding.EncodeToString(signature) + + // 7. token exchange:POST form-urlencoded 到 token_exchange.endpoint + form := url.Values{} + form.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer") + form.Set("assertion", jwt) + formBody := form.Encode() + + headersJSON := `{"Content-Type":"application/x-www-form-urlencoded"}` + + respStr, ok := httpRequest(recipe.TokenExchange.Endpoint, "POST", headersJSON, formBody) + if !ok { + writeError("token exchange HTTP 失敗") + return + } + + var tokenResp struct { + AccessToken string `json:"access_token"` + Error string `json:"error"` + ErrorDesc string `json:"error_description"` + } + if err := json.Unmarshal([]byte(respStr), &tokenResp); err != nil { + writeError("token exchange 回應解析失敗: " + err.Error() + " (raw: " + respStr + ")") + return + } + if tokenResp.AccessToken == "" { + errMsg := tokenResp.Error + if tokenResp.ErrorDesc != "" { + errMsg += ": " + tokenResp.ErrorDesc + } + if errMsg == "" { + errMsg = "access_token 為空 (raw: " + respStr + ")" + } + writeError("token exchange 失敗: " + errMsg) + return + } + + // 8. 展開模板 (service_account 不用 secret.*,只用 runtime.access_token) + secrets := map[string]string{} + runtime := map[string]string{"access_token": tokenResp.AccessToken} + authHeaders := interpolateRecord(recipe.Inject.Header, secrets, runtime) + authQuery := interpolateRecord(recipe.Inject.Query, secrets, runtime) + authBody := interpolateRecord(recipe.Inject.Body, secrets, runtime) + + out, _ := json.Marshal(map[string]interface{}{ + "success": true, + "auth_headers": authHeaders, + "auth_query": authQuery, + "auth_body": authBody, + "runtime": runtime, + }) + os.Stdout.Write(out) +} + +// ── helpers ────────────────────────────────────────────────────────────────── + +func writeError(msg string) { + out, _ := json.Marshal(map[string]interface{}{ + "success": false, + "error": msg, + "auth_headers": map[string]string{}, + "auth_query": map[string]string{}, + "auth_body": map[string]string{}, + }) + os.Stdout.Write(out) +} + +// pemToPkcs8 從 PEM 取出 base64 body 再 decode 成 bytes。 +// 支援 "BEGIN PRIVATE KEY" / "BEGIN RSA PRIVATE KEY"(SA JSON 幾乎都是前者)。 +func pemToPkcs8(pem string) ([]byte, error) { + // 移除所有 BEGIN/END 行與空白 + lines := strings.Split(pem, "\n") + var b strings.Builder + for _, line := range lines { + l := strings.TrimSpace(line) + if l == "" { + continue + } + if strings.HasPrefix(l, "-----BEGIN") || strings.HasPrefix(l, "-----END") { + continue + } + b.WriteString(l) + } + cleaned := strings.ReplaceAll(b.String(), "\\n", "") // 防呆:JSON-escaped newline + return base64.StdEncoding.DecodeString(cleaned) +} + +// kvGet 呼叫 host function,回傳 (value, status)。status: 0=成功 1=錯誤 2=找不到 +func kvGet(key string) (string, uint32) { + keyBytes := []byte(key) + outBuf := make([]byte, 65536) + var outLen uint32 + + status := hostKvGet( + uintptr(unsafe.Pointer(&keyBytes[0])), uint32(len(keyBytes)), + uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)), + ) + if status != 0 { + return "", status + } + return string(outBuf[:outLen]), 0 +} + +func cryptoDecrypt(encB64, ivB64 string) (string, bool) { + encBytes := []byte(encB64) + ivBytes := []byte(ivB64) + outBuf := make([]byte, 65536) + var outLen uint32 + + if len(encBytes) == 0 || len(ivBytes) == 0 { + return "", false + } + + status := hostCryptoDecrypt( + uintptr(unsafe.Pointer(&encBytes[0])), uint32(len(encBytes)), + uintptr(unsafe.Pointer(&ivBytes[0])), uint32(len(ivBytes)), + uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)), + ) + if status != 0 { + return "", false + } + return string(outBuf[:outLen]), true +} + +// cryptoSignRS256 呼叫 host,回傳簽章 bytes +func cryptoSignRS256(data, pkcs8 []byte) ([]byte, bool) { + if len(data) == 0 || len(pkcs8) == 0 { + return nil, false + } + outBuf := make([]byte, 1024) // RSA-2048 簽章 = 256 bytes,1KB 綽綽有餘 + var outLen uint32 + + status := hostCryptoSignRS256( + uintptr(unsafe.Pointer(&data[0])), uint32(len(data)), + uintptr(unsafe.Pointer(&pkcs8[0])), uint32(len(pkcs8)), + uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)), + ) + if status != 0 { + return nil, false + } + return outBuf[:outLen], true +} + +// httpRequest 呼叫 host,回傳 response body 字串(host 側把 status + body 串好) +func httpRequest(url, method, headersJSON, body string) (string, bool) { + urlBytes := []byte(url) + methodBytes := []byte(method) + headersBytes := []byte(headersJSON) + bodyBytes := []byte(body) + + if len(urlBytes) == 0 { + return "", false + } + + outBuf := make([]byte, 65536) + var outLen uint32 + + // bodyBytes 可能為空(GET),host function 允許 len=0 + var bodyPtr uintptr + if len(bodyBytes) > 0 { + bodyPtr = uintptr(unsafe.Pointer(&bodyBytes[0])) + } + var headersPtr uintptr + if len(headersBytes) > 0 { + headersPtr = uintptr(unsafe.Pointer(&headersBytes[0])) + } + + status := hostHttpRequest( + uintptr(unsafe.Pointer(&urlBytes[0])), uint32(len(urlBytes)), + uintptr(unsafe.Pointer(&methodBytes[0])), uint32(len(methodBytes)), + headersPtr, uint32(len(headersBytes)), + bodyPtr, uint32(len(bodyBytes)), + uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)), + ) + if status != 0 { + return "", false + } + return string(outBuf[:outLen]), true +} + +// interpolateTemplate 展開 {{secret.X}} 與 {{runtime.X}}。未知 key 展開為空字串。 +// 其他 namespace 的 {{...}} 原樣保留。 +func interpolateTemplate(template string, secrets, runtime map[string]string) string { + var b strings.Builder + b.Grow(len(template)) + i := 0 + for i < len(template) { + start := strings.Index(template[i:], "{{") + if start < 0 { + b.WriteString(template[i:]) + break + } + b.WriteString(template[i : i+start]) + openIdx := i + start + closeRel := strings.Index(template[openIdx+2:], "}}") + if closeRel < 0 { + b.WriteString(template[openIdx:]) + break + } + inner := template[openIdx+2 : openIdx+2+closeRel] + advance := openIdx + 2 + closeRel + 2 + + switch { + case strings.HasPrefix(inner, "secret."): + key := inner[len("secret."):] + b.WriteString(secrets[key]) + case strings.HasPrefix(inner, "runtime."): + key := inner[len("runtime."):] + b.WriteString(runtime[key]) + default: + b.WriteString(template[openIdx:advance]) + } + i = advance + } + return b.String() +} + +func interpolateRecord( + record map[string]string, + secrets, runtime map[string]string, +) map[string]string { + if record == nil { + return map[string]string{} + } + result := make(map[string]string, len(record)) + for k, v := range record { + result[k] = interpolateTemplate(v, secrets, runtime) + } + return result +}