@adcp/sdk 13.0.0-rc.4 → 13.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/bin/adcp.js +7 -1
- package/dist/lib/adapters/property-list-adapter.d.mts +5 -1
- package/dist/lib/adapters/property-list-adapter.d.ts +5 -1
- package/dist/lib/adapters/property-list-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/property-list-adapter.js +7 -7
- package/dist/lib/adapters/property-list-adapter.js.map +1 -1
- package/dist/lib/adapters/property-list-adapter.mjs +7 -7
- package/dist/lib/adapters/property-list-adapter.mjs.map +1 -1
- package/dist/lib/auth/oauth/CLIFlowHandler.d.mts +10 -3
- package/dist/lib/auth/oauth/CLIFlowHandler.d.ts +10 -3
- package/dist/lib/auth/oauth/CLIFlowHandler.d.ts.map +1 -1
- package/dist/lib/auth/oauth/CLIFlowHandler.js +40 -4
- package/dist/lib/auth/oauth/CLIFlowHandler.js.map +1 -1
- package/dist/lib/auth/oauth/CLIFlowHandler.mjs +40 -4
- package/dist/lib/auth/oauth/CLIFlowHandler.mjs.map +1 -1
- package/dist/lib/auth/oauth/index.d.mts +1 -1
- package/dist/lib/auth/oauth/index.d.ts +1 -1
- package/dist/lib/auth/oauth/index.d.ts.map +1 -1
- package/dist/lib/auth/oauth/index.js +2 -0
- package/dist/lib/auth/oauth/index.js.map +1 -1
- package/dist/lib/auth/oauth/index.mjs +2 -0
- package/dist/lib/auth/oauth/index.mjs.map +1 -1
- package/dist/lib/auth/oauth/web-flow.d.mts +12 -0
- package/dist/lib/auth/oauth/web-flow.d.ts +12 -0
- package/dist/lib/auth/oauth/web-flow.d.ts.map +1 -1
- package/dist/lib/auth/oauth/web-flow.js +20 -2
- package/dist/lib/auth/oauth/web-flow.js.map +1 -1
- package/dist/lib/auth/oauth/web-flow.mjs +19 -2
- package/dist/lib/auth/oauth/web-flow.mjs.map +1 -1
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.mts +27 -1
- package/dist/lib/core/SingleAgentClient.d.ts +27 -1
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +46 -18
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/core/SingleAgentClient.mjs +46 -18
- package/dist/lib/core/SingleAgentClient.mjs.map +1 -1
- package/dist/lib/net/address-guards.d.ts.map +1 -1
- package/dist/lib/net/address-guards.js +4 -0
- package/dist/lib/net/address-guards.js.map +1 -1
- package/dist/lib/net/address-guards.mjs +4 -0
- package/dist/lib/net/address-guards.mjs.map +1 -1
- package/dist/lib/protocols/a2a.js +2 -2
- package/dist/lib/protocols/a2a.js.map +1 -1
- package/dist/lib/protocols/a2a.mjs +2 -2
- package/dist/lib/protocols/a2a.mjs.map +1 -1
- package/dist/lib/protocols/index.d.ts.map +1 -1
- package/dist/lib/protocols/index.js +28 -7
- package/dist/lib/protocols/index.js.map +1 -1
- package/dist/lib/protocols/index.mjs +28 -7
- package/dist/lib/protocols/index.mjs.map +1 -1
- package/dist/lib/protocols/mcp-modern.d.ts.map +1 -1
- package/dist/lib/protocols/mcp-modern.js +18 -5
- package/dist/lib/protocols/mcp-modern.js.map +1 -1
- package/dist/lib/protocols/mcp-modern.mjs +18 -5
- package/dist/lib/protocols/mcp-modern.mjs.map +1 -1
- package/dist/lib/protocols/mcp-tasks.d.ts.map +1 -1
- package/dist/lib/protocols/mcp-tasks.js +2 -12
- package/dist/lib/protocols/mcp-tasks.js.map +1 -1
- package/dist/lib/protocols/mcp-tasks.mjs +2 -12
- package/dist/lib/protocols/mcp-tasks.mjs.map +1 -1
- package/dist/lib/protocols/mcp.js +2 -2
- package/dist/lib/protocols/mcp.js.map +1 -1
- package/dist/lib/protocols/mcp.mjs +2 -2
- package/dist/lib/protocols/mcp.mjs.map +1 -1
- package/dist/lib/registry/types.generated.d.mts +554 -15
- package/dist/lib/registry/types.generated.d.ts +554 -15
- package/dist/lib/registry/types.generated.d.ts.map +1 -1
- package/dist/lib/registry/types.generated.js.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/decisioning/account.d.mts +6 -4
- package/dist/lib/server/decisioning/account.d.ts +6 -4
- package/dist/lib/server/decisioning/account.d.ts.map +1 -1
- package/dist/lib/server/decisioning/account.js.map +1 -1
- package/dist/lib/server/decisioning/account.mjs.map +1 -1
- package/dist/lib/server/decisioning/manifest-helpers.d.mts +6 -5
- package/dist/lib/server/decisioning/manifest-helpers.d.ts +6 -5
- package/dist/lib/server/decisioning/manifest-helpers.d.ts.map +1 -1
- package/dist/lib/server/decisioning/manifest-helpers.js.map +1 -1
- package/dist/lib/server/decisioning/manifest-helpers.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +4 -2
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +4 -2
- package/dist/lib/server/decisioning/runtime/from-platform.mjs.map +1 -1
- package/dist/lib/server/decisioning/tenant-store.d.mts +26 -14
- package/dist/lib/server/decisioning/tenant-store.d.ts +26 -14
- package/dist/lib/server/decisioning/tenant-store.d.ts.map +1 -1
- package/dist/lib/server/decisioning/tenant-store.js +6 -1
- package/dist/lib/server/decisioning/tenant-store.js.map +1 -1
- package/dist/lib/server/decisioning/tenant-store.mjs +6 -1
- package/dist/lib/server/decisioning/tenant-store.mjs.map +1 -1
- package/dist/lib/server/pin-and-bind-fetch.d.mts +2 -0
- package/dist/lib/server/pin-and-bind-fetch.d.ts +2 -0
- package/dist/lib/server/pin-and-bind-fetch.d.ts.map +1 -1
- package/dist/lib/server/pin-and-bind-fetch.js +2 -0
- package/dist/lib/server/pin-and-bind-fetch.js.map +1 -1
- package/dist/lib/server/pin-and-bind-fetch.mjs +2 -0
- package/dist/lib/server/pin-and-bind-fetch.mjs.map +1 -1
- package/dist/lib/server/structured-serialize.d.ts.map +1 -1
- package/dist/lib/server/structured-serialize.js +10 -2
- package/dist/lib/server/structured-serialize.js.map +1 -1
- package/dist/lib/server/structured-serialize.mjs +10 -2
- package/dist/lib/server/structured-serialize.mjs.map +1 -1
- package/dist/lib/server/webhook-emitter.d.ts.map +1 -1
- package/dist/lib/server/webhook-emitter.js +4 -2
- package/dist/lib/server/webhook-emitter.js.map +1 -1
- package/dist/lib/server/webhook-emitter.mjs +4 -2
- package/dist/lib/server/webhook-emitter.mjs.map +1 -1
- package/dist/lib/signing/middleware.d.mts +2 -1
- package/dist/lib/signing/middleware.d.ts +2 -1
- package/dist/lib/signing/middleware.d.ts.map +1 -1
- package/dist/lib/signing/middleware.js +26 -4
- package/dist/lib/signing/middleware.js.map +1 -1
- package/dist/lib/signing/middleware.mjs +26 -4
- package/dist/lib/signing/middleware.mjs.map +1 -1
- package/dist/lib/signing/webhook-verifier.d.ts.map +1 -1
- package/dist/lib/signing/webhook-verifier.js +4 -2
- package/dist/lib/signing/webhook-verifier.js.map +1 -1
- package/dist/lib/signing/webhook-verifier.mjs +4 -2
- package/dist/lib/signing/webhook-verifier.mjs.map +1 -1
- package/dist/lib/testing/agent-tester.d.ts.map +1 -1
- package/dist/lib/testing/agent-tester.js +46 -1
- package/dist/lib/testing/agent-tester.js.map +1 -1
- package/dist/lib/testing/agent-tester.mjs +46 -1
- package/dist/lib/testing/agent-tester.mjs.map +1 -1
- package/dist/lib/types/asset-instances.d.mts +8 -6
- package/dist/lib/types/asset-instances.d.ts +8 -6
- package/dist/lib/types/asset-instances.d.ts.map +1 -1
- package/dist/lib/types/asset-instances.js.map +1 -1
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/index.mjs.map +1 -1
- package/dist/lib/utils/capabilities.d.mts +2 -0
- package/dist/lib/utils/capabilities.d.ts +2 -0
- package/dist/lib/utils/capabilities.d.ts.map +1 -1
- package/dist/lib/utils/capabilities.js +2 -1
- package/dist/lib/utils/capabilities.js.map +1 -1
- package/dist/lib/utils/capabilities.mjs +2 -1
- package/dist/lib/utils/capabilities.mjs.map +1 -1
- package/dist/lib/utils/probe-policy.d.mts +0 -16
- package/dist/lib/utils/probe-policy.d.ts +0 -16
- package/dist/lib/utils/probe-policy.d.ts.map +1 -1
- package/dist/lib/utils/probe-policy.js +15 -1
- package/dist/lib/utils/probe-policy.js.map +1 -1
- package/dist/lib/utils/probe-policy.mjs +15 -1
- package/dist/lib/utils/probe-policy.mjs.map +1 -1
- package/dist/lib/utils/redact-args.d.mts +36 -0
- package/dist/lib/utils/redact-args.d.ts +37 -0
- package/dist/lib/utils/redact-args.d.ts.map +1 -0
- package/dist/lib/utils/redact-args.js +46 -0
- package/dist/lib/utils/redact-args.js.map +1 -0
- package/dist/lib/utils/redact-args.mjs +22 -0
- package/dist/lib/utils/redact-args.mjs.map +1 -0
- package/dist/lib/v2/projection/creative-delivery.js +1 -5
- package/dist/lib/v2/projection/creative-delivery.js.map +1 -1
- package/dist/lib/v2/projection/creative-delivery.mjs +1 -5
- package/dist/lib/v2/projection/creative-delivery.mjs.map +1 -1
- package/dist/lib/validation/index.d.mts +15 -1
- package/dist/lib/validation/index.d.ts +15 -1
- package/dist/lib/validation/index.d.ts.map +1 -1
- package/dist/lib/validation/index.js +4 -8
- package/dist/lib/validation/index.js.map +1 -1
- package/dist/lib/validation/index.mjs +4 -8
- package/dist/lib/validation/index.mjs.map +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/version.mjs +3 -3
- package/dist/lib/version.mjs.map +1 -1
- package/examples/hello_seller_adapter_multi_tenant.ts +7 -0
- package/package.json +3 -2
- package/skills/build-holdco-agent/SKILL.md +19 -0
- package/skills/cross-cutting.md +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/utils/probe-policy.ts"],"sourcesContent":["/**\n * Probe policy for buyer-side discovery (adcp-client#1618).\n *\n * Whether the SDK is willing to issue an outbound probe to a given URL\n * during agent discovery (`detectProtocol` well-known card fetches,\n * `createTestClient` agent URL acceptance). Sits one layer above the\n * `src/lib/net/address-guards.ts` IP classifiers and the `ssrfSafeFetch`\n * primitive — those operate on already-resolved addresses; this layer\n * applies a higher-level allow/deny decision keyed on the URL hostname\n * before anything reaches DNS.\n *\n * ## Default policy\n *\n * | Range | Default | Why |\n * | ------------------------------------------- | ------- | ------------------------------------------------------- |\n * | Loopback (`127.0.0.0/8`, `::1`, `localhost`)| allow | Local dev loops, mock-server tests, `npm run dev` |\n * | RFC 1918 / link-local / IPv6 ULA | deny | Internal subnets behind a server-side comply runner |\n * | Cloud metadata (`169.254.169.254`, `fe80::/10`) | deny | IMDS exfiltration is never a legitimate buyer use case |\n * | Public IPv4/IPv6 | allow | The whole point |\n *\n * ## Single opt-out\n *\n * `ADCP_ALLOW_INTERNAL_PROBES=1` (read once at module load) widens the\n * default to allow RFC-1918/link-local/ULA. IMDS stays blocked even with\n * this flag — cloud metadata reach is never legitimate.\n *\n * NOT controlled by `NODE_ENV`: a multi-tenant staging image with\n * `NODE_ENV=test` MUST NOT inherit looser SSRF posture than production.\n *\n * ## Known gap (TOCTOU) — adcp-client#1627\n *\n * `classifyProbeUrl` does NOT do DNS resolution — it inspects the\n * URL's hostname literal. A hostname like `evil.example.com` that\n * resolves to `169.254.169.254` will pass this gate; the per-IP block\n * inside `ssrfSafeFetch` (which DOES resolve and pin) catches it.\n * Use both: this gate refuses obvious literal attacks; `ssrfSafeFetch`\n * refuses DNS-based attacks. Routing `detectProtocol` through\n * `ssrfSafeFetch` for full TOCTOU defense is tracked under #1627.\n */\n\nimport { BlockList, isIP } from 'node:net';\nimport { isAlwaysBlocked, isPrivateIp } from '../net/address-guards';\n\n// Loopback canonicalization. Node's URL parser canonicalizes IPv4-mapped\n// IPv6 (`::ffff:127.0.0.1`) into binary form (`::ffff:7f00:1`), which\n// would slip past a textual regex. `BlockList` handles all canonical forms\n// natively, including IPv4-mapped IPv6 against the v4 subnet.\nconst loopbackBlock = new BlockList();\nloopbackBlock.addSubnet('127.0.0.0', 8, 'ipv4');\nloopbackBlock.addAddress('::1', 'ipv6');\n\n/**\n * Operator opt-in to allow RFC-1918 / link-local / ULA destinations.\n * Read once at module load — runtime mutation does not propagate. Operators\n * who flip this in CI should set it via the workflow `env:` block.\n *\n * IMDS stays refused regardless.\n */\nconst ALLOW_INTERNAL = process.env.ADCP_ALLOW_INTERNAL_PROBES === '1';\n\n/**\n * Match a host literal that resolves trivially to loopback. Allowed even\n * in strict mode because local dev loops are not an SSRF target — the\n * attacker would have to be on the buyer's own machine to reach them, at\n * which point they have strictly more powerful primitives.\n *\n * `localhost` is hardcoded rather than DNS-resolved so an attacker can't\n * point `localhost.attacker.example.com` at `169.254.169.254` and slip\n * past as \"loopback-named\". Hostname must be exactly `localhost` (case\n * insensitive). IP literals must be in 127/8 or `::1`.\n */\nfunction isLoopbackHost(host: string): boolean {\n if (host.toLowerCase() === 'localhost') return true;\n // Strip URL-style brackets in case the caller didn't (defensive — internal\n // callers strip first).\n let bare = host;\n if (bare.startsWith('[') && bare.endsWith(']')) bare = bare.slice(1, -1);\n const family = isIP(bare);\n if (family === 4) return loopbackBlock.check(bare, 'ipv4');\n if (family === 6) return loopbackBlock.check(bare, 'ipv6');\n return false;\n}\n\nexport type ProbePolicyResult =\n | { allowed: true }\n | { allowed: false; code: 'invalid_url' | 'always_blocked' | 'private_address'; reason: string };\n\n/**\n * Decide whether the SDK should issue a discovery probe to `url`. Returns\n * `{ allowed: true }` when the probe may proceed, otherwise a refusal\n * with a human-readable reason and a typed code for callers that want\n * to map the refusal into their own error envelope.\n *\n * Designed to be cheap and synchronous — call this BEFORE any try/catch\n * in the discovery loop so refusal can't be silently swallowed and\n * converted into \"host doesn't speak A2A, fall back to MCP\" (the\n * code-reviewer-flagged catch-swallow class from #1618 triage).\n *\n * **Error messages do NOT echo resolved IP addresses** — the `address`\n * field on the rejection carries them when present, but the user-visible\n * text names only the hostname so that compliance reports and log\n * aggregators don't leak internal network topology.\n */\nexport function classifyProbeUrl(url: string): ProbePolicyResult {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n // Don't refuse here — let the underlying call surface its own error\n // (it'll fail with a clearer \"Invalid URL\" downstream). This function\n // is not the URL validator, only the SSRF policy.\n return { allowed: true };\n }\n\n // `URL.hostname` returns IPv6 literals wrapped in brackets; the address\n // classifiers want the bare form.\n const host = parsed.hostname.replace(/^\\[|\\]$/g, '');\n\n // IMDS / IPv6 link-local: ALWAYS refused. Cloud metadata reach is never\n // a legitimate buyer-side use case — refuse even when the operator has\n // explicitly opted into private probes.\n if (isAlwaysBlocked(host)) {\n return {\n allowed: false,\n code: 'always_blocked',\n reason: `Refusing to probe '${host}': cloud-metadata or link-local address.`,\n };\n }\n\n // Loopback: ALWAYS allowed. CLI dev loops, mock-server tests, local\n // adapter integration tests all hit this path. Even in strict mode the\n // attacker would need to be on-host already to exploit it.\n if (isLoopbackHost(host)) {\n return { allowed: true };\n }\n\n // Private/RFC-1918/ULA: refused unless the operator opted in via env.\n // The opt-in is read once at module load — see ALLOW_INTERNAL above.\n if (isPrivateIp(host)) {\n if (ALLOW_INTERNAL) {\n return { allowed: true };\n }\n return {\n allowed: false,\n code: 'private_address',\n reason:\n `Refusing to probe '${host}': private/RFC-1918 address. ` +\n `Set ADCP_ALLOW_INTERNAL_PROBES=1 to allow private-network probes (operator-only).`,\n };\n }\n\n return { allowed: true };\n}\n\n/** Test-only accessor for the env flag — exported for unit-test reset semantics. */\nexport function isInternalProbesAllowed(): boolean {\n return ALLOW_INTERNAL;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwCA,sBAAgC;AAChC,4BAA6C;AAM7C,MAAM,gBAAgB,IAAI,0BAAU;AACpC,cAAc,UAAU,aAAa,GAAG,MAAM;AAC9C,cAAc,WAAW,OAAO,MAAM;AAStC,MAAM,iBAAiB,QAAQ,IAAI,+BAA+B;AAalE,SAAS,eAAe,MAAuB;AAC7C,MAAI,KAAK,YAAY,MAAM,YAAa,QAAO;AAG/C,MAAI,OAAO;AACX,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,EAAG,QAAO,KAAK,MAAM,GAAG,EAAE;AACvE,QAAM,aAAS,sBAAK,IAAI;AACxB,MAAI,WAAW,EAAG,QAAO,cAAc,MAAM,MAAM,MAAM;AACzD,MAAI,WAAW,EAAG,QAAO,cAAc,MAAM,MAAM,MAAM;AACzD,SAAO;AACT;
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/utils/probe-policy.ts"],"sourcesContent":["/**\n * Probe policy for buyer-side discovery (adcp-client#1618).\n *\n * Whether the SDK is willing to issue an outbound probe to a given URL\n * during agent discovery (`detectProtocol` well-known card fetches,\n * `createTestClient` agent URL acceptance). Sits one layer above the\n * `src/lib/net/address-guards.ts` IP classifiers and the `ssrfSafeFetch`\n * primitive — those operate on already-resolved addresses; this layer\n * applies a higher-level allow/deny decision keyed on the URL hostname\n * before anything reaches DNS.\n *\n * ## Default policy\n *\n * | Range | Default | Why |\n * | ------------------------------------------- | ------- | ------------------------------------------------------- |\n * | Loopback (`127.0.0.0/8`, `::1`, `localhost`)| allow | Local dev loops, mock-server tests, `npm run dev` |\n * | RFC 1918 / link-local / IPv6 ULA | deny | Internal subnets behind a server-side comply runner |\n * | Cloud metadata (`169.254.169.254`, `fe80::/10`) | deny | IMDS exfiltration is never a legitimate buyer use case |\n * | Public IPv4/IPv6 | allow | The whole point |\n *\n * ## Single opt-out\n *\n * `ADCP_ALLOW_INTERNAL_PROBES=1` (read once at module load) widens the\n * default to allow RFC-1918/link-local/ULA. IMDS stays blocked even with\n * this flag — cloud metadata reach is never legitimate.\n *\n * NOT controlled by `NODE_ENV`: a multi-tenant staging image with\n * `NODE_ENV=test` MUST NOT inherit looser SSRF posture than production.\n *\n * ## Known gap (TOCTOU) — adcp-client#1627\n *\n * `classifyProbeUrl` does NOT do DNS resolution — it inspects the\n * URL's hostname literal. A hostname like `evil.example.com` that\n * resolves to `169.254.169.254` will pass this gate; the per-IP block\n * inside `ssrfSafeFetch` (which DOES resolve and pin) catches it.\n * Use both: this gate refuses obvious literal attacks; `ssrfSafeFetch`\n * refuses DNS-based attacks. Routing `detectProtocol` through\n * `ssrfSafeFetch` for full TOCTOU defense is tracked under #1627.\n */\n\nimport { BlockList, isIP } from 'node:net';\nimport { isAlwaysBlocked, isPrivateIp } from '../net/address-guards';\n\n// Loopback canonicalization. Node's URL parser canonicalizes IPv4-mapped\n// IPv6 (`::ffff:127.0.0.1`) into binary form (`::ffff:7f00:1`), which\n// would slip past a textual regex. `BlockList` handles all canonical forms\n// natively, including IPv4-mapped IPv6 against the v4 subnet.\nconst loopbackBlock = new BlockList();\nloopbackBlock.addSubnet('127.0.0.0', 8, 'ipv4');\nloopbackBlock.addAddress('::1', 'ipv6');\n\n/**\n * Operator opt-in to allow RFC-1918 / link-local / ULA destinations.\n * Read once at module load — runtime mutation does not propagate. Operators\n * who flip this in CI should set it via the workflow `env:` block.\n *\n * IMDS stays refused regardless.\n */\nconst ALLOW_INTERNAL = process.env.ADCP_ALLOW_INTERNAL_PROBES === '1';\n\n/**\n * Match a host literal that resolves trivially to loopback. Allowed even\n * in strict mode because local dev loops are not an SSRF target — the\n * attacker would have to be on the buyer's own machine to reach them, at\n * which point they have strictly more powerful primitives.\n *\n * `localhost` is hardcoded rather than DNS-resolved so an attacker can't\n * point `localhost.attacker.example.com` at `169.254.169.254` and slip\n * past as \"loopback-named\". Hostname must be exactly `localhost` (case\n * insensitive). IP literals must be in 127/8 or `::1`.\n */\nfunction isLoopbackHost(host: string): boolean {\n if (host.toLowerCase() === 'localhost') return true;\n // Strip URL-style brackets in case the caller didn't (defensive — internal\n // callers strip first).\n let bare = host;\n if (bare.startsWith('[') && bare.endsWith(']')) bare = bare.slice(1, -1);\n const family = isIP(bare);\n if (family === 4) return loopbackBlock.check(bare, 'ipv4');\n if (family === 6) return loopbackBlock.check(bare, 'ipv6');\n return false;\n}\n\nexport type ProbePolicyResult =\n | { allowed: true }\n | { allowed: false; code: 'invalid_url' | 'always_blocked' | 'private_address'; reason: string };\n\n/**\n * Decide whether the SDK should issue a discovery probe to `url`. Returns\n * `{ allowed: true }` when the probe may proceed, otherwise a refusal\n * with a human-readable reason and a typed code for callers that want\n * to map the refusal into their own error envelope.\n *\n * Designed to be cheap and synchronous — call this BEFORE any try/catch\n * in the discovery loop so refusal can't be silently swallowed and\n * converted into \"host doesn't speak A2A, fall back to MCP\" (the\n * code-reviewer-flagged catch-swallow class from #1618 triage).\n *\n * **Error messages do NOT echo resolved IP addresses** — the `address`\n * field on the rejection carries them when present, but the user-visible\n * text names only the hostname so that compliance reports and log\n * aggregators don't leak internal network topology.\n */\n/**\n * Cloud-metadata hostnames, lowercased. Registered names rather than IP\n * literals, so the CIDR classifiers in `net/address-guards` cannot see them.\n */\nconst METADATA_HOSTNAMES: ReadonlySet<string> = new Set([\n 'metadata',\n 'metadata.google.internal',\n 'metadata.goog',\n 'metadata.packet.net',\n 'instance-data',\n]);\n\nexport function classifyProbeUrl(url: string): ProbePolicyResult {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n // Don't refuse here — let the underlying call surface its own error\n // (it'll fail with a clearer \"Invalid URL\" downstream). This function\n // is not the URL validator, only the SSRF policy.\n return { allowed: true };\n }\n\n // `URL.hostname` returns IPv6 literals wrapped in brackets; the address\n // classifiers want the bare form. A fully-qualified name keeps its root dot\n // (`localhost.`, `metadata.google.internal.`) and resolves identically, so\n // strip it before any name comparison.\n const host = parsed.hostname.replace(/^\\[|\\]$/g, '').replace(/\\.$/, '');\n\n // Cloud metadata by NAME. These are ordinary registered names, so no CIDR\n // classifier sees them — `metadata.google.internal` is the second-most-used\n // IMDS target after 169.254.169.254 and needs no IP literal at all. Refused\n // even with the opt-in, same as the address-literal IMDS ranges below.\n // Mirrors `WEBHOOK_SSRF_POLICY.hosts_denied_metadata`.\n if (METADATA_HOSTNAMES.has(host)) {\n return {\n allowed: false,\n code: 'always_blocked',\n reason: `Refusing to probe '${host}': cloud-metadata hostname.`,\n };\n }\n\n // IMDS / IPv6 link-local: ALWAYS refused. Cloud metadata reach is never\n // a legitimate buyer-side use case — refuse even when the operator has\n // explicitly opted into private probes.\n if (isAlwaysBlocked(host)) {\n return {\n allowed: false,\n code: 'always_blocked',\n reason: `Refusing to probe '${host}': cloud-metadata or link-local address.`,\n };\n }\n\n // Loopback: ALWAYS allowed. CLI dev loops, mock-server tests, local\n // adapter integration tests all hit this path. Even in strict mode the\n // attacker would need to be on-host already to exploit it.\n if (isLoopbackHost(host)) {\n return { allowed: true };\n }\n\n // Private/RFC-1918/ULA: refused unless the operator opted in via env.\n // The opt-in is read once at module load — see ALLOW_INTERNAL above.\n if (isPrivateIp(host)) {\n if (ALLOW_INTERNAL) {\n return { allowed: true };\n }\n return {\n allowed: false,\n code: 'private_address',\n reason:\n `Refusing to probe '${host}': private/RFC-1918 address. ` +\n `Set ADCP_ALLOW_INTERNAL_PROBES=1 to allow private-network probes (operator-only).`,\n };\n }\n\n return { allowed: true };\n}\n\n/** Test-only accessor for the env flag — exported for unit-test reset semantics. */\nexport function isInternalProbesAllowed(): boolean {\n return ALLOW_INTERNAL;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwCA,sBAAgC;AAChC,4BAA6C;AAM7C,MAAM,gBAAgB,IAAI,0BAAU;AACpC,cAAc,UAAU,aAAa,GAAG,MAAM;AAC9C,cAAc,WAAW,OAAO,MAAM;AAStC,MAAM,iBAAiB,QAAQ,IAAI,+BAA+B;AAalE,SAAS,eAAe,MAAuB;AAC7C,MAAI,KAAK,YAAY,MAAM,YAAa,QAAO;AAG/C,MAAI,OAAO;AACX,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,EAAG,QAAO,KAAK,MAAM,GAAG,EAAE;AACvE,QAAM,aAAS,sBAAK,IAAI;AACxB,MAAI,WAAW,EAAG,QAAO,cAAc,MAAM,MAAM,MAAM;AACzD,MAAI,WAAW,EAAG,QAAO,cAAc,MAAM,MAAM,MAAM;AACzD,SAAO;AACT;AA0BA,MAAM,qBAA0C,oBAAI,IAAI;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,iBAAiB,KAAgC;AAC/D,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AAIN,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AAMA,QAAM,OAAO,OAAO,SAAS,QAAQ,YAAY,EAAE,EAAE,QAAQ,OAAO,EAAE;AAOtE,MAAI,mBAAmB,IAAI,IAAI,GAAG;AAChC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,QAAQ,sBAAsB,IAAI;AAAA,IACpC;AAAA,EACF;AAKA,UAAI,uCAAgB,IAAI,GAAG;AACzB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,QAAQ,sBAAsB,IAAI;AAAA,IACpC;AAAA,EACF;AAKA,MAAI,eAAe,IAAI,GAAG;AACxB,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AAIA,UAAI,mCAAY,IAAI,GAAG;AACrB,QAAI,gBAAgB;AAClB,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AACA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,QACE,sBAAsB,IAAI;AAAA,IAE9B;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,KAAK;AACzB;AAGO,SAAS,0BAAmC;AACjD,SAAO;AACT;","names":[]}
|
|
@@ -13,6 +13,13 @@ function isLoopbackHost(host) {
|
|
|
13
13
|
if (family === 6) return loopbackBlock.check(bare, "ipv6");
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
+
const METADATA_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
17
|
+
"metadata",
|
|
18
|
+
"metadata.google.internal",
|
|
19
|
+
"metadata.goog",
|
|
20
|
+
"metadata.packet.net",
|
|
21
|
+
"instance-data"
|
|
22
|
+
]);
|
|
16
23
|
function classifyProbeUrl(url) {
|
|
17
24
|
let parsed;
|
|
18
25
|
try {
|
|
@@ -20,7 +27,14 @@ function classifyProbeUrl(url) {
|
|
|
20
27
|
} catch {
|
|
21
28
|
return { allowed: true };
|
|
22
29
|
}
|
|
23
|
-
const host = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
30
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
31
|
+
if (METADATA_HOSTNAMES.has(host)) {
|
|
32
|
+
return {
|
|
33
|
+
allowed: false,
|
|
34
|
+
code: "always_blocked",
|
|
35
|
+
reason: `Refusing to probe '${host}': cloud-metadata hostname.`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
24
38
|
if (isAlwaysBlocked(host)) {
|
|
25
39
|
return {
|
|
26
40
|
allowed: false,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/utils/probe-policy.ts"],"sourcesContent":["/**\n * Probe policy for buyer-side discovery (adcp-client#1618).\n *\n * Whether the SDK is willing to issue an outbound probe to a given URL\n * during agent discovery (`detectProtocol` well-known card fetches,\n * `createTestClient` agent URL acceptance). Sits one layer above the\n * `src/lib/net/address-guards.ts` IP classifiers and the `ssrfSafeFetch`\n * primitive — those operate on already-resolved addresses; this layer\n * applies a higher-level allow/deny decision keyed on the URL hostname\n * before anything reaches DNS.\n *\n * ## Default policy\n *\n * | Range | Default | Why |\n * | ------------------------------------------- | ------- | ------------------------------------------------------- |\n * | Loopback (`127.0.0.0/8`, `::1`, `localhost`)| allow | Local dev loops, mock-server tests, `npm run dev` |\n * | RFC 1918 / link-local / IPv6 ULA | deny | Internal subnets behind a server-side comply runner |\n * | Cloud metadata (`169.254.169.254`, `fe80::/10`) | deny | IMDS exfiltration is never a legitimate buyer use case |\n * | Public IPv4/IPv6 | allow | The whole point |\n *\n * ## Single opt-out\n *\n * `ADCP_ALLOW_INTERNAL_PROBES=1` (read once at module load) widens the\n * default to allow RFC-1918/link-local/ULA. IMDS stays blocked even with\n * this flag — cloud metadata reach is never legitimate.\n *\n * NOT controlled by `NODE_ENV`: a multi-tenant staging image with\n * `NODE_ENV=test` MUST NOT inherit looser SSRF posture than production.\n *\n * ## Known gap (TOCTOU) — adcp-client#1627\n *\n * `classifyProbeUrl` does NOT do DNS resolution — it inspects the\n * URL's hostname literal. A hostname like `evil.example.com` that\n * resolves to `169.254.169.254` will pass this gate; the per-IP block\n * inside `ssrfSafeFetch` (which DOES resolve and pin) catches it.\n * Use both: this gate refuses obvious literal attacks; `ssrfSafeFetch`\n * refuses DNS-based attacks. Routing `detectProtocol` through\n * `ssrfSafeFetch` for full TOCTOU defense is tracked under #1627.\n */\n\nimport { BlockList, isIP } from 'node:net';\nimport { isAlwaysBlocked, isPrivateIp } from '../net/address-guards';\n\n// Loopback canonicalization. Node's URL parser canonicalizes IPv4-mapped\n// IPv6 (`::ffff:127.0.0.1`) into binary form (`::ffff:7f00:1`), which\n// would slip past a textual regex. `BlockList` handles all canonical forms\n// natively, including IPv4-mapped IPv6 against the v4 subnet.\nconst loopbackBlock = new BlockList();\nloopbackBlock.addSubnet('127.0.0.0', 8, 'ipv4');\nloopbackBlock.addAddress('::1', 'ipv6');\n\n/**\n * Operator opt-in to allow RFC-1918 / link-local / ULA destinations.\n * Read once at module load — runtime mutation does not propagate. Operators\n * who flip this in CI should set it via the workflow `env:` block.\n *\n * IMDS stays refused regardless.\n */\nconst ALLOW_INTERNAL = process.env.ADCP_ALLOW_INTERNAL_PROBES === '1';\n\n/**\n * Match a host literal that resolves trivially to loopback. Allowed even\n * in strict mode because local dev loops are not an SSRF target — the\n * attacker would have to be on the buyer's own machine to reach them, at\n * which point they have strictly more powerful primitives.\n *\n * `localhost` is hardcoded rather than DNS-resolved so an attacker can't\n * point `localhost.attacker.example.com` at `169.254.169.254` and slip\n * past as \"loopback-named\". Hostname must be exactly `localhost` (case\n * insensitive). IP literals must be in 127/8 or `::1`.\n */\nfunction isLoopbackHost(host: string): boolean {\n if (host.toLowerCase() === 'localhost') return true;\n // Strip URL-style brackets in case the caller didn't (defensive — internal\n // callers strip first).\n let bare = host;\n if (bare.startsWith('[') && bare.endsWith(']')) bare = bare.slice(1, -1);\n const family = isIP(bare);\n if (family === 4) return loopbackBlock.check(bare, 'ipv4');\n if (family === 6) return loopbackBlock.check(bare, 'ipv6');\n return false;\n}\n\nexport type ProbePolicyResult =\n | { allowed: true }\n | { allowed: false; code: 'invalid_url' | 'always_blocked' | 'private_address'; reason: string };\n\n/**\n * Decide whether the SDK should issue a discovery probe to `url`. Returns\n * `{ allowed: true }` when the probe may proceed, otherwise a refusal\n * with a human-readable reason and a typed code for callers that want\n * to map the refusal into their own error envelope.\n *\n * Designed to be cheap and synchronous — call this BEFORE any try/catch\n * in the discovery loop so refusal can't be silently swallowed and\n * converted into \"host doesn't speak A2A, fall back to MCP\" (the\n * code-reviewer-flagged catch-swallow class from #1618 triage).\n *\n * **Error messages do NOT echo resolved IP addresses** — the `address`\n * field on the rejection carries them when present, but the user-visible\n * text names only the hostname so that compliance reports and log\n * aggregators don't leak internal network topology.\n */\nexport function classifyProbeUrl(url: string): ProbePolicyResult {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n // Don't refuse here — let the underlying call surface its own error\n // (it'll fail with a clearer \"Invalid URL\" downstream). This function\n // is not the URL validator, only the SSRF policy.\n return { allowed: true };\n }\n\n // `URL.hostname` returns IPv6 literals wrapped in brackets; the address\n // classifiers want the bare form.\n const host = parsed.hostname.replace(/^\\[|\\]$/g, '');\n\n // IMDS / IPv6 link-local: ALWAYS refused. Cloud metadata reach is never\n // a legitimate buyer-side use case — refuse even when the operator has\n // explicitly opted into private probes.\n if (isAlwaysBlocked(host)) {\n return {\n allowed: false,\n code: 'always_blocked',\n reason: `Refusing to probe '${host}': cloud-metadata or link-local address.`,\n };\n }\n\n // Loopback: ALWAYS allowed. CLI dev loops, mock-server tests, local\n // adapter integration tests all hit this path. Even in strict mode the\n // attacker would need to be on-host already to exploit it.\n if (isLoopbackHost(host)) {\n return { allowed: true };\n }\n\n // Private/RFC-1918/ULA: refused unless the operator opted in via env.\n // The opt-in is read once at module load — see ALLOW_INTERNAL above.\n if (isPrivateIp(host)) {\n if (ALLOW_INTERNAL) {\n return { allowed: true };\n }\n return {\n allowed: false,\n code: 'private_address',\n reason:\n `Refusing to probe '${host}': private/RFC-1918 address. ` +\n `Set ADCP_ALLOW_INTERNAL_PROBES=1 to allow private-network probes (operator-only).`,\n };\n }\n\n return { allowed: true };\n}\n\n/** Test-only accessor for the env flag — exported for unit-test reset semantics. */\nexport function isInternalProbesAllowed(): boolean {\n return ALLOW_INTERNAL;\n}\n"],"mappings":"AAwCA,SAAS,WAAW,YAAY;AAChC,SAAS,iBAAiB,mBAAmB;AAM7C,MAAM,gBAAgB,IAAI,UAAU;AACpC,cAAc,UAAU,aAAa,GAAG,MAAM;AAC9C,cAAc,WAAW,OAAO,MAAM;AAStC,MAAM,iBAAiB,QAAQ,IAAI,+BAA+B;AAalE,SAAS,eAAe,MAAuB;AAC7C,MAAI,KAAK,YAAY,MAAM,YAAa,QAAO;AAG/C,MAAI,OAAO;AACX,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,EAAG,QAAO,KAAK,MAAM,GAAG,EAAE;AACvE,QAAM,SAAS,KAAK,IAAI;AACxB,MAAI,WAAW,EAAG,QAAO,cAAc,MAAM,MAAM,MAAM;AACzD,MAAI,WAAW,EAAG,QAAO,cAAc,MAAM,MAAM,MAAM;AACzD,SAAO;AACT;
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/utils/probe-policy.ts"],"sourcesContent":["/**\n * Probe policy for buyer-side discovery (adcp-client#1618).\n *\n * Whether the SDK is willing to issue an outbound probe to a given URL\n * during agent discovery (`detectProtocol` well-known card fetches,\n * `createTestClient` agent URL acceptance). Sits one layer above the\n * `src/lib/net/address-guards.ts` IP classifiers and the `ssrfSafeFetch`\n * primitive — those operate on already-resolved addresses; this layer\n * applies a higher-level allow/deny decision keyed on the URL hostname\n * before anything reaches DNS.\n *\n * ## Default policy\n *\n * | Range | Default | Why |\n * | ------------------------------------------- | ------- | ------------------------------------------------------- |\n * | Loopback (`127.0.0.0/8`, `::1`, `localhost`)| allow | Local dev loops, mock-server tests, `npm run dev` |\n * | RFC 1918 / link-local / IPv6 ULA | deny | Internal subnets behind a server-side comply runner |\n * | Cloud metadata (`169.254.169.254`, `fe80::/10`) | deny | IMDS exfiltration is never a legitimate buyer use case |\n * | Public IPv4/IPv6 | allow | The whole point |\n *\n * ## Single opt-out\n *\n * `ADCP_ALLOW_INTERNAL_PROBES=1` (read once at module load) widens the\n * default to allow RFC-1918/link-local/ULA. IMDS stays blocked even with\n * this flag — cloud metadata reach is never legitimate.\n *\n * NOT controlled by `NODE_ENV`: a multi-tenant staging image with\n * `NODE_ENV=test` MUST NOT inherit looser SSRF posture than production.\n *\n * ## Known gap (TOCTOU) — adcp-client#1627\n *\n * `classifyProbeUrl` does NOT do DNS resolution — it inspects the\n * URL's hostname literal. A hostname like `evil.example.com` that\n * resolves to `169.254.169.254` will pass this gate; the per-IP block\n * inside `ssrfSafeFetch` (which DOES resolve and pin) catches it.\n * Use both: this gate refuses obvious literal attacks; `ssrfSafeFetch`\n * refuses DNS-based attacks. Routing `detectProtocol` through\n * `ssrfSafeFetch` for full TOCTOU defense is tracked under #1627.\n */\n\nimport { BlockList, isIP } from 'node:net';\nimport { isAlwaysBlocked, isPrivateIp } from '../net/address-guards';\n\n// Loopback canonicalization. Node's URL parser canonicalizes IPv4-mapped\n// IPv6 (`::ffff:127.0.0.1`) into binary form (`::ffff:7f00:1`), which\n// would slip past a textual regex. `BlockList` handles all canonical forms\n// natively, including IPv4-mapped IPv6 against the v4 subnet.\nconst loopbackBlock = new BlockList();\nloopbackBlock.addSubnet('127.0.0.0', 8, 'ipv4');\nloopbackBlock.addAddress('::1', 'ipv6');\n\n/**\n * Operator opt-in to allow RFC-1918 / link-local / ULA destinations.\n * Read once at module load — runtime mutation does not propagate. Operators\n * who flip this in CI should set it via the workflow `env:` block.\n *\n * IMDS stays refused regardless.\n */\nconst ALLOW_INTERNAL = process.env.ADCP_ALLOW_INTERNAL_PROBES === '1';\n\n/**\n * Match a host literal that resolves trivially to loopback. Allowed even\n * in strict mode because local dev loops are not an SSRF target — the\n * attacker would have to be on the buyer's own machine to reach them, at\n * which point they have strictly more powerful primitives.\n *\n * `localhost` is hardcoded rather than DNS-resolved so an attacker can't\n * point `localhost.attacker.example.com` at `169.254.169.254` and slip\n * past as \"loopback-named\". Hostname must be exactly `localhost` (case\n * insensitive). IP literals must be in 127/8 or `::1`.\n */\nfunction isLoopbackHost(host: string): boolean {\n if (host.toLowerCase() === 'localhost') return true;\n // Strip URL-style brackets in case the caller didn't (defensive — internal\n // callers strip first).\n let bare = host;\n if (bare.startsWith('[') && bare.endsWith(']')) bare = bare.slice(1, -1);\n const family = isIP(bare);\n if (family === 4) return loopbackBlock.check(bare, 'ipv4');\n if (family === 6) return loopbackBlock.check(bare, 'ipv6');\n return false;\n}\n\nexport type ProbePolicyResult =\n | { allowed: true }\n | { allowed: false; code: 'invalid_url' | 'always_blocked' | 'private_address'; reason: string };\n\n/**\n * Decide whether the SDK should issue a discovery probe to `url`. Returns\n * `{ allowed: true }` when the probe may proceed, otherwise a refusal\n * with a human-readable reason and a typed code for callers that want\n * to map the refusal into their own error envelope.\n *\n * Designed to be cheap and synchronous — call this BEFORE any try/catch\n * in the discovery loop so refusal can't be silently swallowed and\n * converted into \"host doesn't speak A2A, fall back to MCP\" (the\n * code-reviewer-flagged catch-swallow class from #1618 triage).\n *\n * **Error messages do NOT echo resolved IP addresses** — the `address`\n * field on the rejection carries them when present, but the user-visible\n * text names only the hostname so that compliance reports and log\n * aggregators don't leak internal network topology.\n */\n/**\n * Cloud-metadata hostnames, lowercased. Registered names rather than IP\n * literals, so the CIDR classifiers in `net/address-guards` cannot see them.\n */\nconst METADATA_HOSTNAMES: ReadonlySet<string> = new Set([\n 'metadata',\n 'metadata.google.internal',\n 'metadata.goog',\n 'metadata.packet.net',\n 'instance-data',\n]);\n\nexport function classifyProbeUrl(url: string): ProbePolicyResult {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n // Don't refuse here — let the underlying call surface its own error\n // (it'll fail with a clearer \"Invalid URL\" downstream). This function\n // is not the URL validator, only the SSRF policy.\n return { allowed: true };\n }\n\n // `URL.hostname` returns IPv6 literals wrapped in brackets; the address\n // classifiers want the bare form. A fully-qualified name keeps its root dot\n // (`localhost.`, `metadata.google.internal.`) and resolves identically, so\n // strip it before any name comparison.\n const host = parsed.hostname.replace(/^\\[|\\]$/g, '').replace(/\\.$/, '');\n\n // Cloud metadata by NAME. These are ordinary registered names, so no CIDR\n // classifier sees them — `metadata.google.internal` is the second-most-used\n // IMDS target after 169.254.169.254 and needs no IP literal at all. Refused\n // even with the opt-in, same as the address-literal IMDS ranges below.\n // Mirrors `WEBHOOK_SSRF_POLICY.hosts_denied_metadata`.\n if (METADATA_HOSTNAMES.has(host)) {\n return {\n allowed: false,\n code: 'always_blocked',\n reason: `Refusing to probe '${host}': cloud-metadata hostname.`,\n };\n }\n\n // IMDS / IPv6 link-local: ALWAYS refused. Cloud metadata reach is never\n // a legitimate buyer-side use case — refuse even when the operator has\n // explicitly opted into private probes.\n if (isAlwaysBlocked(host)) {\n return {\n allowed: false,\n code: 'always_blocked',\n reason: `Refusing to probe '${host}': cloud-metadata or link-local address.`,\n };\n }\n\n // Loopback: ALWAYS allowed. CLI dev loops, mock-server tests, local\n // adapter integration tests all hit this path. Even in strict mode the\n // attacker would need to be on-host already to exploit it.\n if (isLoopbackHost(host)) {\n return { allowed: true };\n }\n\n // Private/RFC-1918/ULA: refused unless the operator opted in via env.\n // The opt-in is read once at module load — see ALLOW_INTERNAL above.\n if (isPrivateIp(host)) {\n if (ALLOW_INTERNAL) {\n return { allowed: true };\n }\n return {\n allowed: false,\n code: 'private_address',\n reason:\n `Refusing to probe '${host}': private/RFC-1918 address. ` +\n `Set ADCP_ALLOW_INTERNAL_PROBES=1 to allow private-network probes (operator-only).`,\n };\n }\n\n return { allowed: true };\n}\n\n/** Test-only accessor for the env flag — exported for unit-test reset semantics. */\nexport function isInternalProbesAllowed(): boolean {\n return ALLOW_INTERNAL;\n}\n"],"mappings":"AAwCA,SAAS,WAAW,YAAY;AAChC,SAAS,iBAAiB,mBAAmB;AAM7C,MAAM,gBAAgB,IAAI,UAAU;AACpC,cAAc,UAAU,aAAa,GAAG,MAAM;AAC9C,cAAc,WAAW,OAAO,MAAM;AAStC,MAAM,iBAAiB,QAAQ,IAAI,+BAA+B;AAalE,SAAS,eAAe,MAAuB;AAC7C,MAAI,KAAK,YAAY,MAAM,YAAa,QAAO;AAG/C,MAAI,OAAO;AACX,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,EAAG,QAAO,KAAK,MAAM,GAAG,EAAE;AACvE,QAAM,SAAS,KAAK,IAAI;AACxB,MAAI,WAAW,EAAG,QAAO,cAAc,MAAM,MAAM,MAAM;AACzD,MAAI,WAAW,EAAG,QAAO,cAAc,MAAM,MAAM,MAAM;AACzD,SAAO;AACT;AA0BA,MAAM,qBAA0C,oBAAI,IAAI;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,iBAAiB,KAAgC;AAC/D,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AAIN,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AAMA,QAAM,OAAO,OAAO,SAAS,QAAQ,YAAY,EAAE,EAAE,QAAQ,OAAO,EAAE;AAOtE,MAAI,mBAAmB,IAAI,IAAI,GAAG;AAChC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,QAAQ,sBAAsB,IAAI;AAAA,IACpC;AAAA,EACF;AAKA,MAAI,gBAAgB,IAAI,GAAG;AACzB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,QAAQ,sBAAsB,IAAI;AAAA,IACpC;AAAA,EACF;AAKA,MAAI,eAAe,IAAI,GAAG;AACxB,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AAIA,MAAI,YAAY,IAAI,GAAG;AACrB,QAAI,gBAAgB;AAClB,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AACA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,QACE,sBAAsB,IAAI;AAAA,IAE9B;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,KAAK;AACzB;AAGO,SAAS,0BAAmC;AACjD,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared redaction for tool arguments that reach caller-visible debug logs.
|
|
3
|
+
*
|
|
4
|
+
* `debug_logs` are returned on `TaskResult` and handed to adopter-supplied log
|
|
5
|
+
* sinks, so anything serialized into them can end up in ordinary application
|
|
6
|
+
* logs, CI output, and log aggregators.
|
|
7
|
+
*
|
|
8
|
+
* The credentials that actually travel in AdCP tool arguments are the webhook
|
|
9
|
+
* registrations, and there are TWO of them:
|
|
10
|
+
*
|
|
11
|
+
* - `push_notification_config` (async task-status notifications) — on most
|
|
12
|
+
* mutating tools.
|
|
13
|
+
* - `reporting_webhook` (automated delivery reports) — on `create_media_buy`.
|
|
14
|
+
*
|
|
15
|
+
* Both carry an `authentication.credentials` HMAC shared secret and a `token`
|
|
16
|
+
* the receiver must echo, and `reporting-webhook.json` makes `authentication`
|
|
17
|
+
* REQUIRED for all of AdCP 3.x — so the reporting registration is the one most
|
|
18
|
+
* certain to be populated on the call most likely to be logged. Masking only
|
|
19
|
+
* the push config would leave that secret in plaintext.
|
|
20
|
+
*
|
|
21
|
+
* `idempotency_key` is masked too (a retry-pattern oracle within the seller's
|
|
22
|
+
* TTL; opt out with `ADCP_LOG_IDEMPOTENCY_KEYS=1`).
|
|
23
|
+
*
|
|
24
|
+
* Deliberately NOT masked: nested `context` / `ext` values. Credentials must
|
|
25
|
+
* never be placed there in the first place — see
|
|
26
|
+
* `docs/guides/CTX-METADATA-SAFETY.md` — and masking them would legitimize the
|
|
27
|
+
* channel as a credential carrier.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Mask webhook credentials and the idempotency key in a tool-argument object.
|
|
31
|
+
*
|
|
32
|
+
* The whole `authentication` block is replaced rather than just its
|
|
33
|
+
* `credentials` field: the scheme list carries no secret, but replacing the
|
|
34
|
+
* object wholesale means a future field addition cannot silently start leaking.
|
|
35
|
+
*/
|
|
36
|
+
export declare function redactArgsForLog<T extends Record<string, unknown>>(args: T): T;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared redaction for tool arguments that reach caller-visible debug logs.
|
|
3
|
+
*
|
|
4
|
+
* `debug_logs` are returned on `TaskResult` and handed to adopter-supplied log
|
|
5
|
+
* sinks, so anything serialized into them can end up in ordinary application
|
|
6
|
+
* logs, CI output, and log aggregators.
|
|
7
|
+
*
|
|
8
|
+
* The credentials that actually travel in AdCP tool arguments are the webhook
|
|
9
|
+
* registrations, and there are TWO of them:
|
|
10
|
+
*
|
|
11
|
+
* - `push_notification_config` (async task-status notifications) — on most
|
|
12
|
+
* mutating tools.
|
|
13
|
+
* - `reporting_webhook` (automated delivery reports) — on `create_media_buy`.
|
|
14
|
+
*
|
|
15
|
+
* Both carry an `authentication.credentials` HMAC shared secret and a `token`
|
|
16
|
+
* the receiver must echo, and `reporting-webhook.json` makes `authentication`
|
|
17
|
+
* REQUIRED for all of AdCP 3.x — so the reporting registration is the one most
|
|
18
|
+
* certain to be populated on the call most likely to be logged. Masking only
|
|
19
|
+
* the push config would leave that secret in plaintext.
|
|
20
|
+
*
|
|
21
|
+
* `idempotency_key` is masked too (a retry-pattern oracle within the seller's
|
|
22
|
+
* TTL; opt out with `ADCP_LOG_IDEMPOTENCY_KEYS=1`).
|
|
23
|
+
*
|
|
24
|
+
* Deliberately NOT masked: nested `context` / `ext` values. Credentials must
|
|
25
|
+
* never be placed there in the first place — see
|
|
26
|
+
* `docs/guides/CTX-METADATA-SAFETY.md` — and masking them would legitimize the
|
|
27
|
+
* channel as a credential carrier.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Mask webhook credentials and the idempotency key in a tool-argument object.
|
|
31
|
+
*
|
|
32
|
+
* The whole `authentication` block is replaced rather than just its
|
|
33
|
+
* `credentials` field: the scheme list carries no secret, but replacing the
|
|
34
|
+
* object wholesale means a future field addition cannot silently start leaking.
|
|
35
|
+
*/
|
|
36
|
+
export declare function redactArgsForLog<T extends Record<string, unknown>>(args: T): T;
|
|
37
|
+
//# sourceMappingURL=redact-args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact-args.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/redact-args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAeH;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAe9E"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var redact_args_exports = {};
|
|
20
|
+
__export(redact_args_exports, {
|
|
21
|
+
redactArgsForLog: () => redactArgsForLog
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(redact_args_exports);
|
|
24
|
+
var import_idempotency = require('./idempotency.js');
|
|
25
|
+
const MASK = "***";
|
|
26
|
+
const WEBHOOK_CONFIG_FIELDS = ["push_notification_config", "reporting_webhook"];
|
|
27
|
+
const CREDENTIAL_FIELDS = ["authentication", "token"];
|
|
28
|
+
function redactArgsForLog(args) {
|
|
29
|
+
let redacted = args;
|
|
30
|
+
for (const field of WEBHOOK_CONFIG_FIELDS) {
|
|
31
|
+
const config = redacted[field];
|
|
32
|
+
if (!config || typeof config !== "object") continue;
|
|
33
|
+
const c = config;
|
|
34
|
+
const masked = { ...c };
|
|
35
|
+
for (const credential of CREDENTIAL_FIELDS) {
|
|
36
|
+
if (c[credential] !== void 0) masked[credential] = MASK;
|
|
37
|
+
}
|
|
38
|
+
redacted = { ...redacted, [field]: masked };
|
|
39
|
+
}
|
|
40
|
+
return (0, import_idempotency.redactIdempotencyKeyInArgs)(redacted);
|
|
41
|
+
}
|
|
42
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
43
|
+
0 && (module.exports = {
|
|
44
|
+
redactArgsForLog
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=redact-args.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/utils/redact-args.ts"],"sourcesContent":["/**\n * Shared redaction for tool arguments that reach caller-visible debug logs.\n *\n * `debug_logs` are returned on `TaskResult` and handed to adopter-supplied log\n * sinks, so anything serialized into them can end up in ordinary application\n * logs, CI output, and log aggregators.\n *\n * The credentials that actually travel in AdCP tool arguments are the webhook\n * registrations, and there are TWO of them:\n *\n * - `push_notification_config` (async task-status notifications) — on most\n * mutating tools.\n * - `reporting_webhook` (automated delivery reports) — on `create_media_buy`.\n *\n * Both carry an `authentication.credentials` HMAC shared secret and a `token`\n * the receiver must echo, and `reporting-webhook.json` makes `authentication`\n * REQUIRED for all of AdCP 3.x — so the reporting registration is the one most\n * certain to be populated on the call most likely to be logged. Masking only\n * the push config would leave that secret in plaintext.\n *\n * `idempotency_key` is masked too (a retry-pattern oracle within the seller's\n * TTL; opt out with `ADCP_LOG_IDEMPOTENCY_KEYS=1`).\n *\n * Deliberately NOT masked: nested `context` / `ext` values. Credentials must\n * never be placed there in the first place — see\n * `docs/guides/CTX-METADATA-SAFETY.md` — and masking them would legitimize the\n * channel as a credential carrier.\n */\n\nimport { redactIdempotencyKeyInArgs } from './idempotency';\n\nconst MASK = '***';\n\n/**\n * Argument fields that hold a webhook registration. Kept as a list so adding a\n * third registration shape is one line rather than a new code path.\n */\nconst WEBHOOK_CONFIG_FIELDS = ['push_notification_config', 'reporting_webhook'] as const;\n\n/** Credential-bearing fields within a webhook registration. */\nconst CREDENTIAL_FIELDS = ['authentication', 'token'] as const;\n\n/**\n * Mask webhook credentials and the idempotency key in a tool-argument object.\n *\n * The whole `authentication` block is replaced rather than just its\n * `credentials` field: the scheme list carries no secret, but replacing the\n * object wholesale means a future field addition cannot silently start leaking.\n */\nexport function redactArgsForLog<T extends Record<string, unknown>>(args: T): T {\n let redacted: Record<string, unknown> = args;\n\n for (const field of WEBHOOK_CONFIG_FIELDS) {\n const config = redacted[field];\n if (!config || typeof config !== 'object') continue;\n const c = config as Record<string, unknown>;\n const masked: Record<string, unknown> = { ...c };\n for (const credential of CREDENTIAL_FIELDS) {\n if (c[credential] !== undefined) masked[credential] = MASK;\n }\n redacted = { ...redacted, [field]: masked };\n }\n\n return redactIdempotencyKeyInArgs(redacted) as T;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BA,yBAA2C;AAE3C,MAAM,OAAO;AAMb,MAAM,wBAAwB,CAAC,4BAA4B,mBAAmB;AAG9E,MAAM,oBAAoB,CAAC,kBAAkB,OAAO;AAS7C,SAAS,iBAAoD,MAAY;AAC9E,MAAI,WAAoC;AAExC,aAAW,SAAS,uBAAuB;AACzC,UAAM,SAAS,SAAS,KAAK;AAC7B,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU;AAC3C,UAAM,IAAI;AACV,UAAM,SAAkC,EAAE,GAAG,EAAE;AAC/C,eAAW,cAAc,mBAAmB;AAC1C,UAAI,EAAE,UAAU,MAAM,OAAW,QAAO,UAAU,IAAI;AAAA,IACxD;AACA,eAAW,EAAE,GAAG,UAAU,CAAC,KAAK,GAAG,OAAO;AAAA,EAC5C;AAEA,aAAO,+CAA2B,QAAQ;AAC5C;","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { redactIdempotencyKeyInArgs } from "./idempotency.mjs";
|
|
2
|
+
const MASK = "***";
|
|
3
|
+
const WEBHOOK_CONFIG_FIELDS = ["push_notification_config", "reporting_webhook"];
|
|
4
|
+
const CREDENTIAL_FIELDS = ["authentication", "token"];
|
|
5
|
+
function redactArgsForLog(args) {
|
|
6
|
+
let redacted = args;
|
|
7
|
+
for (const field of WEBHOOK_CONFIG_FIELDS) {
|
|
8
|
+
const config = redacted[field];
|
|
9
|
+
if (!config || typeof config !== "object") continue;
|
|
10
|
+
const c = config;
|
|
11
|
+
const masked = { ...c };
|
|
12
|
+
for (const credential of CREDENTIAL_FIELDS) {
|
|
13
|
+
if (c[credential] !== void 0) masked[credential] = MASK;
|
|
14
|
+
}
|
|
15
|
+
redacted = { ...redacted, [field]: masked };
|
|
16
|
+
}
|
|
17
|
+
return redactIdempotencyKeyInArgs(redacted);
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
redactArgsForLog
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=redact-args.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/utils/redact-args.ts"],"sourcesContent":["/**\n * Shared redaction for tool arguments that reach caller-visible debug logs.\n *\n * `debug_logs` are returned on `TaskResult` and handed to adopter-supplied log\n * sinks, so anything serialized into them can end up in ordinary application\n * logs, CI output, and log aggregators.\n *\n * The credentials that actually travel in AdCP tool arguments are the webhook\n * registrations, and there are TWO of them:\n *\n * - `push_notification_config` (async task-status notifications) — on most\n * mutating tools.\n * - `reporting_webhook` (automated delivery reports) — on `create_media_buy`.\n *\n * Both carry an `authentication.credentials` HMAC shared secret and a `token`\n * the receiver must echo, and `reporting-webhook.json` makes `authentication`\n * REQUIRED for all of AdCP 3.x — so the reporting registration is the one most\n * certain to be populated on the call most likely to be logged. Masking only\n * the push config would leave that secret in plaintext.\n *\n * `idempotency_key` is masked too (a retry-pattern oracle within the seller's\n * TTL; opt out with `ADCP_LOG_IDEMPOTENCY_KEYS=1`).\n *\n * Deliberately NOT masked: nested `context` / `ext` values. Credentials must\n * never be placed there in the first place — see\n * `docs/guides/CTX-METADATA-SAFETY.md` — and masking them would legitimize the\n * channel as a credential carrier.\n */\n\nimport { redactIdempotencyKeyInArgs } from './idempotency';\n\nconst MASK = '***';\n\n/**\n * Argument fields that hold a webhook registration. Kept as a list so adding a\n * third registration shape is one line rather than a new code path.\n */\nconst WEBHOOK_CONFIG_FIELDS = ['push_notification_config', 'reporting_webhook'] as const;\n\n/** Credential-bearing fields within a webhook registration. */\nconst CREDENTIAL_FIELDS = ['authentication', 'token'] as const;\n\n/**\n * Mask webhook credentials and the idempotency key in a tool-argument object.\n *\n * The whole `authentication` block is replaced rather than just its\n * `credentials` field: the scheme list carries no secret, but replacing the\n * object wholesale means a future field addition cannot silently start leaking.\n */\nexport function redactArgsForLog<T extends Record<string, unknown>>(args: T): T {\n let redacted: Record<string, unknown> = args;\n\n for (const field of WEBHOOK_CONFIG_FIELDS) {\n const config = redacted[field];\n if (!config || typeof config !== 'object') continue;\n const c = config as Record<string, unknown>;\n const masked: Record<string, unknown> = { ...c };\n for (const credential of CREDENTIAL_FIELDS) {\n if (c[credential] !== undefined) masked[credential] = MASK;\n }\n redacted = { ...redacted, [field]: masked };\n }\n\n return redactIdempotencyKeyInArgs(redacted) as T;\n}\n"],"mappings":"AA6BA,SAAS,kCAAkC;AAE3C,MAAM,OAAO;AAMb,MAAM,wBAAwB,CAAC,4BAA4B,mBAAmB;AAG9E,MAAM,oBAAoB,CAAC,kBAAkB,OAAO;AAS7C,SAAS,iBAAoD,MAAY;AAC9E,MAAI,WAAoC;AAExC,aAAW,SAAS,uBAAuB;AACzC,UAAM,SAAS,SAAS,KAAK;AAC7B,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU;AAC3C,UAAM,IAAI;AACV,UAAM,SAAkC,EAAE,GAAG,EAAE;AAC/C,eAAW,cAAc,mBAAmB;AAC1C,UAAI,EAAE,UAAU,MAAM,OAAW,QAAO,UAAU,IAAI;AAAA,IACxD;AACA,eAAW,EAAE,GAAG,UAAU,CAAC,KAAK,GAAG,OAAO;AAAA,EAC5C;AAEA,SAAO,2BAA2B,QAAQ;AAC5C;","names":[]}
|
|
@@ -551,11 +551,7 @@ function projectPackageSelectors(pkg, wireMode, operation, legacyFormatConverter
|
|
|
551
551
|
return ref ? [ref] : [];
|
|
552
552
|
});
|
|
553
553
|
if (projected.diagnostics.length > 0 || projectedRefs.length !== legacyRefs.length) {
|
|
554
|
-
|
|
555
|
-
operation,
|
|
556
|
-
"(package selector)",
|
|
557
|
-
"legacy package format_ids cannot be converted to stable canonical format_option_refs"
|
|
558
|
-
);
|
|
554
|
+
return next;
|
|
559
555
|
}
|
|
560
556
|
mappedRefs = projectedRefs;
|
|
561
557
|
}
|