diff --git a/.changeset/gcp-kms-request-signing.md b/.changeset/gcp-kms-request-signing.md new file mode 100644 index 0000000000..8157ce80f3 --- /dev/null +++ b/.changeset/gcp-kms-request-signing.md @@ -0,0 +1,21 @@ +--- +--- + +feat(addie): GCP KMS-backed Ed25519 signing for outbound AdCP requests + +Addie's outbound AdCP calls (`AdCPClient.executeTask`) now attach an RFC 9421 +request-signing block backed by a GCP KMS Ed25519 key when `GCP_SA_JSON` and +`GCP_KMS_KEY_VERSION` are set. Private key material never enters process +memory; signing routes through the `SigningProvider` interface added in +`@adcp/client@5.20.0`. + +Verifiers fetch the public key from `${BASE_URL}/.well-known/jwks.json` +(kid: `aao-signing-2026-04`). The committed `expected-public-key.ts` is the +single source of truth for both the published JWKS and the boot-time +tripwire that asserts the KMS-returned public key matches the repo — +silent key swaps in GCP fail loudly rather than producing signatures +verifiers reject. + +Webhook signing in the training agent still uses an in-process JWK; lifting +that to KMS is a follow-up that needs a `SigningProvider` integration in +`@adcp/client`'s `createWebhookEmitter`. diff --git a/.changeset/revert-kms-eager-init.md b/.changeset/revert-kms-eager-init.md new file mode 100644 index 0000000000..3bc3e2e04c --- /dev/null +++ b/.changeset/revert-kms-eager-init.md @@ -0,0 +1,17 @@ +--- +--- + +fix(boot): drop GCP KMS eager-init at boot + +The eager-init added in #3283 took down the prod deploy: when KMS auth is +misconfigured, the gRPC client retries forever inside `getPublicKey`, the app +never binds port 8080, and Fly's health-check times out without surfacing the +underlying KMS error. + +Lazy init in `getGcpKmsSigningProvider()` is the safer default — a broken +signing path fails per-call (logged + generic message to LLM via the +call-site try/catch in `adcp-tools.ts`) while the rest of the server boots +normally so operators can SSH in and inspect. + +Re-enabling eager init needs either a hard timeout on the `getPublicKey` +round-trip or a deploy-time probe outside the boot critical path. diff --git a/package-lock.json b/package-lock.json index 7adc46039b..6c1331a081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,11 @@ "name": "adcontextprotocol", "version": "3.0.0", "dependencies": { - "@adcp/client": "5.19.0", + "@adcp/client": "5.20.0", "@anthropic-ai/sdk": "^0.91.1", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.5.4", + "@google-cloud/kms": "^5.4.0", "@google/generative-ai": "^0.24.1", "@modelcontextprotocol/sdk": "^1.28.0", "@mozilla/readability": "^0.6.0", @@ -120,9 +121,9 @@ } }, "node_modules/@adcp/client": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@adcp/client/-/client-5.19.0.tgz", - "integrity": "sha512-u+5jGZDcqOVK3aAvqIfH2GSj6+GnCmya9F1qTXpnvE8vURp5Kfh9ixvMTc7IGEw6ybYXCyhapvdWATL9DtMfSA==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@adcp/client/-/client-5.20.0.tgz", + "integrity": "sha512-AVZkWtO+G1Q76QT5Fe4JNNxNpGWZCl1hkC0Giu19VAfaPAeFLbgnmvpxf/tchM6HSeLPA6oFlvTk8N4CtgN4QA==", "license": "Apache-2.0", "dependencies": { "ajv": "^8.18.0", @@ -1463,6 +1464,18 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@google-cloud/kms": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-5.4.0.tgz", + "integrity": "sha512-+06zUCaJM+wyZISM3F6u/jSqoBs0iZ8Aj9rqOJFePoWkNN7FbR4mQpV7okGHA+Y7caVgq+4QtIDKiFd17SZT+A==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@google/generative-ai": { "version": "0.24.1", "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", @@ -1472,6 +1485,61 @@ "node": ">=18.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@hono/node-server": { "version": "1.19.13", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", @@ -2299,6 +2367,102 @@ } } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2348,6 +2512,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@jsep-plugin/assignment": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", @@ -5532,6 +5706,16 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@posthog/core": { "version": "1.27.5", "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.27.5.tgz", @@ -9156,7 +9340,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -10433,7 +10616,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -10448,14 +10630,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10465,7 +10645,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10480,7 +10659,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -10555,7 +10733,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -10568,7 +10745,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/color-string": { @@ -11460,6 +11636,38 @@ "readable-stream": "^2.0.2" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -11913,7 +12121,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13051,7 +13258,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -13375,7 +13581,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -13603,6 +13808,140 @@ "node": ">=18" } }, + "node_modules/google-gax": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", + "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "^10.1.0", + "google-logging-utils": "^1.1.1", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^3.0.0", + "protobufjs": "^7.5.3", + "retry-request": "^8.0.0", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/google-gax/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/google-gax/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/google-gax/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/google-logging-utils": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", @@ -14305,7 +14644,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -15478,6 +15816,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -16134,6 +16487,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -17627,7 +17986,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -18220,7 +18578,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -18640,6 +18997,12 @@ "node": ">= 14" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/package-manager-detector": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", @@ -19465,6 +19828,42 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/protobufjs": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.1.tgz", @@ -20485,7 +20884,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20707,6 +21105,19 @@ "node": ">= 4" } }, + "node_modules/retry-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.2.tgz", + "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/rettime": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.7.tgz", @@ -21423,7 +21834,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -21845,6 +22255,21 @@ "node": ">= 0.4" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -21905,6 +22330,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -22020,6 +22475,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -22085,6 +22553,12 @@ "npm": ">=6" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -22415,6 +22889,21 @@ "node": ">=8" } }, + "node_modules/teeny-request": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.2.tgz", + "integrity": "sha512-Xj0ZAQ0CeuQn6UxCDPLbFRlgcSTUEyO3+wiepr2grjIjyL/lMMs1Z4OwXn8kLvn/V1OuaEP0UY7Na6UDNNsYrQ==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -23944,6 +24433,53 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -24088,7 +24624,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -24120,7 +24655,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -24139,7 +24673,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -24149,14 +24682,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -24166,7 +24697,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", diff --git a/package.json b/package.json index d446fe3922..c22695862c 100644 --- a/package.json +++ b/package.json @@ -71,10 +71,11 @@ "check:images": "bash scripts/check-image-quality.sh" }, "dependencies": { - "@adcp/client": "5.19.0", + "@adcp/client": "5.20.0", "@anthropic-ai/sdk": "^0.91.1", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.5.4", + "@google-cloud/kms": "^5.4.0", "@google/generative-ai": "^0.24.1", "@modelcontextprotocol/sdk": "^1.28.0", "@mozilla/readability": "^0.6.0", diff --git a/server/src/addie/mcp/adcp-tools.ts b/server/src/addie/mcp/adcp-tools.ts index d64710c2a9..0fcec1844a 100644 --- a/server/src/addie/mcp/adcp-tools.ts +++ b/server/src/addie/mcp/adcp-tools.ts @@ -679,6 +679,25 @@ export function createAdcpToolHandlers( try { const { AdCPClient } = await import('@adcp/client'); + const { getGcpKmsSigningProvider } = await import('../../security/gcp-kms-signer.js'); + + // Sign outbound AdCP requests with the GCP KMS-backed Ed25519 key + // when configured. Verifiers fetch the public key from + // `${BASE_URL}/.well-known/jwks.json` (kid: aao-signing-2026-04). + // + // Init failures (KMS unreachable, wrong algorithm, tripwire mismatch, + // bad SA JSON) are fail-closed: structured-log the full error for + // operators, surface a generic message to the LLM. KMS error chains + // include the project ID, IAM principal email, and resource paths; + // those don't belong in the model's context window or in the tool + // result rendered to the end user. + let signingProvider; + try { + signingProvider = await getGcpKmsSigningProvider(); + } catch (kmsErr) { + logger.error({ err: kmsErr, agentUrl, task }, 'GCP KMS signing provider init failed'); + return '**Error:** Outbound AdCP signing is misconfigured. Operator: check structured logs for KMS init failure (gcp-kms-signer module).'; + } const agentConfig = { id: 'target', @@ -688,6 +707,15 @@ export function createAdcpToolHandlers( ...(authInfo?.authType === 'basic' ? { headers: { 'Authorization': `Basic ${authInfo.token}` } } : authInfo ? { auth_token: authInfo.token } : {}), + ...(signingProvider + ? { + request_signing: { + kind: 'provider' as const, + provider: signingProvider, + agent_url: getBaseUrl(), + }, + } + : {}), }; const multiClient = new AdCPClient( diff --git a/server/src/billing/stripe-client.ts b/server/src/billing/stripe-client.ts index 11a99da9f6..3c263019e5 100644 --- a/server/src/billing/stripe-client.ts +++ b/server/src/billing/stripe-client.ts @@ -14,7 +14,7 @@ if (!STRIPE_SECRET_KEY) { export const stripe = STRIPE_SECRET_KEY ? new Stripe(STRIPE_SECRET_KEY, { - apiVersion: '2026-03-25.dahlia', + apiVersion: '2026-04-22.dahlia', }) : null; diff --git a/server/src/http.ts b/server/src/http.ts index 8a8f2871f3..6d7d416b89 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -16,6 +16,7 @@ import { notifySystemError } from "./addie/error-notifier.js"; import { CrawlerService } from "./crawler.js"; import { createLogger, processRole } from "./logger.js"; import { CapabilityDiscovery } from "./capabilities.js"; +import { getPublicSigningJwks } from "./security/jwks.js"; import { PublisherTracker } from "./publishers.js"; import { PropertiesService } from "./properties.js"; import { AdAgentsManager } from "./adagents-manager.js"; @@ -650,6 +651,14 @@ export class HTTPServer { res.redirect(302, '/openapi/registry.yaml'); }); + // RFC 7517 JWKS publishing Addie's request-signing public key. Verifiers + // (sellers receiving signed AdCP requests from Addie) fetch this to + // resolve the `kid` carried in `Signature-Input`. + this.app.get('/.well-known/jwks.json', (_req, res) => { + res.setHeader('Cache-Control', 'public, max-age=3600'); + res.json(getPublicSigningJwks()); + }); + // RFC 9728 protected-resource metadata for the REST API. Points at the same // OAuth 2.1 authorization server that the MCP endpoint uses, so a single // SSO'd token issued via mcpAuthRouter works against /api/* too. diff --git a/server/src/index.ts b/server/src/index.ts index 210ccae09d..37b532d788 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -25,6 +25,12 @@ async function main() { } } + // GCP KMS signing provider initializes lazily on first signed AdCP call + // (see security/gcp-kms-signer.ts). Eager-init at boot was tried and + // pulled — when KMS auth is misconfigured, the gRPC client retries + // forever and the app never binds port 8080, taking down the whole + // deploy instead of just the signing path. Lazy is the safer default. + // Start HTTP server first, then initialize Addie in the background. // initializeAddieBolt uses execSync (git clone) which blocks the event loop, // so it must not run before the server starts listening. diff --git a/server/src/security/expected-public-key.ts b/server/src/security/expected-public-key.ts new file mode 100644 index 0000000000..f7c2431058 --- /dev/null +++ b/server/src/security/expected-public-key.ts @@ -0,0 +1,27 @@ +/** + * The Ed25519 public key the GCP KMS signer is expected to return, plus + * the wire identity (`kid`, `alg`) it's published under. + * + * Committed to the repo as a tripwire: if KMS returns a different key + * (rotation, IAM swap, hostile substitution), the signer init fails loudly + * rather than emitting signatures verifiers — fetching the JWKS published + * from this same constant — would reject. + * + * On rotation: generate the new version in GCP, update `EXPECTED_PUBLIC_KEY_PEM` + * and `KID`, set `GCP_KMS_KEY_VERSION` to the new version path, deploy. + * + * Key version pinned by `GCP_KMS_KEY_VERSION` secret. Current logical key: + * projects/adcp-production/locations/us-east4/keyRings/aao_signing/cryptoKeys/addie_request_signing/cryptoKeyVersions/1 + * + * `KID` is the wire identifier published in `Signature-Input`'s `keyid` + * parameter and at `${BASE_URL}/.well-known/jwks.json`. The date-suffix + * pattern lets a future rotation publish both old and new under different + * `kid` values during the cutover window. + */ +export const EXPECTED_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEASRYr8eSvjkZF6dAUquI1sKuU4YGZkoGH+2jwkz4dRJg= +-----END PUBLIC KEY----- +`; + +export const KID = 'aao-signing-2026-04'; +export const ALGORITHM = 'ed25519' as const; diff --git a/server/src/security/gcp-kms-signer.ts b/server/src/security/gcp-kms-signer.ts new file mode 100644 index 0000000000..e8aa31d791 --- /dev/null +++ b/server/src/security/gcp-kms-signer.ts @@ -0,0 +1,194 @@ +/** + * GCP KMS-backed RFC 9421 SigningProvider for Addie's outbound AdCP requests. + * + * Reads two Fly secrets: + * - GCP_SA_JSON: service-account credentials JSON (IAM identity) + * - GCP_KMS_KEY_VERSION: full resource name + * `projects/.../keyRings/.../cryptoKeys/.../cryptoKeyVersions/N` + * + * On first call, builds a `KeyManagementServiceClient`, fetches the public + * key, asserts it's Ed25519, and asserts it matches `EXPECTED_PUBLIC_KEY_PEM`. + * Mismatch fails loudly — tripwire against an out-of-band key swap in GCP + * that would otherwise silently re-sign with an unexpected key. + * + * Singleton — one provider per process. The KMS client is fetched lazily so + * boot doesn't fail in dev where the secrets aren't set; production callers + * can opt into eager init via `eagerInitGcpKmsSigningProvider()`. + */ + +import { createPublicKey } from 'node:crypto'; +import { KeyManagementServiceClient } from '@google-cloud/kms'; +import type { SigningProvider } from '@adcp/client/signing'; +import { createLogger } from '../logger.js'; +import { EXPECTED_PUBLIC_KEY_PEM, KID, ALGORITHM } from './expected-public-key.js'; + +const logger = createLogger('gcp-kms-signer'); + +const KMS_ALG_ED25519 = 'EC_SIGN_ED25519'; + +let cached: SigningProvider | null = null; +let initInFlight: Promise | null = null; + +/** + * Returns the GCP KMS-backed signing provider, or null if env vars are + * unset (dev / non-signing deployments). Throws if env is set but + * misconfigured — fail-fast so a half-configured production deploy + * doesn't silently fall through to unsigned requests. + * + * Only successful init is cached. Transient KMS errors (network blip + * during `getPublicKey`) are retried on the next call rather than + * permanently sticking the process. Concurrent callers share one + * in-flight init promise so a thundering herd doesn't fan out into + * many `getPublicKey` calls. + */ +export async function getGcpKmsSigningProvider(): Promise { + if (cached) return cached; + + const saJson = process.env.GCP_SA_JSON; + const keyVersion = process.env.GCP_KMS_KEY_VERSION; + + if (!saJson && !keyVersion) { + return null; + } + if (!saJson || !keyVersion) { + throw new Error( + 'GCP KMS signing partially configured: both GCP_SA_JSON and GCP_KMS_KEY_VERSION must be set, or neither.' + ); + } + + if (!initInFlight) { + // Set `cached` inside the IIFE *before* the .finally clears + // initInFlight. If a third caller arrives between the .finally + // microtask and the outer await resume, they'll see `cached` already + // populated and skip init entirely. + initInFlight = (async () => { + const credentials = parseServiceAccountJson(saJson); + const client = new KeyManagementServiceClient({ credentials }); + const provider = await buildProvider(client, keyVersion); + cached = provider; + logger.info( + { kid: KID, algorithm: ALGORITHM, keyVersion: redactKeyVersion(keyVersion) }, + 'GCP KMS signing provider initialized' + ); + return provider; + })().finally(() => { + initInFlight = null; + }); + } + + return initInFlight; +} + +/** + * Boot-path eager init. Call from the server startup if KMS env is set so + * deploy fails before traffic is taken (rather than every tool-call + * failing post-rollout). Silent no-op when env is unset. + */ +export async function eagerInitGcpKmsSigningProvider(): Promise { + if (!process.env.GCP_SA_JSON && !process.env.GCP_KMS_KEY_VERSION) return; + await getGcpKmsSigningProvider(); +} + +interface ServiceAccountCredentials { + client_email: string; + private_key: string; +} + +function parseServiceAccountJson(raw: string): ServiceAccountCredentials { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + // Don't include the parser detail — the offset/character it cites can + // quote bytes from the malformed value, including private-key fragments. + throw new Error('GCP_SA_JSON is not valid JSON'); + } + if (!parsed || typeof parsed !== 'object') { + throw new Error('GCP_SA_JSON must be a JSON object'); + } + const obj = parsed as Record; + const client_email = typeof obj.client_email === 'string' ? obj.client_email : null; + const private_key = typeof obj.private_key === 'string' ? obj.private_key : null; + if (!client_email || !private_key) { + throw new Error('GCP_SA_JSON must contain `client_email` and `private_key` fields'); + } + return { client_email, private_key }; +} + +async function buildProvider( + client: KeyManagementServiceClient, + keyVersion: string +): Promise { + const [pubResp] = await client.getPublicKey({ name: keyVersion }); + const kmsAlgorithm = pubResp.algorithm ?? ''; + const pem = pubResp.pem ?? ''; + if (!pem) { + throw new Error(`GCP KMS getPublicKey returned no PEM for ${redactKeyVersion(keyVersion)}`); + } + if (kmsAlgorithm !== KMS_ALG_ED25519) { + throw new Error( + `GCP KMS key ${redactKeyVersion(keyVersion)} has algorithm '${kmsAlgorithm}', expected '${KMS_ALG_ED25519}'` + ); + } + + assertPublicKeyMatchesCommitted(pem, keyVersion); + + return { + keyid: KID, + algorithm: ALGORITHM, + fingerprint: keyVersion, + async sign(payload: Uint8Array): Promise { + const [resp] = await client.asymmetricSign({ + name: keyVersion, + data: payload, + }); + return coerceSignature(resp.signature); + }, + }; +} + +function coerceSignature(value: Buffer | Uint8Array | string | null | undefined): Uint8Array { + if (value == null) { + throw new Error('GCP KMS asymmetricSign returned no signature bytes'); + } + if (typeof value === 'string') { + // The Node KMS client returns Buffer in practice; the string union is + // declared by the proto-generated types but doesn't appear at runtime. + // Refuse rather than guess at an encoding. + throw new Error('GCP KMS asymmetricSign returned a string signature; expected Buffer/Uint8Array'); + } + return value instanceof Uint8Array ? value : new Uint8Array(value); +} + +/** + * Tripwire: compare the KMS-returned PEM to the one committed in this repo. + * Different bytes mean the GCP key was rotated or replaced without a + * corresponding code change — refuse to sign rather than emit signatures + * verifiers (looking at the published JWKS) will reject. + * + * Comparison is on the SPKI public-key bytes, not the raw PEM string, so + * formatting differences (line endings, header capitalization) don't + * trigger false positives. + */ +function assertPublicKeyMatchesCommitted(actualPem: string, keyVersion: string): void { + const actualSpki = createPublicKey(actualPem).export({ type: 'spki', format: 'der' }) as Buffer; + const expectedSpki = createPublicKey(EXPECTED_PUBLIC_KEY_PEM).export({ type: 'spki', format: 'der' }) as Buffer; + if (!actualSpki.equals(expectedSpki)) { + throw new Error( + `GCP KMS public key for ${redactKeyVersion(keyVersion)} does not match the committed expected key. ` + + `If the key was rotated, update server/src/security/expected-public-key.ts and redeploy.` + ); + } +} + +function redactKeyVersion(keyVersion: string): string { + // Logs go to PostHog/OpenTelemetry; the resource name isn't a secret but + // there's no reason to spray the project ID through every log line. + return keyVersion.replace(/projects\/[^/]+/, 'projects/'); +} + +/** Test-only — drop the cached provider so a subsequent call re-initializes. */ +export function resetGcpKmsSignerForTests(): void { + cached = null; + initInFlight = null; +} diff --git a/server/src/security/jwks.ts b/server/src/security/jwks.ts new file mode 100644 index 0000000000..4406bc3cee --- /dev/null +++ b/server/src/security/jwks.ts @@ -0,0 +1,55 @@ +/** + * Public JWKS for Addie's request-signing key. + * + * Derived from the committed `EXPECTED_PUBLIC_KEY_PEM` so the published key + * and the signer's tripwire always reference the same source of truth — + * rotation is a one-line edit to that file plus a `GCP_KMS_KEY_VERSION` + * secret update. + */ + +import { createPublicKey } from 'node:crypto'; +import { EXPECTED_PUBLIC_KEY_PEM, KID } from './expected-public-key.js'; + +interface PublicJwk { + kty: string; + crv: string; + x: string; + kid: string; + alg: string; + use: string; + adcp_use: string; + key_ops: string[]; +} + +let cached: { keys: PublicJwk[] } | null = null; + +export function getPublicSigningJwks(): { keys: PublicJwk[] } { + if (cached) return cached; + const raw = createPublicKey(EXPECTED_PUBLIC_KEY_PEM).export({ format: 'jwk' }) as { + kty?: string; + crv?: string; + x?: string; + }; + if (raw.kty !== 'OKP' || raw.crv !== 'Ed25519' || typeof raw.x !== 'string') { + throw new Error( + `Expected public key is not Ed25519 OKP (got kty=${raw.kty}, crv=${raw.crv}). ` + + 'Update expected-public-key.ts.' + ); + } + const jwk: PublicJwk = { + kty: 'OKP', + crv: 'Ed25519', + x: raw.x, + kid: KID, + alg: 'EdDSA', + use: 'sig', + adcp_use: 'request-signing', + key_ops: ['verify'], + }; + cached = { keys: [jwk] }; + return cached; +} + +export function resetJwksForTests(): void { + cached = null; +} diff --git a/server/tests/unit/gcp-kms-signer.test.ts b/server/tests/unit/gcp-kms-signer.test.ts new file mode 100644 index 0000000000..0011bfc099 --- /dev/null +++ b/server/tests/unit/gcp-kms-signer.test.ts @@ -0,0 +1,116 @@ +/** + * Tests for the GCP KMS-backed RFC 9421 signing provider. + * + * The signer module reads two Fly secrets (GCP_SA_JSON, GCP_KMS_KEY_VERSION). + * These tests cover the env-handling paths and the JWKS publication that + * doesn't need a real KMS — full KMS round-trips need a mocked client and + * are out of scope for unit tests. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { createPublicKey } from 'node:crypto'; + +import { getGcpKmsSigningProvider, resetGcpKmsSignerForTests } from '../../src/security/gcp-kms-signer.js'; +import { getPublicSigningJwks, resetJwksForTests } from '../../src/security/jwks.js'; +import { EXPECTED_PUBLIC_KEY_PEM, KID } from '../../src/security/expected-public-key.js'; + +describe('gcp-kms-signer env handling', () => { + const originalSa = process.env.GCP_SA_JSON; + const originalKey = process.env.GCP_KMS_KEY_VERSION; + + beforeEach(() => { + delete process.env.GCP_SA_JSON; + delete process.env.GCP_KMS_KEY_VERSION; + resetGcpKmsSignerForTests(); + }); + + afterEach(() => { + if (originalSa === undefined) delete process.env.GCP_SA_JSON; + else process.env.GCP_SA_JSON = originalSa; + if (originalKey === undefined) delete process.env.GCP_KMS_KEY_VERSION; + else process.env.GCP_KMS_KEY_VERSION = originalKey; + resetGcpKmsSignerForTests(); + }); + + it('returns null when neither secret is set (dev default)', async () => { + const provider = await getGcpKmsSigningProvider(); + expect(provider).toBeNull(); + }); + + it('throws when GCP_SA_JSON is set but GCP_KMS_KEY_VERSION is not', async () => { + process.env.GCP_SA_JSON = '{"client_email":"x@example.com","private_key":"x"}'; + await expect(getGcpKmsSigningProvider()).rejects.toThrow(/partially configured/i); + }); + + it('throws when GCP_KMS_KEY_VERSION is set but GCP_SA_JSON is not', async () => { + process.env.GCP_KMS_KEY_VERSION = 'projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1'; + await expect(getGcpKmsSigningProvider()).rejects.toThrow(/partially configured/i); + }); + + it('throws when GCP_SA_JSON is not valid JSON', async () => { + process.env.GCP_SA_JSON = 'not json'; + process.env.GCP_KMS_KEY_VERSION = 'projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1'; + await expect(getGcpKmsSigningProvider()).rejects.toThrow(/not valid JSON/i); + }); + + it('throws when GCP_SA_JSON lacks client_email or private_key', async () => { + process.env.GCP_SA_JSON = '{"client_email":"x@example.com"}'; + process.env.GCP_KMS_KEY_VERSION = 'projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1'; + await expect(getGcpKmsSigningProvider()).rejects.toThrow(/client_email.*private_key|private_key.*client_email/i); + }); + + it('JSON.parse error message does NOT include parser detail (parser offset can quote secret bytes)', async () => { + process.env.GCP_SA_JSON = '{"private_key":"-----BEGIN ROOT_OF_TRUST_BYTES-----'; + process.env.GCP_KMS_KEY_VERSION = 'projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1'; + await expect(getGcpKmsSigningProvider()).rejects.toThrow(/^GCP_SA_JSON is not valid JSON$/); + // Negative assertion: the rejected error must not echo the malformed payload. + await expect( + getGcpKmsSigningProvider().catch((e: Error) => e.message) + ).resolves.not.toMatch(/ROOT_OF_TRUST_BYTES|position \d+|Unexpected/); + }); + + it('concurrent first calls share one in-flight init (env-rejection path)', async () => { + process.env.GCP_SA_JSON = '{"client_email":"x@example.com"}'; + process.env.GCP_KMS_KEY_VERSION = 'projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1'; + // Both should reject for the same reason (missing private_key) — and the + // race-fix guarantees they don't fan out to two independent KMS clients + // even when the rejection is synchronous-ish. + const [a, b] = await Promise.allSettled([ + getGcpKmsSigningProvider(), + getGcpKmsSigningProvider(), + ]); + expect(a.status).toBe('rejected'); + expect(b.status).toBe('rejected'); + }); +}); + +describe('getPublicSigningJwks', () => { + beforeEach(() => { + resetJwksForTests(); + }); + + it('publishes one Ed25519 JWK with the expected kid', () => { + const jwks = getPublicSigningJwks(); + expect(jwks.keys).toHaveLength(1); + const jwk = jwks.keys[0]; + expect(jwk.kty).toBe('OKP'); + expect(jwk.crv).toBe('Ed25519'); + expect(jwk.alg).toBe('EdDSA'); + expect(jwk.use).toBe('sig'); + expect(jwk.adcp_use).toBe('request-signing'); + expect(jwk.kid).toBe(KID); + expect(jwk.key_ops).toEqual(['verify']); + }); + + it('JWK x parameter matches the committed PEM public key', () => { + const jwks = getPublicSigningJwks(); + const jwk = jwks.keys[0]; + const pemDerived = createPublicKey(EXPECTED_PUBLIC_KEY_PEM).export({ format: 'jwk' }) as { x?: string }; + expect(jwk.x).toBe(pemDerived.x); + }); + + it('returns the same object on repeated calls (cache)', () => { + const a = getPublicSigningJwks(); + const b = getPublicSigningJwks(); + expect(a).toBe(b); + }); +});