@adcp/sdk 13.0.0-rc.3 → 13.0.0-rc.5

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.
Files changed (211) hide show
  1. package/README.md +18 -15
  2. package/bin/adcp.js +7 -1
  3. package/dist/lib/adapters/property-list-adapter.d.mts +5 -1
  4. package/dist/lib/adapters/property-list-adapter.d.ts +5 -1
  5. package/dist/lib/adapters/property-list-adapter.d.ts.map +1 -1
  6. package/dist/lib/adapters/property-list-adapter.js +7 -7
  7. package/dist/lib/adapters/property-list-adapter.js.map +1 -1
  8. package/dist/lib/adapters/property-list-adapter.mjs +7 -7
  9. package/dist/lib/adapters/property-list-adapter.mjs.map +1 -1
  10. package/dist/lib/auth/oauth/CLIFlowHandler.d.mts +10 -3
  11. package/dist/lib/auth/oauth/CLIFlowHandler.d.ts +10 -3
  12. package/dist/lib/auth/oauth/CLIFlowHandler.d.ts.map +1 -1
  13. package/dist/lib/auth/oauth/CLIFlowHandler.js +40 -4
  14. package/dist/lib/auth/oauth/CLIFlowHandler.js.map +1 -1
  15. package/dist/lib/auth/oauth/CLIFlowHandler.mjs +40 -4
  16. package/dist/lib/auth/oauth/CLIFlowHandler.mjs.map +1 -1
  17. package/dist/lib/auth/oauth/index.d.mts +1 -1
  18. package/dist/lib/auth/oauth/index.d.ts +1 -1
  19. package/dist/lib/auth/oauth/index.d.ts.map +1 -1
  20. package/dist/lib/auth/oauth/index.js +2 -0
  21. package/dist/lib/auth/oauth/index.js.map +1 -1
  22. package/dist/lib/auth/oauth/index.mjs +2 -0
  23. package/dist/lib/auth/oauth/index.mjs.map +1 -1
  24. package/dist/lib/auth/oauth/web-flow.d.mts +12 -0
  25. package/dist/lib/auth/oauth/web-flow.d.ts +12 -0
  26. package/dist/lib/auth/oauth/web-flow.d.ts.map +1 -1
  27. package/dist/lib/auth/oauth/web-flow.js +20 -2
  28. package/dist/lib/auth/oauth/web-flow.js.map +1 -1
  29. package/dist/lib/auth/oauth/web-flow.mjs +19 -2
  30. package/dist/lib/auth/oauth/web-flow.mjs.map +1 -1
  31. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  32. package/dist/lib/core/SingleAgentClient.d.mts +27 -1
  33. package/dist/lib/core/SingleAgentClient.d.ts +27 -1
  34. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  35. package/dist/lib/core/SingleAgentClient.js +46 -18
  36. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  37. package/dist/lib/core/SingleAgentClient.mjs +46 -18
  38. package/dist/lib/core/SingleAgentClient.mjs.map +1 -1
  39. package/dist/lib/index.d.mts +1 -1
  40. package/dist/lib/index.d.ts +1 -1
  41. package/dist/lib/index.d.ts.map +1 -1
  42. package/dist/lib/index.js.map +1 -1
  43. package/dist/lib/index.mjs.map +1 -1
  44. package/dist/lib/net/address-guards.d.ts.map +1 -1
  45. package/dist/lib/net/address-guards.js +4 -0
  46. package/dist/lib/net/address-guards.js.map +1 -1
  47. package/dist/lib/net/address-guards.mjs +4 -0
  48. package/dist/lib/net/address-guards.mjs.map +1 -1
  49. package/dist/lib/protocols/a2a.js +2 -2
  50. package/dist/lib/protocols/a2a.js.map +1 -1
  51. package/dist/lib/protocols/a2a.mjs +2 -2
  52. package/dist/lib/protocols/a2a.mjs.map +1 -1
  53. package/dist/lib/protocols/index.d.ts.map +1 -1
  54. package/dist/lib/protocols/index.js +28 -7
  55. package/dist/lib/protocols/index.js.map +1 -1
  56. package/dist/lib/protocols/index.mjs +28 -7
  57. package/dist/lib/protocols/index.mjs.map +1 -1
  58. package/dist/lib/protocols/mcp-modern.d.ts.map +1 -1
  59. package/dist/lib/protocols/mcp-modern.js +24 -8
  60. package/dist/lib/protocols/mcp-modern.js.map +1 -1
  61. package/dist/lib/protocols/mcp-modern.mjs +24 -8
  62. package/dist/lib/protocols/mcp-modern.mjs.map +1 -1
  63. package/dist/lib/protocols/mcp-tasks.d.ts.map +1 -1
  64. package/dist/lib/protocols/mcp-tasks.js +2 -12
  65. package/dist/lib/protocols/mcp-tasks.js.map +1 -1
  66. package/dist/lib/protocols/mcp-tasks.mjs +2 -12
  67. package/dist/lib/protocols/mcp-tasks.mjs.map +1 -1
  68. package/dist/lib/protocols/mcp.js +2 -2
  69. package/dist/lib/protocols/mcp.js.map +1 -1
  70. package/dist/lib/protocols/mcp.mjs +2 -2
  71. package/dist/lib/protocols/mcp.mjs.map +1 -1
  72. package/dist/lib/registry/index.d.mts +5 -25
  73. package/dist/lib/registry/index.d.ts +5 -25
  74. package/dist/lib/registry/index.d.ts.map +1 -1
  75. package/dist/lib/registry/index.js +5 -130
  76. package/dist/lib/registry/index.js.map +1 -1
  77. package/dist/lib/registry/index.mjs +5 -130
  78. package/dist/lib/registry/index.mjs.map +1 -1
  79. package/dist/lib/registry/sync.d.mts +8 -2
  80. package/dist/lib/registry/sync.d.ts +8 -2
  81. package/dist/lib/registry/sync.d.ts.map +1 -1
  82. package/dist/lib/registry/sync.js +8 -2
  83. package/dist/lib/registry/sync.js.map +1 -1
  84. package/dist/lib/registry/sync.mjs +8 -2
  85. package/dist/lib/registry/sync.mjs.map +1 -1
  86. package/dist/lib/registry/types.d.mts +15 -29
  87. package/dist/lib/registry/types.d.ts +15 -29
  88. package/dist/lib/registry/types.d.ts.map +1 -1
  89. package/dist/lib/registry/types.generated.d.mts +567 -15
  90. package/dist/lib/registry/types.generated.d.ts +567 -15
  91. package/dist/lib/registry/types.generated.d.ts.map +1 -1
  92. package/dist/lib/registry/types.generated.js.map +1 -1
  93. package/dist/lib/registry/types.js.map +1 -1
  94. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  95. package/dist/lib/server/decisioning/account.d.mts +6 -4
  96. package/dist/lib/server/decisioning/account.d.ts +6 -4
  97. package/dist/lib/server/decisioning/account.d.ts.map +1 -1
  98. package/dist/lib/server/decisioning/account.js.map +1 -1
  99. package/dist/lib/server/decisioning/account.mjs.map +1 -1
  100. package/dist/lib/server/decisioning/manifest-helpers.d.mts +6 -5
  101. package/dist/lib/server/decisioning/manifest-helpers.d.ts +6 -5
  102. package/dist/lib/server/decisioning/manifest-helpers.d.ts.map +1 -1
  103. package/dist/lib/server/decisioning/manifest-helpers.js.map +1 -1
  104. package/dist/lib/server/decisioning/manifest-helpers.mjs.map +1 -1
  105. package/dist/lib/server/decisioning/runtime/from-platform.js +4 -2
  106. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  107. package/dist/lib/server/decisioning/runtime/from-platform.mjs +4 -2
  108. package/dist/lib/server/decisioning/runtime/from-platform.mjs.map +1 -1
  109. package/dist/lib/server/decisioning/tenant-store.d.mts +26 -14
  110. package/dist/lib/server/decisioning/tenant-store.d.ts +26 -14
  111. package/dist/lib/server/decisioning/tenant-store.d.ts.map +1 -1
  112. package/dist/lib/server/decisioning/tenant-store.js +6 -1
  113. package/dist/lib/server/decisioning/tenant-store.js.map +1 -1
  114. package/dist/lib/server/decisioning/tenant-store.mjs +6 -1
  115. package/dist/lib/server/decisioning/tenant-store.mjs.map +1 -1
  116. package/dist/lib/server/pin-and-bind-fetch.d.mts +2 -0
  117. package/dist/lib/server/pin-and-bind-fetch.d.ts +2 -0
  118. package/dist/lib/server/pin-and-bind-fetch.d.ts.map +1 -1
  119. package/dist/lib/server/pin-and-bind-fetch.js +2 -0
  120. package/dist/lib/server/pin-and-bind-fetch.js.map +1 -1
  121. package/dist/lib/server/pin-and-bind-fetch.mjs +2 -0
  122. package/dist/lib/server/pin-and-bind-fetch.mjs.map +1 -1
  123. package/dist/lib/server/structured-serialize.d.ts.map +1 -1
  124. package/dist/lib/server/structured-serialize.js +10 -2
  125. package/dist/lib/server/structured-serialize.js.map +1 -1
  126. package/dist/lib/server/structured-serialize.mjs +10 -2
  127. package/dist/lib/server/structured-serialize.mjs.map +1 -1
  128. package/dist/lib/server/webhook-emitter.d.ts.map +1 -1
  129. package/dist/lib/server/webhook-emitter.js +4 -2
  130. package/dist/lib/server/webhook-emitter.js.map +1 -1
  131. package/dist/lib/server/webhook-emitter.mjs +4 -2
  132. package/dist/lib/server/webhook-emitter.mjs.map +1 -1
  133. package/dist/lib/signing/middleware.d.mts +2 -1
  134. package/dist/lib/signing/middleware.d.ts +2 -1
  135. package/dist/lib/signing/middleware.d.ts.map +1 -1
  136. package/dist/lib/signing/middleware.js +26 -4
  137. package/dist/lib/signing/middleware.js.map +1 -1
  138. package/dist/lib/signing/middleware.mjs +26 -4
  139. package/dist/lib/signing/middleware.mjs.map +1 -1
  140. package/dist/lib/signing/webhook-verifier.d.ts.map +1 -1
  141. package/dist/lib/signing/webhook-verifier.js +4 -2
  142. package/dist/lib/signing/webhook-verifier.js.map +1 -1
  143. package/dist/lib/signing/webhook-verifier.mjs +4 -2
  144. package/dist/lib/signing/webhook-verifier.mjs.map +1 -1
  145. package/dist/lib/testing/agent-tester.d.ts.map +1 -1
  146. package/dist/lib/testing/agent-tester.js +46 -1
  147. package/dist/lib/testing/agent-tester.js.map +1 -1
  148. package/dist/lib/testing/agent-tester.mjs +46 -1
  149. package/dist/lib/testing/agent-tester.mjs.map +1 -1
  150. package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
  151. package/dist/lib/testing/storyboard/runner.js +40 -6
  152. package/dist/lib/testing/storyboard/runner.js.map +1 -1
  153. package/dist/lib/testing/storyboard/runner.mjs +40 -6
  154. package/dist/lib/testing/storyboard/runner.mjs.map +1 -1
  155. package/dist/lib/types/asset-instances.d.mts +8 -6
  156. package/dist/lib/types/asset-instances.d.ts +8 -6
  157. package/dist/lib/types/asset-instances.d.ts.map +1 -1
  158. package/dist/lib/types/asset-instances.js.map +1 -1
  159. package/dist/lib/types/core.generated.d.mts +105 -58
  160. package/dist/lib/types/core.generated.d.ts +105 -58
  161. package/dist/lib/types/core.generated.d.ts.map +1 -1
  162. package/dist/lib/types/core.generated.js.map +1 -1
  163. package/dist/lib/types/index.d.ts.map +1 -1
  164. package/dist/lib/types/index.js.map +1 -1
  165. package/dist/lib/types/index.mjs.map +1 -1
  166. package/dist/lib/types/schemas.generated.d.mts +2352 -473
  167. package/dist/lib/types/schemas.generated.d.ts +2352 -473
  168. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  169. package/dist/lib/types/schemas.generated.js +78 -45
  170. package/dist/lib/types/schemas.generated.js.map +1 -1
  171. package/dist/lib/types/schemas.generated.mjs +71 -45
  172. package/dist/lib/types/schemas.generated.mjs.map +1 -1
  173. package/dist/lib/types/tools.generated.d.mts +48 -44
  174. package/dist/lib/types/tools.generated.d.ts +48 -44
  175. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  176. package/dist/lib/types/tools.generated.js.map +1 -1
  177. package/dist/lib/utils/probe-policy.d.mts +0 -16
  178. package/dist/lib/utils/probe-policy.d.ts +0 -16
  179. package/dist/lib/utils/probe-policy.d.ts.map +1 -1
  180. package/dist/lib/utils/probe-policy.js +15 -1
  181. package/dist/lib/utils/probe-policy.js.map +1 -1
  182. package/dist/lib/utils/probe-policy.mjs +15 -1
  183. package/dist/lib/utils/probe-policy.mjs.map +1 -1
  184. package/dist/lib/utils/redact-args.d.mts +36 -0
  185. package/dist/lib/utils/redact-args.d.ts +37 -0
  186. package/dist/lib/utils/redact-args.d.ts.map +1 -0
  187. package/dist/lib/utils/redact-args.js +46 -0
  188. package/dist/lib/utils/redact-args.js.map +1 -0
  189. package/dist/lib/utils/redact-args.mjs +22 -0
  190. package/dist/lib/utils/redact-args.mjs.map +1 -0
  191. package/dist/lib/v2/projection/creative-delivery.js +1 -5
  192. package/dist/lib/v2/projection/creative-delivery.js.map +1 -1
  193. package/dist/lib/v2/projection/creative-delivery.mjs +1 -5
  194. package/dist/lib/v2/projection/creative-delivery.mjs.map +1 -1
  195. package/dist/lib/validation/index.d.mts +15 -1
  196. package/dist/lib/validation/index.d.ts +15 -1
  197. package/dist/lib/validation/index.d.ts.map +1 -1
  198. package/dist/lib/validation/index.js +4 -8
  199. package/dist/lib/validation/index.js.map +1 -1
  200. package/dist/lib/validation/index.mjs +4 -8
  201. package/dist/lib/validation/index.mjs.map +1 -1
  202. package/dist/lib/version.d.mts +3 -3
  203. package/dist/lib/version.d.ts +3 -3
  204. package/dist/lib/version.js +3 -3
  205. package/dist/lib/version.js.map +1 -1
  206. package/dist/lib/version.mjs +3 -3
  207. package/dist/lib/version.mjs.map +1 -1
  208. package/examples/hello_seller_adapter_multi_tenant.ts +7 -0
  209. package/package.json +6 -5
  210. package/skills/build-holdco-agent/SKILL.md +19 -0
  211. package/skills/cross-cutting.md +3 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/server/pin-and-bind-fetch.ts"],"sourcesContent":["/**\n * Pin-and-bind fetch — DNS-rebinding-resistant `fetch` for outbound webhook\n * delivery and other callbacks where the destination is buyer-supplied.\n *\n * The problem: validating a `push_notification_config.url`'s LITERAL hostname\n * against an SSRF deny list does not protect against a DNS-rebinding attack.\n * A buyer can register `https://rebind.attacker.com/`, pass the literal-host\n * check, and then flip the A-record TTL to `169.254.169.254` (cloud metadata)\n * or `127.0.0.1` (loopback) before the webhook fires. Node's `fetch` resolves\n * the host fresh at request time, gets the rebound IP, and posts the\n * signed payload to the attacker-controlled destination.\n *\n * The fix: control DNS resolution and connect-target inside the fetch itself.\n * 1. Resolve the hostname.\n * 2. Validate every resolved IP against the SSRF policy (CIDR deny lists,\n * metadata-host names, scheme rules).\n * 3. Pin the connection to the validated IP — undici opens TCP/TLS to that\n * specific address, but the original hostname is preserved for TLS SNI\n * and the `Host:` header so HTTPS routing still works.\n *\n * Implementation note: undici's `Agent` accepts a `connect.lookup` callback\n * with the same signature as `dns.lookup`. We hook the callback, resolve via\n * `dns.lookup({ all: true })` to see EVERY address the host can reach, run\n * the resolved IPs through {@link enforceSsrfPolicyResolved}, and only return\n * a pinned address when every resolved IP is in an allowed range. This\n * matches the AdCP substitution-observer SSRF contract — a host that\n * resolves to *any* denied IP is treated as suspect and rejected wholesale.\n */\n\nimport { Agent, fetch as undiciFetch, type Dispatcher } from 'undici';\nimport { lookup as nativeDnsLookup, type LookupAddress } from 'node:dns';\nimport { isIPv6 } from 'node:net';\n\nimport { enforceSsrfPolicy, enforceSsrfPolicyResolved } from '../substitution/observer/ssrf';\nimport type { SsrfPolicy } from '../substitution/types';\n\n/**\n * Default SSRF policy for outbound webhook delivery. Stricter than\n * `DEFAULT_SSRF_POLICY` (substitution observer) only in that it allows\n * `host_literal_policy: 'allow'` — webhook URLs MAY use IP literals\n * (e.g. `https://203.0.113.10/cb`) as long as the IP is not in a denied\n * CIDR range. Schemes restricted to https; signed webhooks SHOULD be\n * delivered over TLS.\n *\n * For storyboard / in-process tests where the receiver runs on\n * `http://127.0.0.1:port`, use {@link LOOPBACK_OK_WEBHOOK_SSRF_POLICY}\n * instead. That preset relaxes only the loopback + http rules and keeps\n * every other deny range — adopters get most of the SSRF protection\n * during tests without disabling pin-and-bind entirely.\n */\nexport const WEBHOOK_SSRF_POLICY: SsrfPolicy = Object.freeze({\n schemes_allowed: Object.freeze(['https']),\n schemes_denied: Object.freeze(['http', 'file', 'gopher', 'ftp', 'ftps', 'data', 'javascript', 'about', 'ws', 'wss']),\n hosts_denied_ipv4_cidrs: Object.freeze([\n '0.0.0.0/8',\n '10.0.0.0/8',\n '100.64.0.0/10',\n '127.0.0.0/8',\n '169.254.0.0/16',\n '172.16.0.0/12',\n '192.0.0.0/24',\n '192.168.0.0/16',\n '224.0.0.0/4',\n '240.0.0.0/4',\n ]),\n hosts_denied_ipv6_cidrs: Object.freeze([\n '::1/128',\n '::/128',\n '::ffff:0:0/96',\n '64:ff9b::/96',\n 'fc00::/7',\n 'fe80::/10',\n 'ff00::/8',\n ]),\n hosts_denied_metadata: Object.freeze([\n 'metadata.google.internal',\n 'metadata',\n 'metadata.packet.net',\n 'fd00:ec2::254',\n ]),\n host_literal_policy: 'allow',\n}) as SsrfPolicy;\n\n/**\n * Pin-and-bind policy that allows http URLs and IPv4/IPv6 loopback so\n * adopters can enable pin-and-bind for production webhook delivery while\n * keeping storyboard / in-process tests working — `createWebhookReceiver`\n * listens on `http://127.0.0.1:port`. Every other private CIDR, metadata\n * host, and link-local range is still denied, so loopback is the only\n * relaxation. Pass to `createPinAndBindFetch({ policy })` from your test\n * fixture or storyboard runner harness; do NOT use in production.\n */\nexport const LOOPBACK_OK_WEBHOOK_SSRF_POLICY: SsrfPolicy = Object.freeze({\n schemes_allowed: Object.freeze(['http', 'https']),\n schemes_denied: Object.freeze(['file', 'gopher', 'ftp', 'ftps', 'data', 'javascript', 'about', 'ws', 'wss']),\n hosts_denied_ipv4_cidrs: Object.freeze([\n '0.0.0.0/8',\n '10.0.0.0/8',\n '100.64.0.0/10',\n // 127.0.0.0/8 omitted — loopback allowed for tests.\n '169.254.0.0/16',\n '172.16.0.0/12',\n '192.0.0.0/24',\n '192.168.0.0/16',\n '224.0.0.0/4',\n '240.0.0.0/4',\n ]),\n hosts_denied_ipv6_cidrs: Object.freeze([\n // ::1/128 and ::/128 omitted — IPv6 loopback allowed for tests.\n '::ffff:0:0/96',\n '64:ff9b::/96',\n 'fc00::/7',\n 'fe80::/10',\n 'ff00::/8',\n ]),\n hosts_denied_metadata: Object.freeze([\n 'metadata.google.internal',\n 'metadata',\n 'metadata.packet.net',\n 'fd00:ec2::254',\n ]),\n host_literal_policy: 'allow',\n}) as SsrfPolicy;\n\n/**\n * Signature of the lookup callback `dns.lookup` accepts. Re-declared here\n * because the native type from `node:dns` is a complex overload set; the\n * shape we actually need is the all=true variant the Agent expects.\n */\nexport type DnsLookupAll = (\n hostname: string,\n options: { family?: number; hints?: number; all: true; verbatim?: boolean },\n callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void\n) => void;\n\nexport interface PinAndBindFetchOptions {\n /**\n * SSRF policy to enforce against resolved IPs. Defaults to\n * {@link WEBHOOK_SSRF_POLICY} (https-only, all private/loopback/metadata\n * ranges denied, IP literals allowed).\n */\n policy?: SsrfPolicy;\n /**\n * Override the underlying DNS lookup. Default uses `dns.lookup` with\n * `all: true`. Tests inject a stub to simulate rebinding attacks without\n * touching real DNS.\n */\n lookup?: DnsLookupAll;\n /**\n * Forward to `Agent` — connect-attempt timeout, TLS options, etc. Cannot\n * override `lookup`; that is wired by this helper.\n */\n agentOptions?: Omit<Agent.Options, 'connect'> & {\n connect?: Omit<NonNullable<Agent.Options['connect']>, 'lookup'>;\n };\n}\n\nconst DEFAULT_LOOKUP_ALL: DnsLookupAll = (hostname, options, callback) => {\n nativeDnsLookup(hostname, { ...options, all: true }, (err, addresses) => {\n callback(err, addresses as LookupAddress[]);\n });\n};\n\n/**\n * Build a `fetch` that pins outbound connections to the IPs the SSRF policy\n * allows, defeating DNS-rebinding attacks against per-attempt DNS resolution.\n *\n * This is the default `fetch` for `createWebhookEmitter` /\n * `createAdcpServer({ webhooks })`, so production webhook delivery is\n * rebinding-protected without extra wiring. Call it explicitly only to\n * override the policy — e.g. {@link LOOPBACK_OK_WEBHOOK_SSRF_POLICY} for\n * storyboard tests that deliver to a loopback http receiver. See\n * `docs/guides/SIGNING-GUIDE.md` § Webhook SSRF defense.\n *\n * Construct once per emitter and reuse — each call instantiates a fresh\n * `undici.Agent` with its own connection pool.\n *\n * @example\n * ```ts\n * import { createWebhookEmitter, createPinAndBindFetch } from '@adcp/sdk/server';\n *\n * const emitter = createWebhookEmitter({\n * signerKey: webhookKey,\n * fetch: createPinAndBindFetch(),\n * });\n * ```\n */\nexport function createPinAndBindFetch(options: PinAndBindFetchOptions = {}): typeof fetch {\n const policy = options.policy ?? WEBHOOK_SSRF_POLICY;\n const lookupImpl = options.lookup ?? DEFAULT_LOOKUP_ALL;\n\n const guardedLookup = (\n hostname: string,\n opts: { family?: number; hints?: number; all?: boolean; verbatim?: boolean } | undefined,\n callback: (err: NodeJS.ErrnoException | null, addressOrAll?: string | LookupAddress[], family?: number) => void\n ): void => {\n const wantsAll = opts?.all === true;\n lookupImpl(hostname, { ...(opts ?? {}), all: true }, (err, addresses) => {\n if (err) {\n callback(err);\n return;\n }\n if (!Array.isArray(addresses) || addresses.length === 0) {\n callback(\n makeSsrfError(`DNS resolution returned no addresses for ${hostname}`, 'dns_revalidation:no_addresses')\n );\n return;\n }\n\n // The URL passed in here only matters for scheme + hostname checks,\n // both of which were already validated synchronously by undici when\n // the request started. We re-build a placeholder URL to feed the\n // resolved-address rule, which is the load-bearing check for\n // rebinding defense.\n const url = new URL(`https://${bracketIfV6(hostname)}`);\n const ips = addresses.map(a => a.address);\n const result = enforceSsrfPolicyResolved(url, ips, policy);\n if (!result.allowed) {\n callback(makeSsrfError(result.message ?? 'SSRF policy denied resolved address', result.rule ?? 'ssrf'));\n return;\n }\n\n if (wantsAll) {\n callback(null, addresses);\n return;\n }\n // Pin to the first resolved address. enforceSsrfPolicyResolved is\n // all-or-none: if it allowed the resolution, every entry passed.\n const first = addresses[0]!;\n callback(null, first.address, first.family);\n });\n };\n\n const dispatcher = new Agent({\n ...(options.agentOptions ?? {}),\n connect: {\n ...(options.agentOptions?.connect ?? {}),\n // undici's connect type accepts a lookup with the dns.lookup signature.\n lookup: guardedLookup as unknown as Agent.Options['connect'] extends infer T\n ? T extends { lookup?: infer L }\n ? L\n : never\n : never,\n },\n });\n\n const wrapped = async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]): Promise<Response> => {\n // Synchronous pre-check for the URL's literal scheme + (if it's an IP)\n // its CIDR membership. undici skips `connect.lookup` for IP-literal\n // hostnames, so the resolved-IP path below would never see them.\n // This pre-check enforces the same SSRF policy on URLs like\n // `https://127.0.0.1/cb` or `https://[::1]/cb`.\n const url = resolveRequestUrl(input);\n if (url) {\n const sync = enforceSsrfPolicy(url, policy);\n if (!sync.allowed) {\n throw makeSsrfError(sync.message ?? 'SSRF policy denied URL', sync.rule ?? 'ssrf');\n }\n }\n return undiciFetch(input as Parameters<typeof undiciFetch>[0], {\n ...(init as Parameters<typeof undiciFetch>[1]),\n dispatcher: dispatcher as unknown as Dispatcher,\n }) as unknown as Response;\n };\n\n return wrapped as typeof fetch;\n}\n\nfunction resolveRequestUrl(input: Parameters<typeof fetch>[0]): URL | null {\n try {\n if (typeof input === 'string') return new URL(input);\n if (input instanceof URL) return input;\n if (\n typeof input === 'object' &&\n input !== null &&\n 'url' in input &&\n typeof (input as { url: unknown }).url === 'string'\n ) {\n return new URL((input as { url: string }).url);\n }\n } catch {\n // Let undici surface the parse error in its own shape.\n return null;\n }\n return null;\n}\n\nfunction bracketIfV6(host: string): string {\n return isIPv6(host) ? `[${host}]` : host;\n}\n\nfunction makeSsrfError(message: string, rule: string): NodeJS.ErrnoException {\n const err = new Error(`pin-and-bind: ${rule}: ${message}`) as NodeJS.ErrnoException;\n err.code = 'EADCP_SSRF_BLOCKED';\n return err;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/lib/server/pin-and-bind-fetch.ts"],"sourcesContent":["/**\n * Pin-and-bind fetch — DNS-rebinding-resistant `fetch` for outbound webhook\n * delivery and other callbacks where the destination is buyer-supplied.\n *\n * The problem: validating a `push_notification_config.url`'s LITERAL hostname\n * against an SSRF deny list does not protect against a DNS-rebinding attack.\n * A buyer can register `https://rebind.attacker.com/`, pass the literal-host\n * check, and then flip the A-record TTL to `169.254.169.254` (cloud metadata)\n * or `127.0.0.1` (loopback) before the webhook fires. Node's `fetch` resolves\n * the host fresh at request time, gets the rebound IP, and posts the\n * signed payload to the attacker-controlled destination.\n *\n * The fix: control DNS resolution and connect-target inside the fetch itself.\n * 1. Resolve the hostname.\n * 2. Validate every resolved IP against the SSRF policy (CIDR deny lists,\n * metadata-host names, scheme rules).\n * 3. Pin the connection to the validated IP — undici opens TCP/TLS to that\n * specific address, but the original hostname is preserved for TLS SNI\n * and the `Host:` header so HTTPS routing still works.\n * 4. Never follow redirects. Steps 1-3 only ever see the URL the caller\n * passed, so a followed `Location:` hop would bypass all of them.\n *\n * Implementation note: undici's `Agent` accepts a `connect.lookup` callback\n * with the same signature as `dns.lookup`. We hook the callback, resolve via\n * `dns.lookup({ all: true })` to see EVERY address the host can reach, run\n * the resolved IPs through {@link enforceSsrfPolicyResolved}, and only return\n * a pinned address when every resolved IP is in an allowed range. This\n * matches the AdCP substitution-observer SSRF contract — a host that\n * resolves to *any* denied IP is treated as suspect and rejected wholesale.\n */\n\nimport { Agent, fetch as undiciFetch, type Dispatcher } from 'undici';\nimport { lookup as nativeDnsLookup, type LookupAddress } from 'node:dns';\nimport { isIPv6 } from 'node:net';\n\nimport { enforceSsrfPolicy, enforceSsrfPolicyResolved } from '../substitution/observer/ssrf';\nimport type { SsrfPolicy } from '../substitution/types';\n\n/**\n * Default SSRF policy for outbound webhook delivery. Stricter than\n * `DEFAULT_SSRF_POLICY` (substitution observer) only in that it allows\n * `host_literal_policy: 'allow'` — webhook URLs MAY use IP literals\n * (e.g. `https://203.0.113.10/cb`) as long as the IP is not in a denied\n * CIDR range. Schemes restricted to https; signed webhooks SHOULD be\n * delivered over TLS.\n *\n * For storyboard / in-process tests where the receiver runs on\n * `http://127.0.0.1:port`, use {@link LOOPBACK_OK_WEBHOOK_SSRF_POLICY}\n * instead. That preset relaxes only the loopback + http rules and keeps\n * every other deny range — adopters get most of the SSRF protection\n * during tests without disabling pin-and-bind entirely.\n */\nexport const WEBHOOK_SSRF_POLICY: SsrfPolicy = Object.freeze({\n schemes_allowed: Object.freeze(['https']),\n schemes_denied: Object.freeze(['http', 'file', 'gopher', 'ftp', 'ftps', 'data', 'javascript', 'about', 'ws', 'wss']),\n hosts_denied_ipv4_cidrs: Object.freeze([\n '0.0.0.0/8',\n '10.0.0.0/8',\n '100.64.0.0/10',\n '127.0.0.0/8',\n '169.254.0.0/16',\n '172.16.0.0/12',\n '192.0.0.0/24',\n '192.168.0.0/16',\n '224.0.0.0/4',\n '240.0.0.0/4',\n ]),\n hosts_denied_ipv6_cidrs: Object.freeze([\n '::1/128',\n '::/128',\n '::ffff:0:0/96',\n '64:ff9b::/96',\n 'fc00::/7',\n 'fe80::/10',\n 'ff00::/8',\n ]),\n hosts_denied_metadata: Object.freeze([\n 'metadata.google.internal',\n 'metadata',\n 'metadata.packet.net',\n 'fd00:ec2::254',\n ]),\n host_literal_policy: 'allow',\n}) as SsrfPolicy;\n\n/**\n * Pin-and-bind policy that allows http URLs and IPv4/IPv6 loopback so\n * adopters can enable pin-and-bind for production webhook delivery while\n * keeping storyboard / in-process tests working — `createWebhookReceiver`\n * listens on `http://127.0.0.1:port`. Every other private CIDR, metadata\n * host, and link-local range is still denied, so loopback is the only\n * relaxation. Pass to `createPinAndBindFetch({ policy })` from your test\n * fixture or storyboard runner harness; do NOT use in production.\n */\nexport const LOOPBACK_OK_WEBHOOK_SSRF_POLICY: SsrfPolicy = Object.freeze({\n schemes_allowed: Object.freeze(['http', 'https']),\n schemes_denied: Object.freeze(['file', 'gopher', 'ftp', 'ftps', 'data', 'javascript', 'about', 'ws', 'wss']),\n hosts_denied_ipv4_cidrs: Object.freeze([\n '0.0.0.0/8',\n '10.0.0.0/8',\n '100.64.0.0/10',\n // 127.0.0.0/8 omitted — loopback allowed for tests.\n '169.254.0.0/16',\n '172.16.0.0/12',\n '192.0.0.0/24',\n '192.168.0.0/16',\n '224.0.0.0/4',\n '240.0.0.0/4',\n ]),\n hosts_denied_ipv6_cidrs: Object.freeze([\n // ::1/128 and ::/128 omitted — IPv6 loopback allowed for tests.\n '::ffff:0:0/96',\n '64:ff9b::/96',\n 'fc00::/7',\n 'fe80::/10',\n 'ff00::/8',\n ]),\n hosts_denied_metadata: Object.freeze([\n 'metadata.google.internal',\n 'metadata',\n 'metadata.packet.net',\n 'fd00:ec2::254',\n ]),\n host_literal_policy: 'allow',\n}) as SsrfPolicy;\n\n/**\n * Signature of the lookup callback `dns.lookup` accepts. Re-declared here\n * because the native type from `node:dns` is a complex overload set; the\n * shape we actually need is the all=true variant the Agent expects.\n */\nexport type DnsLookupAll = (\n hostname: string,\n options: { family?: number; hints?: number; all: true; verbatim?: boolean },\n callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void\n) => void;\n\nexport interface PinAndBindFetchOptions {\n /**\n * SSRF policy to enforce against resolved IPs. Defaults to\n * {@link WEBHOOK_SSRF_POLICY} (https-only, all private/loopback/metadata\n * ranges denied, IP literals allowed).\n */\n policy?: SsrfPolicy;\n /**\n * Override the underlying DNS lookup. Default uses `dns.lookup` with\n * `all: true`. Tests inject a stub to simulate rebinding attacks without\n * touching real DNS.\n */\n lookup?: DnsLookupAll;\n /**\n * Forward to `Agent` — connect-attempt timeout, TLS options, etc. Cannot\n * override `lookup`; that is wired by this helper.\n */\n agentOptions?: Omit<Agent.Options, 'connect'> & {\n connect?: Omit<NonNullable<Agent.Options['connect']>, 'lookup'>;\n };\n}\n\nconst DEFAULT_LOOKUP_ALL: DnsLookupAll = (hostname, options, callback) => {\n nativeDnsLookup(hostname, { ...options, all: true }, (err, addresses) => {\n callback(err, addresses as LookupAddress[]);\n });\n};\n\n/**\n * Build a `fetch` that pins outbound connections to the IPs the SSRF policy\n * allows, defeating DNS-rebinding attacks against per-attempt DNS resolution.\n *\n * This is the default `fetch` for `createWebhookEmitter` /\n * `createAdcpServer({ webhooks })`, so production webhook delivery is\n * rebinding-protected without extra wiring. Call it explicitly only to\n * override the policy — e.g. {@link LOOPBACK_OK_WEBHOOK_SSRF_POLICY} for\n * storyboard tests that deliver to a loopback http receiver. See\n * `docs/guides/SIGNING-GUIDE.md` § Webhook SSRF defense.\n *\n * Construct once per emitter and reuse — each call instantiates a fresh\n * `undici.Agent` with its own connection pool.\n *\n * @example\n * ```ts\n * import { createWebhookEmitter, createPinAndBindFetch } from '@adcp/sdk/server';\n *\n * const emitter = createWebhookEmitter({\n * signerKey: webhookKey,\n * fetch: createPinAndBindFetch(),\n * });\n * ```\n */\nexport function createPinAndBindFetch(options: PinAndBindFetchOptions = {}): typeof fetch {\n const policy = options.policy ?? WEBHOOK_SSRF_POLICY;\n const lookupImpl = options.lookup ?? DEFAULT_LOOKUP_ALL;\n\n const guardedLookup = (\n hostname: string,\n opts: { family?: number; hints?: number; all?: boolean; verbatim?: boolean } | undefined,\n callback: (err: NodeJS.ErrnoException | null, addressOrAll?: string | LookupAddress[], family?: number) => void\n ): void => {\n const wantsAll = opts?.all === true;\n lookupImpl(hostname, { ...(opts ?? {}), all: true }, (err, addresses) => {\n if (err) {\n callback(err);\n return;\n }\n if (!Array.isArray(addresses) || addresses.length === 0) {\n callback(\n makeSsrfError(`DNS resolution returned no addresses for ${hostname}`, 'dns_revalidation:no_addresses')\n );\n return;\n }\n\n // The URL passed in here only matters for scheme + hostname checks,\n // both of which were already validated synchronously by undici when\n // the request started. We re-build a placeholder URL to feed the\n // resolved-address rule, which is the load-bearing check for\n // rebinding defense.\n const url = new URL(`https://${bracketIfV6(hostname)}`);\n const ips = addresses.map(a => a.address);\n const result = enforceSsrfPolicyResolved(url, ips, policy);\n if (!result.allowed) {\n callback(makeSsrfError(result.message ?? 'SSRF policy denied resolved address', result.rule ?? 'ssrf'));\n return;\n }\n\n if (wantsAll) {\n callback(null, addresses);\n return;\n }\n // Pin to the first resolved address. enforceSsrfPolicyResolved is\n // all-or-none: if it allowed the resolution, every entry passed.\n const first = addresses[0]!;\n callback(null, first.address, first.family);\n });\n };\n\n const dispatcher = new Agent({\n ...(options.agentOptions ?? {}),\n connect: {\n ...(options.agentOptions?.connect ?? {}),\n // undici's connect type accepts a lookup with the dns.lookup signature.\n lookup: guardedLookup as unknown as Agent.Options['connect'] extends infer T\n ? T extends { lookup?: infer L }\n ? L\n : never\n : never,\n },\n });\n\n const wrapped = async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]): Promise<Response> => {\n // Synchronous pre-check for the URL's literal scheme + (if it's an IP)\n // its CIDR membership. undici skips `connect.lookup` for IP-literal\n // hostnames, so the resolved-IP path below would never see them.\n // This pre-check enforces the same SSRF policy on URLs like\n // `https://127.0.0.1/cb` or `https://[::1]/cb`.\n const url = resolveRequestUrl(input);\n if (url) {\n const sync = enforceSsrfPolicy(url, policy);\n if (!sync.allowed) {\n throw makeSsrfError(sync.message ?? 'SSRF policy denied URL', sync.rule ?? 'ssrf');\n }\n }\n // Redirects are never followed, and this is not caller-overridable.\n //\n // Both guards above only ever see the URL the caller passed. Following a\n // redirect would send the request to a destination neither one evaluated:\n // the synchronous check has already run, and undici skips `connect.lookup`\n // for IP-literal hosts, so a `Location: https://169.254.169.254/` hop\n // reaches the metadata service with the payload attached. Honouring a\n // caller's `redirect: 'follow'` would reopen exactly that hole, so the\n // mode is forced rather than defaulted.\n //\n // A 3xx therefore surfaces to the caller as an ordinary non-2xx response.\n // For signed webhook delivery that is also the correct outcome on its own\n // terms — the signature covers `@target-uri`, so a request replayed at a\n // redirect target would not verify there anyway. Callers that genuinely\n // need to follow a hop should re-enter this fetch with the new URL, which\n // re-runs the full policy on it.\n const redirect = (init as { redirect?: string } | undefined)?.redirect === 'error' ? 'error' : 'manual';\n return undiciFetch(input as Parameters<typeof undiciFetch>[0], {\n ...(init as Parameters<typeof undiciFetch>[1]),\n redirect,\n dispatcher: dispatcher as unknown as Dispatcher,\n }) as unknown as Response;\n };\n\n return wrapped as typeof fetch;\n}\n\nfunction resolveRequestUrl(input: Parameters<typeof fetch>[0]): URL | null {\n try {\n if (typeof input === 'string') return new URL(input);\n if (input instanceof URL) return input;\n if (\n typeof input === 'object' &&\n input !== null &&\n 'url' in input &&\n typeof (input as { url: unknown }).url === 'string'\n ) {\n return new URL((input as { url: string }).url);\n }\n } catch {\n // Let undici surface the parse error in its own shape.\n return null;\n }\n return null;\n}\n\nfunction bracketIfV6(host: string): string {\n return isIPv6(host) ? `[${host}]` : host;\n}\n\nfunction makeSsrfError(message: string, rule: string): NodeJS.ErrnoException {\n const err = new Error(`pin-and-bind: ${rule}: ${message}`) as NodeJS.ErrnoException;\n err.code = 'EADCP_SSRF_BLOCKED';\n return err;\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"structured-serialize.d.ts","sourceRoot":"","sources":["../../../src/lib/server/structured-serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAoCH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAqC3D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA6B7D"}
1
+ {"version":3,"file":"structured-serialize.d.ts","sourceRoot":"","sources":["../../../src/lib/server/structured-serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAsDH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAqC3D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA6B7D"}
@@ -22,6 +22,14 @@ __export(structured_serialize_exports, {
22
22
  structuredSerialize: () => structuredSerialize
23
23
  });
24
24
  module.exports = __toCommonJS(structured_serialize_exports);
25
+ function defineOwn(target, key, value) {
26
+ Object.defineProperty(target, key, {
27
+ value,
28
+ enumerable: true,
29
+ writable: true,
30
+ configurable: true
31
+ });
32
+ }
25
33
  function isEnvelope(value) {
26
34
  if (typeof value !== "object" || value === null) return false;
27
35
  const o = value;
@@ -61,7 +69,7 @@ function structuredSerialize(value) {
61
69
  if (t === "object") {
62
70
  const out = {};
63
71
  for (const [key, v] of Object.entries(value)) {
64
- out[key] = structuredSerialize(v);
72
+ defineOwn(out, key, structuredSerialize(v));
65
73
  }
66
74
  return out;
67
75
  }
@@ -87,7 +95,7 @@ function structuredDeserialize(value) {
87
95
  if (t === "object") {
88
96
  const out = {};
89
97
  for (const [key, v] of Object.entries(value)) {
90
- out[key] = structuredDeserialize(v);
98
+ defineOwn(out, key, structuredDeserialize(v));
91
99
  }
92
100
  return out;
93
101
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/server/structured-serialize.ts"],"sourcesContent":["/**\n * Lossless serialization of rich JS types through {@link AdcpStateStore}.\n *\n * AdcpStateStore accepts `Record<string, unknown>` and round-trips through\n * JSON, which silently drops `Map`, `Set`, and converts `Date` to string.\n * Handlers that keep those types in memory can wrap reads and writes:\n *\n * ```ts\n * import { structuredSerialize, structuredDeserialize } from '@adcp/sdk/server';\n *\n * await ctx.store.put('sessions', id, structuredSerialize(session));\n * const session = structuredDeserialize(await ctx.store.get('sessions', id));\n * ```\n *\n * Tagged envelopes use the namespaced field `__adcpType` to avoid collision\n * with domain data. Unknown tag values pass through unchanged on deserialize,\n * so a future tag addition won't corrupt caller data that already uses\n * `__adcpType` for something else.\n */\n\ntype Primitive = string | number | boolean | null;\n\ninterface DateEnvelope {\n __adcpType: 'Date';\n value: string;\n}\n\ninterface MapEnvelope {\n __adcpType: 'Map';\n entries: [unknown, unknown][];\n}\n\ninterface SetEnvelope {\n __adcpType: 'Set';\n values: unknown[];\n}\n\ntype Envelope = DateEnvelope | MapEnvelope | SetEnvelope;\n\nfunction isEnvelope(value: unknown): value is Envelope {\n if (typeof value !== 'object' || value === null) return false;\n const o = value as Record<string, unknown>;\n switch (o.__adcpType) {\n case 'Date':\n return typeof o.value === 'string';\n case 'Map':\n return Array.isArray(o.entries);\n case 'Set':\n return Array.isArray(o.values);\n default:\n return false;\n }\n}\n\n/**\n * Walk `value` replacing `Map`, `Set`, and `Date` instances with JSON-safe\n * tagged envelopes. Plain objects and arrays recurse.\n */\nexport function structuredSerialize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return value as Primitive;\n\n if (value instanceof Date) {\n return { __adcpType: 'Date', value: value.toISOString() } satisfies DateEnvelope;\n }\n\n if (value instanceof Map) {\n return {\n __adcpType: 'Map',\n entries: [...value.entries()].map(([k, v]) => [structuredSerialize(k), structuredSerialize(v)]),\n } satisfies MapEnvelope;\n }\n\n if (value instanceof Set) {\n return {\n __adcpType: 'Set',\n values: [...value.values()].map(v => structuredSerialize(v)),\n } satisfies SetEnvelope;\n }\n\n if (Array.isArray(value)) {\n return value.map(v => structuredSerialize(v));\n }\n\n if (t === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n out[key] = structuredSerialize(v);\n }\n return out;\n }\n\n // Functions, symbols, bigints — drop silently (consistent with JSON.stringify).\n return undefined;\n}\n\n/**\n * Inverse of {@link structuredSerialize}. Walks `value` converting tagged\n * envelopes back into native `Map`, `Set`, and `Date` instances.\n */\nexport function structuredDeserialize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return value;\n\n if (Array.isArray(value)) {\n return value.map(v => structuredDeserialize(v));\n }\n\n if (isEnvelope(value)) {\n switch (value.__adcpType) {\n case 'Date':\n return new Date(value.value);\n case 'Map':\n return new Map(value.entries.map(([k, v]) => [structuredDeserialize(k), structuredDeserialize(v)]));\n case 'Set':\n return new Set(value.values.map(v => structuredDeserialize(v)));\n }\n }\n\n if (t === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n out[key] = structuredDeserialize(v);\n }\n return out;\n }\n\n return value;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuCA,SAAS,WAAW,OAAmC;AACrD,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,IAAI;AACV,UAAQ,EAAE,YAAY;AAAA,IACpB,KAAK;AACH,aAAO,OAAO,EAAE,UAAU;AAAA,IAC5B,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,OAAO;AAAA,IAChC,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,MAAM;AAAA,IAC/B;AACE,aAAO;AAAA,EACX;AACF;AAMO,SAAS,oBAAoB,OAAyB;AAC3D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW,QAAO;AAEhE,MAAI,iBAAiB,MAAM;AACzB,WAAO,EAAE,YAAY,QAAQ,OAAO,MAAM,YAAY,EAAE;AAAA,EAC1D;AAEA,MAAI,iBAAiB,KAAK;AACxB,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC;AAAA,IAChG;AAAA,EACF;AAEA,MAAI,iBAAiB,KAAK;AACxB,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,IAAI,OAAK,oBAAoB,CAAC,CAAC;AAAA,IAC7D;AAAA,EACF;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,OAAK,oBAAoB,CAAC,CAAC;AAAA,EAC9C;AAEA,MAAI,MAAM,UAAU;AAClB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACvE,UAAI,GAAG,IAAI,oBAAoB,CAAC;AAAA,IAClC;AACA,WAAO;AAAA,EACT;AAGA,SAAO;AACT;AAMO,SAAS,sBAAsB,OAAyB;AAC7D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW,QAAO;AAEhE,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,OAAK,sBAAsB,CAAC,CAAC;AAAA,EAChD;AAEA,MAAI,WAAW,KAAK,GAAG;AACrB,YAAQ,MAAM,YAAY;AAAA,MACxB,KAAK;AACH,eAAO,IAAI,KAAK,MAAM,KAAK;AAAA,MAC7B,KAAK;AACH,eAAO,IAAI,IAAI,MAAM,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,sBAAsB,CAAC,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAAC;AAAA,MACpG,KAAK;AACH,eAAO,IAAI,IAAI,MAAM,OAAO,IAAI,OAAK,sBAAsB,CAAC,CAAC,CAAC;AAAA,IAClE;AAAA,EACF;AAEA,MAAI,MAAM,UAAU;AAClB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACvE,UAAI,GAAG,IAAI,sBAAsB,CAAC;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/lib/server/structured-serialize.ts"],"sourcesContent":["/**\n * Lossless serialization of rich JS types through {@link AdcpStateStore}.\n *\n * AdcpStateStore accepts `Record<string, unknown>` and round-trips through\n * JSON, which silently drops `Map`, `Set`, and converts `Date` to string.\n * Handlers that keep those types in memory can wrap reads and writes:\n *\n * ```ts\n * import { structuredSerialize, structuredDeserialize } from '@adcp/sdk/server';\n *\n * await ctx.store.put('sessions', id, structuredSerialize(session));\n * const session = structuredDeserialize(await ctx.store.get('sessions', id));\n * ```\n *\n * Tagged envelopes use the namespaced field `__adcpType` to avoid collision\n * with domain data. Unknown tag values pass through unchanged on deserialize,\n * so a future tag addition won't corrupt caller data that already uses\n * `__adcpType` for something else.\n */\n\ntype Primitive = string | number | boolean | null;\n\ninterface DateEnvelope {\n __adcpType: 'Date';\n value: string;\n}\n\ninterface MapEnvelope {\n __adcpType: 'Map';\n entries: [unknown, unknown][];\n}\n\ninterface SetEnvelope {\n __adcpType: 'Set';\n values: unknown[];\n}\n\ntype Envelope = DateEnvelope | MapEnvelope | SetEnvelope;\n\n/**\n * Copy one key onto a fresh result object as an own data property.\n *\n * `JSON.parse` produces a genuine own `__proto__` key, and `Object.entries`\n * enumerates it. Plain `out[key] = value` assignment would reach\n * `Object.prototype`'s `__proto__` setter and replace the result's prototype\n * with caller data instead of creating the key. `defineProperty` always creates\n * an own property, so `__proto__` round-trips like any other field.\n */\nfunction defineOwn(target: Record<string, unknown>, key: string, value: unknown): void {\n Object.defineProperty(target, key, {\n value,\n enumerable: true,\n writable: true,\n configurable: true,\n });\n}\n\nfunction isEnvelope(value: unknown): value is Envelope {\n if (typeof value !== 'object' || value === null) return false;\n const o = value as Record<string, unknown>;\n switch (o.__adcpType) {\n case 'Date':\n return typeof o.value === 'string';\n case 'Map':\n return Array.isArray(o.entries);\n case 'Set':\n return Array.isArray(o.values);\n default:\n return false;\n }\n}\n\n/**\n * Walk `value` replacing `Map`, `Set`, and `Date` instances with JSON-safe\n * tagged envelopes. Plain objects and arrays recurse.\n */\nexport function structuredSerialize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return value as Primitive;\n\n if (value instanceof Date) {\n return { __adcpType: 'Date', value: value.toISOString() } satisfies DateEnvelope;\n }\n\n if (value instanceof Map) {\n return {\n __adcpType: 'Map',\n entries: [...value.entries()].map(([k, v]) => [structuredSerialize(k), structuredSerialize(v)]),\n } satisfies MapEnvelope;\n }\n\n if (value instanceof Set) {\n return {\n __adcpType: 'Set',\n values: [...value.values()].map(v => structuredSerialize(v)),\n } satisfies SetEnvelope;\n }\n\n if (Array.isArray(value)) {\n return value.map(v => structuredSerialize(v));\n }\n\n if (t === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n defineOwn(out, key, structuredSerialize(v));\n }\n return out;\n }\n\n // Functions, symbols, bigints — drop silently (consistent with JSON.stringify).\n return undefined;\n}\n\n/**\n * Inverse of {@link structuredSerialize}. Walks `value` converting tagged\n * envelopes back into native `Map`, `Set`, and `Date` instances.\n */\nexport function structuredDeserialize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return value;\n\n if (Array.isArray(value)) {\n return value.map(v => structuredDeserialize(v));\n }\n\n if (isEnvelope(value)) {\n switch (value.__adcpType) {\n case 'Date':\n return new Date(value.value);\n case 'Map':\n return new Map(value.entries.map(([k, v]) => [structuredDeserialize(k), structuredDeserialize(v)]));\n case 'Set':\n return new Set(value.values.map(v => structuredDeserialize(v)));\n }\n }\n\n if (t === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n defineOwn(out, key, structuredDeserialize(v));\n }\n return out;\n }\n\n return value;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgDA,SAAS,UAAU,QAAiC,KAAa,OAAsB;AACrF,SAAO,eAAe,QAAQ,KAAK;AAAA,IACjC;AAAA,IACA,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,cAAc;AAAA,EAChB,CAAC;AACH;AAEA,SAAS,WAAW,OAAmC;AACrD,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,IAAI;AACV,UAAQ,EAAE,YAAY;AAAA,IACpB,KAAK;AACH,aAAO,OAAO,EAAE,UAAU;AAAA,IAC5B,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,OAAO;AAAA,IAChC,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,MAAM;AAAA,IAC/B;AACE,aAAO;AAAA,EACX;AACF;AAMO,SAAS,oBAAoB,OAAyB;AAC3D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW,QAAO;AAEhE,MAAI,iBAAiB,MAAM;AACzB,WAAO,EAAE,YAAY,QAAQ,OAAO,MAAM,YAAY,EAAE;AAAA,EAC1D;AAEA,MAAI,iBAAiB,KAAK;AACxB,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC;AAAA,IAChG;AAAA,EACF;AAEA,MAAI,iBAAiB,KAAK;AACxB,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,IAAI,OAAK,oBAAoB,CAAC,CAAC;AAAA,IAC7D;AAAA,EACF;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,OAAK,oBAAoB,CAAC,CAAC;AAAA,EAC9C;AAEA,MAAI,MAAM,UAAU;AAClB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACvE,gBAAU,KAAK,KAAK,oBAAoB,CAAC,CAAC;AAAA,IAC5C;AACA,WAAO;AAAA,EACT;AAGA,SAAO;AACT;AAMO,SAAS,sBAAsB,OAAyB;AAC7D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW,QAAO;AAEhE,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,OAAK,sBAAsB,CAAC,CAAC;AAAA,EAChD;AAEA,MAAI,WAAW,KAAK,GAAG;AACrB,YAAQ,MAAM,YAAY;AAAA,MACxB,KAAK;AACH,eAAO,IAAI,KAAK,MAAM,KAAK;AAAA,MAC7B,KAAK;AACH,eAAO,IAAI,IAAI,MAAM,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,sBAAsB,CAAC,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAAC;AAAA,MACpG,KAAK;AACH,eAAO,IAAI,IAAI,MAAM,OAAO,IAAI,OAAK,sBAAsB,CAAC,CAAC,CAAC;AAAA,IAClE;AAAA,EACF;AAEA,MAAI,MAAM,UAAU;AAClB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACvE,gBAAU,KAAK,KAAK,sBAAsB,CAAC,CAAC;AAAA,IAC9C;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":[]}
@@ -1,3 +1,11 @@
1
+ function defineOwn(target, key, value) {
2
+ Object.defineProperty(target, key, {
3
+ value,
4
+ enumerable: true,
5
+ writable: true,
6
+ configurable: true
7
+ });
8
+ }
1
9
  function isEnvelope(value) {
2
10
  if (typeof value !== "object" || value === null) return false;
3
11
  const o = value;
@@ -37,7 +45,7 @@ function structuredSerialize(value) {
37
45
  if (t === "object") {
38
46
  const out = {};
39
47
  for (const [key, v] of Object.entries(value)) {
40
- out[key] = structuredSerialize(v);
48
+ defineOwn(out, key, structuredSerialize(v));
41
49
  }
42
50
  return out;
43
51
  }
@@ -63,7 +71,7 @@ function structuredDeserialize(value) {
63
71
  if (t === "object") {
64
72
  const out = {};
65
73
  for (const [key, v] of Object.entries(value)) {
66
- out[key] = structuredDeserialize(v);
74
+ defineOwn(out, key, structuredDeserialize(v));
67
75
  }
68
76
  return out;
69
77
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/server/structured-serialize.ts"],"sourcesContent":["/**\n * Lossless serialization of rich JS types through {@link AdcpStateStore}.\n *\n * AdcpStateStore accepts `Record<string, unknown>` and round-trips through\n * JSON, which silently drops `Map`, `Set`, and converts `Date` to string.\n * Handlers that keep those types in memory can wrap reads and writes:\n *\n * ```ts\n * import { structuredSerialize, structuredDeserialize } from '@adcp/sdk/server';\n *\n * await ctx.store.put('sessions', id, structuredSerialize(session));\n * const session = structuredDeserialize(await ctx.store.get('sessions', id));\n * ```\n *\n * Tagged envelopes use the namespaced field `__adcpType` to avoid collision\n * with domain data. Unknown tag values pass through unchanged on deserialize,\n * so a future tag addition won't corrupt caller data that already uses\n * `__adcpType` for something else.\n */\n\ntype Primitive = string | number | boolean | null;\n\ninterface DateEnvelope {\n __adcpType: 'Date';\n value: string;\n}\n\ninterface MapEnvelope {\n __adcpType: 'Map';\n entries: [unknown, unknown][];\n}\n\ninterface SetEnvelope {\n __adcpType: 'Set';\n values: unknown[];\n}\n\ntype Envelope = DateEnvelope | MapEnvelope | SetEnvelope;\n\nfunction isEnvelope(value: unknown): value is Envelope {\n if (typeof value !== 'object' || value === null) return false;\n const o = value as Record<string, unknown>;\n switch (o.__adcpType) {\n case 'Date':\n return typeof o.value === 'string';\n case 'Map':\n return Array.isArray(o.entries);\n case 'Set':\n return Array.isArray(o.values);\n default:\n return false;\n }\n}\n\n/**\n * Walk `value` replacing `Map`, `Set`, and `Date` instances with JSON-safe\n * tagged envelopes. Plain objects and arrays recurse.\n */\nexport function structuredSerialize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return value as Primitive;\n\n if (value instanceof Date) {\n return { __adcpType: 'Date', value: value.toISOString() } satisfies DateEnvelope;\n }\n\n if (value instanceof Map) {\n return {\n __adcpType: 'Map',\n entries: [...value.entries()].map(([k, v]) => [structuredSerialize(k), structuredSerialize(v)]),\n } satisfies MapEnvelope;\n }\n\n if (value instanceof Set) {\n return {\n __adcpType: 'Set',\n values: [...value.values()].map(v => structuredSerialize(v)),\n } satisfies SetEnvelope;\n }\n\n if (Array.isArray(value)) {\n return value.map(v => structuredSerialize(v));\n }\n\n if (t === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n out[key] = structuredSerialize(v);\n }\n return out;\n }\n\n // Functions, symbols, bigints — drop silently (consistent with JSON.stringify).\n return undefined;\n}\n\n/**\n * Inverse of {@link structuredSerialize}. Walks `value` converting tagged\n * envelopes back into native `Map`, `Set`, and `Date` instances.\n */\nexport function structuredDeserialize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return value;\n\n if (Array.isArray(value)) {\n return value.map(v => structuredDeserialize(v));\n }\n\n if (isEnvelope(value)) {\n switch (value.__adcpType) {\n case 'Date':\n return new Date(value.value);\n case 'Map':\n return new Map(value.entries.map(([k, v]) => [structuredDeserialize(k), structuredDeserialize(v)]));\n case 'Set':\n return new Set(value.values.map(v => structuredDeserialize(v)));\n }\n }\n\n if (t === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n out[key] = structuredDeserialize(v);\n }\n return out;\n }\n\n return value;\n}\n"],"mappings":"AAuCA,SAAS,WAAW,OAAmC;AACrD,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,IAAI;AACV,UAAQ,EAAE,YAAY;AAAA,IACpB,KAAK;AACH,aAAO,OAAO,EAAE,UAAU;AAAA,IAC5B,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,OAAO;AAAA,IAChC,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,MAAM;AAAA,IAC/B;AACE,aAAO;AAAA,EACX;AACF;AAMO,SAAS,oBAAoB,OAAyB;AAC3D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW,QAAO;AAEhE,MAAI,iBAAiB,MAAM;AACzB,WAAO,EAAE,YAAY,QAAQ,OAAO,MAAM,YAAY,EAAE;AAAA,EAC1D;AAEA,MAAI,iBAAiB,KAAK;AACxB,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC;AAAA,IAChG;AAAA,EACF;AAEA,MAAI,iBAAiB,KAAK;AACxB,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,IAAI,OAAK,oBAAoB,CAAC,CAAC;AAAA,IAC7D;AAAA,EACF;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,OAAK,oBAAoB,CAAC,CAAC;AAAA,EAC9C;AAEA,MAAI,MAAM,UAAU;AAClB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACvE,UAAI,GAAG,IAAI,oBAAoB,CAAC;AAAA,IAClC;AACA,WAAO;AAAA,EACT;AAGA,SAAO;AACT;AAMO,SAAS,sBAAsB,OAAyB;AAC7D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW,QAAO;AAEhE,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,OAAK,sBAAsB,CAAC,CAAC;AAAA,EAChD;AAEA,MAAI,WAAW,KAAK,GAAG;AACrB,YAAQ,MAAM,YAAY;AAAA,MACxB,KAAK;AACH,eAAO,IAAI,KAAK,MAAM,KAAK;AAAA,MAC7B,KAAK;AACH,eAAO,IAAI,IAAI,MAAM,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,sBAAsB,CAAC,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAAC;AAAA,MACpG,KAAK;AACH,eAAO,IAAI,IAAI,MAAM,OAAO,IAAI,OAAK,sBAAsB,CAAC,CAAC,CAAC;AAAA,IAClE;AAAA,EACF;AAEA,MAAI,MAAM,UAAU;AAClB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACvE,UAAI,GAAG,IAAI,sBAAsB,CAAC;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/lib/server/structured-serialize.ts"],"sourcesContent":["/**\n * Lossless serialization of rich JS types through {@link AdcpStateStore}.\n *\n * AdcpStateStore accepts `Record<string, unknown>` and round-trips through\n * JSON, which silently drops `Map`, `Set`, and converts `Date` to string.\n * Handlers that keep those types in memory can wrap reads and writes:\n *\n * ```ts\n * import { structuredSerialize, structuredDeserialize } from '@adcp/sdk/server';\n *\n * await ctx.store.put('sessions', id, structuredSerialize(session));\n * const session = structuredDeserialize(await ctx.store.get('sessions', id));\n * ```\n *\n * Tagged envelopes use the namespaced field `__adcpType` to avoid collision\n * with domain data. Unknown tag values pass through unchanged on deserialize,\n * so a future tag addition won't corrupt caller data that already uses\n * `__adcpType` for something else.\n */\n\ntype Primitive = string | number | boolean | null;\n\ninterface DateEnvelope {\n __adcpType: 'Date';\n value: string;\n}\n\ninterface MapEnvelope {\n __adcpType: 'Map';\n entries: [unknown, unknown][];\n}\n\ninterface SetEnvelope {\n __adcpType: 'Set';\n values: unknown[];\n}\n\ntype Envelope = DateEnvelope | MapEnvelope | SetEnvelope;\n\n/**\n * Copy one key onto a fresh result object as an own data property.\n *\n * `JSON.parse` produces a genuine own `__proto__` key, and `Object.entries`\n * enumerates it. Plain `out[key] = value` assignment would reach\n * `Object.prototype`'s `__proto__` setter and replace the result's prototype\n * with caller data instead of creating the key. `defineProperty` always creates\n * an own property, so `__proto__` round-trips like any other field.\n */\nfunction defineOwn(target: Record<string, unknown>, key: string, value: unknown): void {\n Object.defineProperty(target, key, {\n value,\n enumerable: true,\n writable: true,\n configurable: true,\n });\n}\n\nfunction isEnvelope(value: unknown): value is Envelope {\n if (typeof value !== 'object' || value === null) return false;\n const o = value as Record<string, unknown>;\n switch (o.__adcpType) {\n case 'Date':\n return typeof o.value === 'string';\n case 'Map':\n return Array.isArray(o.entries);\n case 'Set':\n return Array.isArray(o.values);\n default:\n return false;\n }\n}\n\n/**\n * Walk `value` replacing `Map`, `Set`, and `Date` instances with JSON-safe\n * tagged envelopes. Plain objects and arrays recurse.\n */\nexport function structuredSerialize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return value as Primitive;\n\n if (value instanceof Date) {\n return { __adcpType: 'Date', value: value.toISOString() } satisfies DateEnvelope;\n }\n\n if (value instanceof Map) {\n return {\n __adcpType: 'Map',\n entries: [...value.entries()].map(([k, v]) => [structuredSerialize(k), structuredSerialize(v)]),\n } satisfies MapEnvelope;\n }\n\n if (value instanceof Set) {\n return {\n __adcpType: 'Set',\n values: [...value.values()].map(v => structuredSerialize(v)),\n } satisfies SetEnvelope;\n }\n\n if (Array.isArray(value)) {\n return value.map(v => structuredSerialize(v));\n }\n\n if (t === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n defineOwn(out, key, structuredSerialize(v));\n }\n return out;\n }\n\n // Functions, symbols, bigints — drop silently (consistent with JSON.stringify).\n return undefined;\n}\n\n/**\n * Inverse of {@link structuredSerialize}. Walks `value` converting tagged\n * envelopes back into native `Map`, `Set`, and `Date` instances.\n */\nexport function structuredDeserialize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return value;\n\n if (Array.isArray(value)) {\n return value.map(v => structuredDeserialize(v));\n }\n\n if (isEnvelope(value)) {\n switch (value.__adcpType) {\n case 'Date':\n return new Date(value.value);\n case 'Map':\n return new Map(value.entries.map(([k, v]) => [structuredDeserialize(k), structuredDeserialize(v)]));\n case 'Set':\n return new Set(value.values.map(v => structuredDeserialize(v)));\n }\n }\n\n if (t === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n defineOwn(out, key, structuredDeserialize(v));\n }\n return out;\n }\n\n return value;\n}\n"],"mappings":"AAgDA,SAAS,UAAU,QAAiC,KAAa,OAAsB;AACrF,SAAO,eAAe,QAAQ,KAAK;AAAA,IACjC;AAAA,IACA,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,cAAc;AAAA,EAChB,CAAC;AACH;AAEA,SAAS,WAAW,OAAmC;AACrD,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,IAAI;AACV,UAAQ,EAAE,YAAY;AAAA,IACpB,KAAK;AACH,aAAO,OAAO,EAAE,UAAU;AAAA,IAC5B,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,OAAO;AAAA,IAChC,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,MAAM;AAAA,IAC/B;AACE,aAAO;AAAA,EACX;AACF;AAMO,SAAS,oBAAoB,OAAyB;AAC3D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW,QAAO;AAEhE,MAAI,iBAAiB,MAAM;AACzB,WAAO,EAAE,YAAY,QAAQ,OAAO,MAAM,YAAY,EAAE;AAAA,EAC1D;AAEA,MAAI,iBAAiB,KAAK;AACxB,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC;AAAA,IAChG;AAAA,EACF;AAEA,MAAI,iBAAiB,KAAK;AACxB,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,IAAI,OAAK,oBAAoB,CAAC,CAAC;AAAA,IAC7D;AAAA,EACF;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,OAAK,oBAAoB,CAAC,CAAC;AAAA,EAC9C;AAEA,MAAI,MAAM,UAAU;AAClB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACvE,gBAAU,KAAK,KAAK,oBAAoB,CAAC,CAAC;AAAA,IAC5C;AACA,WAAO;AAAA,EACT;AAGA,SAAO;AACT;AAMO,SAAS,sBAAsB,OAAyB;AAC7D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW,QAAO;AAEhE,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,OAAK,sBAAsB,CAAC,CAAC;AAAA,EAChD;AAEA,MAAI,WAAW,KAAK,GAAG;AACrB,YAAQ,MAAM,YAAY;AAAA,MACxB,KAAK;AACH,eAAO,IAAI,KAAK,MAAM,KAAK;AAAA,MAC7B,KAAK;AACH,eAAO,IAAI,IAAI,MAAM,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,sBAAsB,CAAC,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAAC;AAAA,MACpG,KAAK;AACH,eAAO,IAAI,IAAI,MAAM,OAAO,IAAI,OAAK,sBAAsB,CAAC,CAAC,CAAC;AAAA,IAClE;AAAA,EACF;AAEA,MAAI,MAAM,UAAU;AAClB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACvE,gBAAU,KAAK,KAAK,sBAAsB,CAAC,CAAC;AAAA,IAC9C;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-emitter.d.ts","sourceRoot":"","sources":["../../../src/lib/server/webhook-emitter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webhook-emitter.d.ts","sourceRoot":"","sources":["../../../src/lib/server/webhook-emitter.ts"],"names":[],"mappings":"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"}
@@ -105,7 +105,7 @@ function createWebhookEmitter(options) {
105
105
  };
106
106
  }
107
107
  terminal = isTerminalStatus(status, response.wwwAuthenticate);
108
- error = `HTTP ${status}${response.wwwAuthenticate ? ` (${response.wwwAuthenticate})` : ""}`;
108
+ error = status >= 300 && status < 400 ? `HTTP ${status} redirect${response.location ? ` to ${response.location}` : ""} \u2014 redirects are never followed for signed webhook delivery, because the signature covers @target-uri and would not verify at the redirect target. Re-register the webhook with the final URL.` : `HTTP ${status}${response.wwwAuthenticate ? ` (${response.wwwAuthenticate})` : ""}`;
109
109
  } catch (err) {
110
110
  error = formatTransportError(err);
111
111
  if (errorContainsCode(err, "EADCP_SSRF_BLOCKED")) {
@@ -144,7 +144,8 @@ async function deliverOnce(args) {
144
144
  });
145
145
  return {
146
146
  status: response.status,
147
- ...response.headers.get("www-authenticate") && { wwwAuthenticate: response.headers.get("www-authenticate") }
147
+ ...response.headers.get("www-authenticate") && { wwwAuthenticate: response.headers.get("www-authenticate") },
148
+ ...response.headers.get("location") && { location: response.headers.get("location") }
148
149
  };
149
150
  }
150
151
  async function buildHeaders(args) {
@@ -202,6 +203,7 @@ function defaultGenerateIdempotencyKey() {
202
203
  function isTerminalStatus(status, wwwAuthenticate) {
203
204
  if (status === 429) return false;
204
205
  if (status >= 500) return false;
206
+ if (status >= 300 && status < 400) return true;
205
207
  if (status === 401 && wwwAuthenticate && TERMINAL_SIGNATURE_WWW_AUTH_RE.test(wwwAuthenticate)) return true;
206
208
  if (status >= 400 && status < 500) return true;
207
209
  return false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/server/webhook-emitter.ts"],"sourcesContent":["/**\n * Publisher-side webhook emitter — the symmetric counterpart to PR #629's\n * receiver-side dedup. A seller / governance agent / rights agent building\n * with `@adcp/sdk` gets a one-call API that handles:\n *\n * - RFC 9421 webhook signing on every attempt (adcp#2423).\n * - A stable `idempotency_key` per logical event, reused across retries\n * (adcp#2417) — regenerating on retry is the #1 at-least-once-delivery\n * bug the runner-side conformance suite catches.\n * - Compact-separator JSON serialization once, signed once, posted once\n * (adcp#2478) — prevents the serialization-mismatch trap where a\n * signer's byte view differs from what the HTTP client writes.\n * - Retry/backoff on 5xx and 429. Terminal on 4xx and on 401 responses\n * carrying `WWW-Authenticate: Signature error=\"webhook_signature_*\"`\n * (spec says retrying a signature failure just fails identically).\n * - HMAC-SHA256 fallback for legacy buyers that registered\n * `push_notification_config.authentication.credentials` — still pinned\n * to compact separators per adcp#2478.\n *\n * Handler authors using `createAdcpServer` call `ctx.emitWebhook(...)` with\n * a `url`, `payload`, and `operation_id` — everything else is wired in.\n */\n\nimport { signWebhook, type SignerKey } from '../signing/signer';\nimport { signWebhookAsync } from '../signing/signer-async';\nimport type { SigningProvider } from '../signing/provider';\nimport type { RequestLike } from '../signing/canonicalize';\nimport { createPinAndBindFetch } from './pin-and-bind-fetch';\nimport { createHmac, randomUUID } from 'node:crypto';\n\n/**\n * Minimum pattern per adcp#2417 / core/mcp-webhook-payload.json.\n * Publisher-side check — catches `generateIdempotencyKey` overrides that\n * produce keys too short for a conformant receiver to accept.\n */\nconst IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_.:-]{16,255}$/;\n\n/** WWW-Authenticate header pattern signaling a signature-layer reject. */\nconst TERMINAL_SIGNATURE_WWW_AUTH_RE = /Signature\\s+error=\"webhook_signature_/i;\n\n/**\n * Per-(operation_id) idempotency-key store. The key MUST be stable across\n * every retry of the same logical event — that's the load-bearing invariant\n * the receiver-side dedup depends on.\n *\n * Defaults to an in-memory Map. Production publishers with multi-replica\n * emitters SHOULD inject a durable backend (the same way\n * `AsyncHandlerConfig.webhookDedup` accepts a pluggable store on the\n * receiver side).\n */\nexport interface WebhookIdempotencyKeyStore {\n get(operation_id: string): Promise<string | undefined> | string | undefined;\n /** Called only on first-seen operation_id. Subsequent emits read via get(). */\n set(operation_id: string, key: string): Promise<void> | void;\n}\n\nexport function memoryWebhookKeyStore(): WebhookIdempotencyKeyStore {\n const m = new Map<string, string>();\n return {\n get: id => m.get(id),\n set: (id, key) => {\n m.set(id, key);\n },\n };\n}\n\n/**\n * Authentication mode for a single delivery. Omit / pass `null` to use the\n * 9421 baseline. `bearer` / `hmac_sha256` drop back to legacy flows for\n * buyers who populated `push_notification_config.authentication.credentials`.\n *\n * @deprecated The `hmac_sha256` variant is deprecated. HMAC remains in the\n * AdCP spec as a legacy fallback for buyers that registered\n * `push_notification_config.authentication.credentials`, so the SDK keeps\n * supporting it — but the spec-current path is RFC 9421 webhook signatures.\n * See docs/migration-4.x-to-5.x.md#webhook-hmac-legacy-deprecation.\n */\nexport type WebhookAuthentication = { type: 'bearer'; token: string } | { type: 'hmac_sha256'; secret: string } | null;\n\nlet hmacWarningFired = false;\n\nfunction maybeWarnHmacDeprecation(suppressLegacyWarnings?: boolean): void {\n if (hmacWarningFired) return;\n if (suppressLegacyWarnings) return;\n if (process.env.ADCP_SUPPRESS_HMAC_WARNING === '1') return;\n hmacWarningFired = true;\n console.warn(\n '[adcp] Warning: webhook HMAC-SHA256 authentication is deprecated. ' +\n 'HMAC remains supported in the AdCP spec as a legacy fallback but RFC ' +\n '9421 is the spec-current path; migrate when your counterparties are ' +\n 'ready. See docs/migration-4.x-to-5.x.md#webhook-hmac-legacy-deprecation. ' +\n 'Suppress with ADCP_SUPPRESS_HMAC_WARNING=1 (env) or ' +\n 'createWebhookEmitter({ suppressLegacyWarnings: true }) (programmatic).'\n );\n}\n\nexport interface WebhookRetryOptions {\n /** Max delivery attempts (≥1). Default 5. */\n maxAttempts?: number;\n /** Initial backoff in ms. Default 1000. */\n initialDelayMs?: number;\n /** Cap per-attempt backoff. Default 60000. */\n maxDelayMs?: number;\n /** Jitter factor ∈ [0,1]: 0 = none, 0.5 = ±50%. Default 0.25. */\n jitter?: number;\n}\n\nexport interface WebhookEmitterOptions {\n /**\n * In-process JWK signing key. Its JWKS entry SHOULD carry\n * `adcp_use: \"request-signing\"` — webhooks are signed with the agent's\n * request-signing key (the deprecated `\"webhook-signing\"` value is still\n * accepted by verifiers for backward compatibility). Mutually exclusive\n * with `signerProvider` — exactly one must be provided.\n */\n signerKey?: SignerKey;\n /**\n * Async KMS-backed signing provider (GCP KMS, AWS KMS, Azure Key Vault, etc.).\n * Routes webhook signing through `signWebhookAsync` so the private key never\n * enters process memory. Mutually exclusive with `signerKey` — exactly one\n * must be provided.\n *\n * **Key purpose.** Webhooks are signed with a `request-signing` key; domain\n * separation between requests and webhooks is carried by the signature\n * `tag`, not the `adcp_use` discriminator. The same `SigningProvider` used\n * for `request_signing.provider` MAY be reused here. To isolate webhook key\n * material (so a webhook-key compromise does not extend to request signing,\n * or to rotate independently), wrap a second `request-signing`\n * `cryptoKeyVersion` published under a distinct `kid` — isolation comes from\n * the `kid`, not a distinct `adcp_use`. (The deprecated `\"webhook-signing\"`\n * purpose is still accepted by verifiers for backward compatibility.)\n */\n signerProvider?: SigningProvider;\n retries?: WebhookRetryOptions;\n idempotencyKeyStore?: WebhookIdempotencyKeyStore;\n /**\n * Override the default idempotency-key generator. Must return a value\n * matching `/^[A-Za-z0-9_.:-]{16,255}$/` — the emitter rejects anything\n * else (a malformed key would fail the receiver's schema check, which\n * would report as a conformance violation of the publisher).\n */\n generateIdempotencyKey?: () => string;\n /**\n * Override the HTTP client. Defaults to `createPinAndBindFetch()`, which\n * defeats DNS-rebinding / SSRF attacks against the buyer-supplied\n * `push_notification_config.url` (https-only; loopback, private, and cloud\n * metadata ranges denied). See `docs/guides/SIGNING-GUIDE.md` § Webhook\n * SSRF defense.\n *\n * In-process storyboard / test harnesses that deliver to a loopback\n * `http://127.0.0.1:port` receiver must opt into the relaxed policy\n * explicitly: `fetch: createPinAndBindFetch({ policy:\n * LOOPBACK_OK_WEBHOOK_SSRF_POLICY })`. Passing `globalThis.fetch` here\n * restores the unguarded pre-9.8 behavior — only do so behind your own\n * URL validation.\n */\n fetch?: typeof fetch;\n /** Default `User-Agent` header. */\n userAgent?: string;\n /** Signing tag override. Defaults to `adcp/webhook-signing/v1`. */\n tag?: string;\n /** Observability hook called BEFORE each attempt. */\n onAttempt?: (info: WebhookEmitAttempt) => void;\n /** Observability hook called AFTER each attempt completes. */\n onAttemptResult?: (info: WebhookEmitAttemptResult) => void;\n /**\n * Sleeper override. Production uses `setTimeout`; tests inject a stub to\n * skip real backoff. Takes (ms, abortSignal) and resolves when slept.\n */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Suppress the one-time `console.warn` emitted on first HMAC-SHA256\n * webhook delivery. Programmatic equivalent of\n * `ADCP_SUPPRESS_HMAC_WARNING=1`, for libraries embedded in agents where\n * setting an env var is awkward. Does not affect the `@deprecated` JSDoc\n * flag on `WebhookAuthentication` — the type-level deprecation signal\n * always shows up in IDEs.\n */\n suppressLegacyWarnings?: boolean;\n}\n\nexport interface WebhookEmitParams {\n /** Full destination URL. Typically from `push_notification_config.url`. */\n url: string;\n /** Object body. Serialized with compact separators (adcp#2478). */\n payload: Record<string, unknown>;\n /**\n * Stable logical event id. Two emits with the same operation_id reuse\n * the same `idempotency_key` — this is the cross-attempt AND\n * cross-process invariant the receiver dedups on.\n */\n operation_id: string;\n /**\n * Per-emit override of the delivery's authentication mode. Omit for the\n * 9421 default.\n */\n authentication?: WebhookAuthentication;\n /** Per-emit retries override. */\n retries?: WebhookRetryOptions;\n}\n\nexport interface WebhookEmitAttempt {\n operation_id: string;\n idempotency_key: string;\n attempt: number;\n url: string;\n}\n\nexport interface WebhookEmitAttemptResult extends WebhookEmitAttempt {\n status?: number;\n durationMs: number;\n error?: string;\n willRetry: boolean;\n}\n\nexport interface WebhookEmitResult {\n operation_id: string;\n idempotency_key: string;\n attempts: number;\n delivered: boolean;\n final_status?: number;\n /**\n * Per-attempt error messages (transport / signer / network failures).\n *\n * **Logging caution:** when a `signerProvider` rejection bubbles into\n * this array, the message text comes from the adapter and may include\n * infra-flavored detail (KMS resource names, IAM principals, project\n * IDs). Mirrors the same caution flagged on `SigningProvider.fingerprint`.\n * If you pipe `errors[]` into shared logs / observability pipelines,\n * sanitize or redact at your boundary — adapter messages aren't\n * guaranteed to be operator-safe.\n */\n errors: string[];\n}\n\nexport interface WebhookEmitter {\n emit(params: WebhookEmitParams): Promise<WebhookEmitResult>;\n}\n\n// ────────────────────────────────────────────────────────────\n// Factory\n// ────────────────────────────────────────────────────────────\n\nexport function createWebhookEmitter(options: WebhookEmitterOptions): WebhookEmitter {\n if (options.signerKey && options.signerProvider) {\n throw new TypeError('createWebhookEmitter: provide exactly one of signerKey or signerProvider, not both');\n }\n if (!options.signerKey && !options.signerProvider) {\n throw new TypeError('createWebhookEmitter: one of signerKey or signerProvider is required');\n }\n const store = options.idempotencyKeyStore ?? memoryWebhookKeyStore();\n const generateKey = options.generateIdempotencyKey ?? defaultGenerateIdempotencyKey;\n const fetchImpl = options.fetch ?? createPinAndBindFetch();\n const sleep = options.sleep ?? defaultSleep;\n\n return {\n async emit(params: WebhookEmitParams): Promise<WebhookEmitResult> {\n const retries = resolveRetries(params.retries ?? options.retries);\n const idempotency_key = await resolveIdempotencyKey(store, params.operation_id, generateKey);\n\n // Serialize ONCE with compact separators — the same bytes feed both\n // the content-digest input and the HTTP body on every attempt. This\n // is the load-bearing rule from adcp#2478.\n const bodyPayload = { ...params.payload, idempotency_key };\n const bodyBytes = JSON.stringify(bodyPayload);\n\n const errors: string[] = [];\n let lastStatus: number | undefined;\n\n for (let attempt = 1; attempt <= retries.maxAttempts; attempt++) {\n const attemptInfo: WebhookEmitAttempt = {\n operation_id: params.operation_id,\n idempotency_key,\n attempt,\n url: params.url,\n };\n options.onAttempt?.(attemptInfo);\n\n const started = Date.now();\n let status: number | undefined;\n let error: string | undefined;\n let terminal = false;\n\n try {\n const response = await deliverOnce({\n url: params.url,\n bodyBytes,\n signerKey: options.signerKey,\n signerProvider: options.signerProvider,\n authentication: params.authentication ?? null,\n tag: options.tag,\n userAgent: options.userAgent,\n fetch: fetchImpl,\n suppressLegacyWarnings: options.suppressLegacyWarnings,\n });\n status = response.status;\n lastStatus = status;\n\n if (status >= 200 && status < 300) {\n const durationMs = Date.now() - started;\n options.onAttemptResult?.({ ...attemptInfo, status, durationMs, willRetry: false });\n return {\n operation_id: params.operation_id,\n idempotency_key,\n attempts: attempt,\n delivered: true,\n final_status: status,\n errors,\n };\n }\n\n terminal = isTerminalStatus(status, response.wwwAuthenticate);\n error = `HTTP ${status}${response.wwwAuthenticate ? ` (${response.wwwAuthenticate})` : ''}`;\n } catch (err) {\n error = formatTransportError(err);\n // Network / transport errors are retryable — the delivery didn't\n // reach the receiver, so no risk of double-processing.\n // Pin-and-bind SSRF blocks are themselves terminal: the URL\n // (or its DNS resolution) violates policy and won't change on retry.\n if (errorContainsCode(err, 'EADCP_SSRF_BLOCKED')) {\n terminal = true;\n }\n }\n\n if (error) errors.push(`attempt ${attempt}: ${error}`);\n\n const willRetry = !terminal && attempt < retries.maxAttempts;\n options.onAttemptResult?.({\n ...attemptInfo,\n ...(status !== undefined && { status }),\n durationMs: Date.now() - started,\n ...(error !== undefined && { error }),\n willRetry,\n });\n\n if (!willRetry) break;\n\n await sleep(backoffDelay(attempt, retries));\n }\n\n return {\n operation_id: params.operation_id,\n idempotency_key,\n attempts: errors.length,\n delivered: false,\n ...(lastStatus !== undefined && { final_status: lastStatus }),\n errors,\n };\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Delivery primitives\n// ────────────────────────────────────────────────────────────\n\ninterface DeliveryResponse {\n status: number;\n wwwAuthenticate?: string;\n}\n\nasync function deliverOnce(args: {\n url: string;\n bodyBytes: string;\n signerKey?: SignerKey;\n signerProvider?: SigningProvider;\n authentication: WebhookAuthentication;\n tag?: string;\n userAgent?: string;\n fetch: typeof fetch;\n suppressLegacyWarnings?: boolean;\n}): Promise<DeliveryResponse> {\n const headers = await buildHeaders(args);\n const response = await args.fetch(args.url, {\n method: 'POST',\n headers,\n body: args.bodyBytes,\n });\n return {\n status: response.status,\n ...(response.headers.get('www-authenticate') && { wwwAuthenticate: response.headers.get('www-authenticate')! }),\n };\n}\n\nasync function buildHeaders(args: {\n url: string;\n bodyBytes: string;\n signerKey?: SignerKey;\n signerProvider?: SigningProvider;\n authentication: WebhookAuthentication;\n tag?: string;\n userAgent?: string;\n suppressLegacyWarnings?: boolean;\n}): Promise<Record<string, string>> {\n const baseHeaders: Record<string, string> = {\n 'content-type': 'application/json',\n };\n if (args.userAgent) baseHeaders['user-agent'] = args.userAgent;\n\n // Legacy HMAC-SHA256 path. Matches docs/building/implementation/webhooks.mdx\n // §3.0 legacy section: X-ADCP-Signature + X-ADCP-Timestamp over\n // `${ts}.${raw_body_bytes}`.\n if (args.authentication?.type === 'hmac_sha256') {\n maybeWarnHmacDeprecation(args.suppressLegacyWarnings);\n const ts = Math.floor(Date.now() / 1000).toString();\n const hmac = createHmac('sha256', args.authentication.secret);\n hmac.update(`${ts}.${args.bodyBytes}`, 'utf8');\n return {\n ...baseHeaders,\n 'x-adcp-timestamp': ts,\n 'x-adcp-signature': `sha256=${hmac.digest('hex')}`,\n };\n }\n\n // Bearer fallback — the legacy path for buyers that registered an API\n // key in push_notification_config.authentication. No body signing —\n // just a header. Not recommended; strictly for interop with 2.x buyers.\n if (args.authentication?.type === 'bearer') {\n return { ...baseHeaders, authorization: `Bearer ${args.authentication.token}` };\n }\n\n // Default: 9421 webhook signing. Fresh nonce + fresh created/expires per\n // attempt, but the `idempotency_key` inside the body stays stable — the\n // signature covers the body bytes, which include the key; multiple\n // retries of the same logical event produce different signatures over\n // the same body, which is exactly what the receiver expects.\n const request: RequestLike = {\n method: 'POST',\n url: args.url,\n headers: baseHeaders,\n body: args.bodyBytes,\n };\n const signOpts = args.tag !== undefined ? { tag: args.tag } : {};\n const signed = args.signerProvider\n ? await signWebhookAsync(request, args.signerProvider, signOpts)\n : signWebhook(request, args.signerKey!, signOpts);\n return { ...baseHeaders, ...signed.headers };\n}\n\n// ────────────────────────────────────────────────────────────\n// Helpers\n// ────────────────────────────────────────────────────────────\n\nasync function resolveIdempotencyKey(\n store: WebhookIdempotencyKeyStore,\n operation_id: string,\n generate: () => string\n): Promise<string> {\n const existing = await store.get(operation_id);\n if (existing) {\n if (!IDEMPOTENCY_KEY_PATTERN.test(existing)) {\n throw new Error(\n `idempotency-key store returned \"${existing}\" for operation_id \"${operation_id}\"; ` +\n `does not match required pattern ${IDEMPOTENCY_KEY_PATTERN.source}`\n );\n }\n return existing;\n }\n const fresh = generate();\n if (!IDEMPOTENCY_KEY_PATTERN.test(fresh)) {\n throw new Error(\n `generateIdempotencyKey produced \"${fresh}\" for operation_id \"${operation_id}\"; ` +\n `must match ${IDEMPOTENCY_KEY_PATTERN.source}`\n );\n }\n await store.set(operation_id, fresh);\n return fresh;\n}\n\n/**\n * Default key generator — `evt_` prefix + a base64url 18-byte random.\n * Length 27 (comfortably within 16–255), only base64url-safe characters,\n * obviously-webhook-scoped prefix for log grepping.\n */\nfunction defaultGenerateIdempotencyKey(): string {\n const uuid = randomUUID().replace(/-/g, '');\n return `evt_${uuid.slice(0, 24)}`;\n}\n\nfunction isTerminalStatus(status: number, wwwAuthenticate?: string): boolean {\n if (status === 429) return false;\n if (status >= 500) return false;\n // 401 with a signature-layer reject is terminal per adcp#2423 —\n // retrying a signature failure produces identical bytes and identical\n // rejection. Non-signature 401s (opaque auth failures) are also\n // terminal; there's nothing the publisher can do by retrying.\n if (status === 401 && wwwAuthenticate && TERMINAL_SIGNATURE_WWW_AUTH_RE.test(wwwAuthenticate)) return true;\n if (status >= 400 && status < 500) return true;\n return false;\n}\n\nfunction backoffDelay(attempt: number, retries: Required<WebhookRetryOptions>): number {\n const base = Math.min(retries.initialDelayMs * Math.pow(2, attempt - 1), retries.maxDelayMs);\n if (retries.jitter <= 0) return base;\n const jitterWindow = base * retries.jitter;\n const offset = Math.random() * jitterWindow * 2 - jitterWindow;\n return Math.max(0, Math.floor(base + offset));\n}\n\nfunction resolveRetries(opts?: WebhookRetryOptions): Required<WebhookRetryOptions> {\n return {\n maxAttempts: Math.max(1, opts?.maxAttempts ?? 5),\n initialDelayMs: Math.max(0, opts?.initialDelayMs ?? 1000),\n maxDelayMs: Math.max(0, opts?.maxDelayMs ?? 60_000),\n jitter: Math.max(0, Math.min(1, opts?.jitter ?? 0.25)),\n };\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise(r => {\n const t = setTimeout(r, ms);\n t.unref?.();\n });\n}\n\n/**\n * Format a transport-layer error for `result.errors[]`. Walks `cause` chains\n * (undici wraps the connector failure under a generic \"fetch failed\") so\n * operators see the actual rule that fired (e.g. SSRF policy block) instead\n * of an opaque outer message.\n */\nfunction formatTransportError(err: unknown): string {\n if (!(err instanceof Error)) return String(err);\n const parts: string[] = [];\n let cur: Error | undefined = err;\n let depth = 0;\n while (cur && depth < 5) {\n const code = (cur as NodeJS.ErrnoException).code;\n parts.push(code ? `${code}: ${cur.message}` : cur.message);\n cur = cur.cause instanceof Error ? cur.cause : undefined;\n depth++;\n }\n return parts.join(' — ');\n}\n\nfunction errorContainsCode(err: unknown, code: string): boolean {\n let cur: unknown = err;\n let depth = 0;\n while (cur instanceof Error && depth < 5) {\n if ((cur as NodeJS.ErrnoException).code === code) return true;\n cur = (cur as { cause?: unknown }).cause;\n depth++;\n }\n return false;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/lib/server/webhook-emitter.ts"],"sourcesContent":["/**\n * Publisher-side webhook emitter — the symmetric counterpart to PR #629's\n * receiver-side dedup. A seller / governance agent / rights agent building\n * with `@adcp/sdk` gets a one-call API that handles:\n *\n * - RFC 9421 webhook signing on every attempt (adcp#2423).\n * - A stable `idempotency_key` per logical event, reused across retries\n * (adcp#2417) — regenerating on retry is the #1 at-least-once-delivery\n * bug the runner-side conformance suite catches.\n * - Compact-separator JSON serialization once, signed once, posted once\n * (adcp#2478) — prevents the serialization-mismatch trap where a\n * signer's byte view differs from what the HTTP client writes.\n * - Retry/backoff on 5xx and 429. Terminal on 4xx and on 401 responses\n * carrying `WWW-Authenticate: Signature error=\"webhook_signature_*\"`\n * (spec says retrying a signature failure just fails identically).\n * - HMAC-SHA256 fallback for legacy buyers that registered\n * `push_notification_config.authentication.credentials` — still pinned\n * to compact separators per adcp#2478.\n *\n * Handler authors using `createAdcpServer` call `ctx.emitWebhook(...)` with\n * a `url`, `payload`, and `operation_id` — everything else is wired in.\n */\n\nimport { signWebhook, type SignerKey } from '../signing/signer';\nimport { signWebhookAsync } from '../signing/signer-async';\nimport type { SigningProvider } from '../signing/provider';\nimport type { RequestLike } from '../signing/canonicalize';\nimport { createPinAndBindFetch } from './pin-and-bind-fetch';\nimport { createHmac, randomUUID } from 'node:crypto';\n\n/**\n * Minimum pattern per adcp#2417 / core/mcp-webhook-payload.json.\n * Publisher-side check — catches `generateIdempotencyKey` overrides that\n * produce keys too short for a conformant receiver to accept.\n */\nconst IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_.:-]{16,255}$/;\n\n/** WWW-Authenticate header pattern signaling a signature-layer reject. */\nconst TERMINAL_SIGNATURE_WWW_AUTH_RE = /Signature\\s+error=\"webhook_signature_/i;\n\n/**\n * Per-(operation_id) idempotency-key store. The key MUST be stable across\n * every retry of the same logical event — that's the load-bearing invariant\n * the receiver-side dedup depends on.\n *\n * Defaults to an in-memory Map. Production publishers with multi-replica\n * emitters SHOULD inject a durable backend (the same way\n * `AsyncHandlerConfig.webhookDedup` accepts a pluggable store on the\n * receiver side).\n */\nexport interface WebhookIdempotencyKeyStore {\n get(operation_id: string): Promise<string | undefined> | string | undefined;\n /** Called only on first-seen operation_id. Subsequent emits read via get(). */\n set(operation_id: string, key: string): Promise<void> | void;\n}\n\nexport function memoryWebhookKeyStore(): WebhookIdempotencyKeyStore {\n const m = new Map<string, string>();\n return {\n get: id => m.get(id),\n set: (id, key) => {\n m.set(id, key);\n },\n };\n}\n\n/**\n * Authentication mode for a single delivery. Omit / pass `null` to use the\n * 9421 baseline. `bearer` / `hmac_sha256` drop back to legacy flows for\n * buyers who populated `push_notification_config.authentication.credentials`.\n *\n * @deprecated The `hmac_sha256` variant is deprecated. HMAC remains in the\n * AdCP spec as a legacy fallback for buyers that registered\n * `push_notification_config.authentication.credentials`, so the SDK keeps\n * supporting it — but the spec-current path is RFC 9421 webhook signatures.\n * See docs/migration-4.x-to-5.x.md#webhook-hmac-legacy-deprecation.\n */\nexport type WebhookAuthentication = { type: 'bearer'; token: string } | { type: 'hmac_sha256'; secret: string } | null;\n\nlet hmacWarningFired = false;\n\nfunction maybeWarnHmacDeprecation(suppressLegacyWarnings?: boolean): void {\n if (hmacWarningFired) return;\n if (suppressLegacyWarnings) return;\n if (process.env.ADCP_SUPPRESS_HMAC_WARNING === '1') return;\n hmacWarningFired = true;\n console.warn(\n '[adcp] Warning: webhook HMAC-SHA256 authentication is deprecated. ' +\n 'HMAC remains supported in the AdCP spec as a legacy fallback but RFC ' +\n '9421 is the spec-current path; migrate when your counterparties are ' +\n 'ready. See docs/migration-4.x-to-5.x.md#webhook-hmac-legacy-deprecation. ' +\n 'Suppress with ADCP_SUPPRESS_HMAC_WARNING=1 (env) or ' +\n 'createWebhookEmitter({ suppressLegacyWarnings: true }) (programmatic).'\n );\n}\n\nexport interface WebhookRetryOptions {\n /** Max delivery attempts (≥1). Default 5. */\n maxAttempts?: number;\n /** Initial backoff in ms. Default 1000. */\n initialDelayMs?: number;\n /** Cap per-attempt backoff. Default 60000. */\n maxDelayMs?: number;\n /** Jitter factor ∈ [0,1]: 0 = none, 0.5 = ±50%. Default 0.25. */\n jitter?: number;\n}\n\nexport interface WebhookEmitterOptions {\n /**\n * In-process JWK signing key. Its JWKS entry SHOULD carry\n * `adcp_use: \"request-signing\"` — webhooks are signed with the agent's\n * request-signing key (the deprecated `\"webhook-signing\"` value is still\n * accepted by verifiers for backward compatibility). Mutually exclusive\n * with `signerProvider` — exactly one must be provided.\n */\n signerKey?: SignerKey;\n /**\n * Async KMS-backed signing provider (GCP KMS, AWS KMS, Azure Key Vault, etc.).\n * Routes webhook signing through `signWebhookAsync` so the private key never\n * enters process memory. Mutually exclusive with `signerKey` — exactly one\n * must be provided.\n *\n * **Key purpose.** Webhooks are signed with a `request-signing` key; domain\n * separation between requests and webhooks is carried by the signature\n * `tag`, not the `adcp_use` discriminator. The same `SigningProvider` used\n * for `request_signing.provider` MAY be reused here. To isolate webhook key\n * material (so a webhook-key compromise does not extend to request signing,\n * or to rotate independently), wrap a second `request-signing`\n * `cryptoKeyVersion` published under a distinct `kid` — isolation comes from\n * the `kid`, not a distinct `adcp_use`. (The deprecated `\"webhook-signing\"`\n * purpose is still accepted by verifiers for backward compatibility.)\n */\n signerProvider?: SigningProvider;\n retries?: WebhookRetryOptions;\n idempotencyKeyStore?: WebhookIdempotencyKeyStore;\n /**\n * Override the default idempotency-key generator. Must return a value\n * matching `/^[A-Za-z0-9_.:-]{16,255}$/` — the emitter rejects anything\n * else (a malformed key would fail the receiver's schema check, which\n * would report as a conformance violation of the publisher).\n */\n generateIdempotencyKey?: () => string;\n /**\n * Override the HTTP client. Defaults to `createPinAndBindFetch()`, which\n * defeats DNS-rebinding / SSRF attacks against the buyer-supplied\n * `push_notification_config.url` (https-only; loopback, private, and cloud\n * metadata ranges denied). See `docs/guides/SIGNING-GUIDE.md` § Webhook\n * SSRF defense.\n *\n * In-process storyboard / test harnesses that deliver to a loopback\n * `http://127.0.0.1:port` receiver must opt into the relaxed policy\n * explicitly: `fetch: createPinAndBindFetch({ policy:\n * LOOPBACK_OK_WEBHOOK_SSRF_POLICY })`. Passing `globalThis.fetch` here\n * restores the unguarded pre-9.8 behavior — only do so behind your own\n * URL validation.\n */\n fetch?: typeof fetch;\n /** Default `User-Agent` header. */\n userAgent?: string;\n /** Signing tag override. Defaults to `adcp/webhook-signing/v1`. */\n tag?: string;\n /** Observability hook called BEFORE each attempt. */\n onAttempt?: (info: WebhookEmitAttempt) => void;\n /** Observability hook called AFTER each attempt completes. */\n onAttemptResult?: (info: WebhookEmitAttemptResult) => void;\n /**\n * Sleeper override. Production uses `setTimeout`; tests inject a stub to\n * skip real backoff. Takes (ms, abortSignal) and resolves when slept.\n */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Suppress the one-time `console.warn` emitted on first HMAC-SHA256\n * webhook delivery. Programmatic equivalent of\n * `ADCP_SUPPRESS_HMAC_WARNING=1`, for libraries embedded in agents where\n * setting an env var is awkward. Does not affect the `@deprecated` JSDoc\n * flag on `WebhookAuthentication` — the type-level deprecation signal\n * always shows up in IDEs.\n */\n suppressLegacyWarnings?: boolean;\n}\n\nexport interface WebhookEmitParams {\n /** Full destination URL. Typically from `push_notification_config.url`. */\n url: string;\n /** Object body. Serialized with compact separators (adcp#2478). */\n payload: Record<string, unknown>;\n /**\n * Stable logical event id. Two emits with the same operation_id reuse\n * the same `idempotency_key` — this is the cross-attempt AND\n * cross-process invariant the receiver dedups on.\n */\n operation_id: string;\n /**\n * Per-emit override of the delivery's authentication mode. Omit for the\n * 9421 default.\n */\n authentication?: WebhookAuthentication;\n /** Per-emit retries override. */\n retries?: WebhookRetryOptions;\n}\n\nexport interface WebhookEmitAttempt {\n operation_id: string;\n idempotency_key: string;\n attempt: number;\n url: string;\n}\n\nexport interface WebhookEmitAttemptResult extends WebhookEmitAttempt {\n status?: number;\n durationMs: number;\n error?: string;\n willRetry: boolean;\n}\n\nexport interface WebhookEmitResult {\n operation_id: string;\n idempotency_key: string;\n attempts: number;\n delivered: boolean;\n final_status?: number;\n /**\n * Per-attempt error messages (transport / signer / network failures).\n *\n * **Logging caution:** when a `signerProvider` rejection bubbles into\n * this array, the message text comes from the adapter and may include\n * infra-flavored detail (KMS resource names, IAM principals, project\n * IDs). Mirrors the same caution flagged on `SigningProvider.fingerprint`.\n * If you pipe `errors[]` into shared logs / observability pipelines,\n * sanitize or redact at your boundary — adapter messages aren't\n * guaranteed to be operator-safe.\n */\n errors: string[];\n}\n\nexport interface WebhookEmitter {\n emit(params: WebhookEmitParams): Promise<WebhookEmitResult>;\n}\n\n// ────────────────────────────────────────────────────────────\n// Factory\n// ────────────────────────────────────────────────────────────\n\nexport function createWebhookEmitter(options: WebhookEmitterOptions): WebhookEmitter {\n if (options.signerKey && options.signerProvider) {\n throw new TypeError('createWebhookEmitter: provide exactly one of signerKey or signerProvider, not both');\n }\n if (!options.signerKey && !options.signerProvider) {\n throw new TypeError('createWebhookEmitter: one of signerKey or signerProvider is required');\n }\n const store = options.idempotencyKeyStore ?? memoryWebhookKeyStore();\n const generateKey = options.generateIdempotencyKey ?? defaultGenerateIdempotencyKey;\n const fetchImpl = options.fetch ?? createPinAndBindFetch();\n const sleep = options.sleep ?? defaultSleep;\n\n return {\n async emit(params: WebhookEmitParams): Promise<WebhookEmitResult> {\n const retries = resolveRetries(params.retries ?? options.retries);\n const idempotency_key = await resolveIdempotencyKey(store, params.operation_id, generateKey);\n\n // Serialize ONCE with compact separators — the same bytes feed both\n // the content-digest input and the HTTP body on every attempt. This\n // is the load-bearing rule from adcp#2478.\n const bodyPayload = { ...params.payload, idempotency_key };\n const bodyBytes = JSON.stringify(bodyPayload);\n\n const errors: string[] = [];\n let lastStatus: number | undefined;\n\n for (let attempt = 1; attempt <= retries.maxAttempts; attempt++) {\n const attemptInfo: WebhookEmitAttempt = {\n operation_id: params.operation_id,\n idempotency_key,\n attempt,\n url: params.url,\n };\n options.onAttempt?.(attemptInfo);\n\n const started = Date.now();\n let status: number | undefined;\n let error: string | undefined;\n let terminal = false;\n\n try {\n const response = await deliverOnce({\n url: params.url,\n bodyBytes,\n signerKey: options.signerKey,\n signerProvider: options.signerProvider,\n authentication: params.authentication ?? null,\n tag: options.tag,\n userAgent: options.userAgent,\n fetch: fetchImpl,\n suppressLegacyWarnings: options.suppressLegacyWarnings,\n });\n status = response.status;\n lastStatus = status;\n\n if (status >= 200 && status < 300) {\n const durationMs = Date.now() - started;\n options.onAttemptResult?.({ ...attemptInfo, status, durationMs, willRetry: false });\n return {\n operation_id: params.operation_id,\n idempotency_key,\n attempts: attempt,\n delivered: true,\n final_status: status,\n errors,\n };\n }\n\n terminal = isTerminalStatus(status, response.wwwAuthenticate);\n error =\n status >= 300 && status < 400\n ? `HTTP ${status} redirect${response.location ? ` to ${response.location}` : ''} — redirects are ` +\n `never followed for signed webhook delivery, because the signature covers @target-uri and ` +\n `would not verify at the redirect target. Re-register the webhook with the final URL.`\n : `HTTP ${status}${response.wwwAuthenticate ? ` (${response.wwwAuthenticate})` : ''}`;\n } catch (err) {\n error = formatTransportError(err);\n // Network / transport errors are retryable — the delivery didn't\n // reach the receiver, so no risk of double-processing.\n // Pin-and-bind SSRF blocks are themselves terminal: the URL\n // (or its DNS resolution) violates policy and won't change on retry.\n if (errorContainsCode(err, 'EADCP_SSRF_BLOCKED')) {\n terminal = true;\n }\n }\n\n if (error) errors.push(`attempt ${attempt}: ${error}`);\n\n const willRetry = !terminal && attempt < retries.maxAttempts;\n options.onAttemptResult?.({\n ...attemptInfo,\n ...(status !== undefined && { status }),\n durationMs: Date.now() - started,\n ...(error !== undefined && { error }),\n willRetry,\n });\n\n if (!willRetry) break;\n\n await sleep(backoffDelay(attempt, retries));\n }\n\n return {\n operation_id: params.operation_id,\n idempotency_key,\n attempts: errors.length,\n delivered: false,\n ...(lastStatus !== undefined && { final_status: lastStatus }),\n errors,\n };\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Delivery primitives\n// ────────────────────────────────────────────────────────────\n\ninterface DeliveryResponse {\n status: number;\n wwwAuthenticate?: string;\n /** `Location` on a 3xx, so the diagnostic can name the redirect target. */\n location?: string;\n}\n\nasync function deliverOnce(args: {\n url: string;\n bodyBytes: string;\n signerKey?: SignerKey;\n signerProvider?: SigningProvider;\n authentication: WebhookAuthentication;\n tag?: string;\n userAgent?: string;\n fetch: typeof fetch;\n suppressLegacyWarnings?: boolean;\n}): Promise<DeliveryResponse> {\n const headers = await buildHeaders(args);\n const response = await args.fetch(args.url, {\n method: 'POST',\n headers,\n body: args.bodyBytes,\n });\n return {\n status: response.status,\n ...(response.headers.get('www-authenticate') && { wwwAuthenticate: response.headers.get('www-authenticate')! }),\n ...(response.headers.get('location') && { location: response.headers.get('location')! }),\n };\n}\n\nasync function buildHeaders(args: {\n url: string;\n bodyBytes: string;\n signerKey?: SignerKey;\n signerProvider?: SigningProvider;\n authentication: WebhookAuthentication;\n tag?: string;\n userAgent?: string;\n suppressLegacyWarnings?: boolean;\n}): Promise<Record<string, string>> {\n const baseHeaders: Record<string, string> = {\n 'content-type': 'application/json',\n };\n if (args.userAgent) baseHeaders['user-agent'] = args.userAgent;\n\n // Legacy HMAC-SHA256 path. Matches docs/building/implementation/webhooks.mdx\n // §3.0 legacy section: X-ADCP-Signature + X-ADCP-Timestamp over\n // `${ts}.${raw_body_bytes}`.\n if (args.authentication?.type === 'hmac_sha256') {\n maybeWarnHmacDeprecation(args.suppressLegacyWarnings);\n const ts = Math.floor(Date.now() / 1000).toString();\n const hmac = createHmac('sha256', args.authentication.secret);\n hmac.update(`${ts}.${args.bodyBytes}`, 'utf8');\n return {\n ...baseHeaders,\n 'x-adcp-timestamp': ts,\n 'x-adcp-signature': `sha256=${hmac.digest('hex')}`,\n };\n }\n\n // Bearer fallback — the legacy path for buyers that registered an API\n // key in push_notification_config.authentication. No body signing —\n // just a header. Not recommended; strictly for interop with 2.x buyers.\n if (args.authentication?.type === 'bearer') {\n return { ...baseHeaders, authorization: `Bearer ${args.authentication.token}` };\n }\n\n // Default: 9421 webhook signing. Fresh nonce + fresh created/expires per\n // attempt, but the `idempotency_key` inside the body stays stable — the\n // signature covers the body bytes, which include the key; multiple\n // retries of the same logical event produce different signatures over\n // the same body, which is exactly what the receiver expects.\n const request: RequestLike = {\n method: 'POST',\n url: args.url,\n headers: baseHeaders,\n body: args.bodyBytes,\n };\n const signOpts = args.tag !== undefined ? { tag: args.tag } : {};\n const signed = args.signerProvider\n ? await signWebhookAsync(request, args.signerProvider, signOpts)\n : signWebhook(request, args.signerKey!, signOpts);\n return { ...baseHeaders, ...signed.headers };\n}\n\n// ────────────────────────────────────────────────────────────\n// Helpers\n// ────────────────────────────────────────────────────────────\n\nasync function resolveIdempotencyKey(\n store: WebhookIdempotencyKeyStore,\n operation_id: string,\n generate: () => string\n): Promise<string> {\n const existing = await store.get(operation_id);\n if (existing) {\n if (!IDEMPOTENCY_KEY_PATTERN.test(existing)) {\n throw new Error(\n `idempotency-key store returned \"${existing}\" for operation_id \"${operation_id}\"; ` +\n `does not match required pattern ${IDEMPOTENCY_KEY_PATTERN.source}`\n );\n }\n return existing;\n }\n const fresh = generate();\n if (!IDEMPOTENCY_KEY_PATTERN.test(fresh)) {\n throw new Error(\n `generateIdempotencyKey produced \"${fresh}\" for operation_id \"${operation_id}\"; ` +\n `must match ${IDEMPOTENCY_KEY_PATTERN.source}`\n );\n }\n await store.set(operation_id, fresh);\n return fresh;\n}\n\n/**\n * Default key generator — `evt_` prefix + a base64url 18-byte random.\n * Length 27 (comfortably within 16–255), only base64url-safe characters,\n * obviously-webhook-scoped prefix for log grepping.\n */\nfunction defaultGenerateIdempotencyKey(): string {\n const uuid = randomUUID().replace(/-/g, '');\n return `evt_${uuid.slice(0, 24)}`;\n}\n\nfunction isTerminalStatus(status: number, wwwAuthenticate?: string): boolean {\n if (status === 429) return false;\n if (status >= 500) return false;\n // A redirect is a configuration error, not a transient one — retrying it\n // produces the same redirect. `createPinAndBindFetch` deliberately does not\n // follow redirects (the signature covers `@target-uri`, so a replayed hop\n // would not verify at the target anyway), so a 3xx means the registered URL\n // is not the final one.\n if (status >= 300 && status < 400) return true;\n // 401 with a signature-layer reject is terminal per adcp#2423 —\n // retrying a signature failure produces identical bytes and identical\n // rejection. Non-signature 401s (opaque auth failures) are also\n // terminal; there's nothing the publisher can do by retrying.\n if (status === 401 && wwwAuthenticate && TERMINAL_SIGNATURE_WWW_AUTH_RE.test(wwwAuthenticate)) return true;\n if (status >= 400 && status < 500) return true;\n return false;\n}\n\nfunction backoffDelay(attempt: number, retries: Required<WebhookRetryOptions>): number {\n const base = Math.min(retries.initialDelayMs * Math.pow(2, attempt - 1), retries.maxDelayMs);\n if (retries.jitter <= 0) return base;\n const jitterWindow = base * retries.jitter;\n const offset = Math.random() * jitterWindow * 2 - jitterWindow;\n return Math.max(0, Math.floor(base + offset));\n}\n\nfunction resolveRetries(opts?: WebhookRetryOptions): Required<WebhookRetryOptions> {\n return {\n maxAttempts: Math.max(1, opts?.maxAttempts ?? 5),\n initialDelayMs: Math.max(0, opts?.initialDelayMs ?? 1000),\n maxDelayMs: Math.max(0, opts?.maxDelayMs ?? 60_000),\n jitter: Math.max(0, Math.min(1, opts?.jitter ?? 0.25)),\n };\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise(r => {\n const t = setTimeout(r, ms);\n t.unref?.();\n });\n}\n\n/**\n * Format a transport-layer error for `result.errors[]`. Walks `cause` chains\n * (undici wraps the connector failure under a generic \"fetch failed\") so\n * operators see the actual rule that fired (e.g. SSRF policy block) instead\n * of an opaque outer message.\n */\nfunction formatTransportError(err: unknown): string {\n if (!(err instanceof Error)) return String(err);\n const parts: string[] = [];\n let cur: Error | undefined = err;\n let depth = 0;\n while (cur && depth < 5) {\n const code = (cur as NodeJS.ErrnoException).code;\n parts.push(code ? `${code}: ${cur.message}` : cur.message);\n cur = cur.cause instanceof Error ? cur.cause : undefined;\n depth++;\n }\n return parts.join(' — ');\n}\n\nfunction errorContainsCode(err: unknown, code: string): boolean {\n let cur: unknown = err;\n let depth = 0;\n while (cur instanceof Error && depth < 5) {\n if ((cur as NodeJS.ErrnoException).code === code) return true;\n cur = (cur as { cause?: unknown }).cause;\n depth++;\n }\n return false;\n}\n"],"mappings":"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","names":[]}
@@ -81,7 +81,7 @@ function createWebhookEmitter(options) {
81
81
  };
82
82
  }
83
83
  terminal = isTerminalStatus(status, response.wwwAuthenticate);
84
- error = `HTTP ${status}${response.wwwAuthenticate ? ` (${response.wwwAuthenticate})` : ""}`;
84
+ error = status >= 300 && status < 400 ? `HTTP ${status} redirect${response.location ? ` to ${response.location}` : ""} \u2014 redirects are never followed for signed webhook delivery, because the signature covers @target-uri and would not verify at the redirect target. Re-register the webhook with the final URL.` : `HTTP ${status}${response.wwwAuthenticate ? ` (${response.wwwAuthenticate})` : ""}`;
85
85
  } catch (err) {
86
86
  error = formatTransportError(err);
87
87
  if (errorContainsCode(err, "EADCP_SSRF_BLOCKED")) {
@@ -120,7 +120,8 @@ async function deliverOnce(args) {
120
120
  });
121
121
  return {
122
122
  status: response.status,
123
- ...response.headers.get("www-authenticate") && { wwwAuthenticate: response.headers.get("www-authenticate") }
123
+ ...response.headers.get("www-authenticate") && { wwwAuthenticate: response.headers.get("www-authenticate") },
124
+ ...response.headers.get("location") && { location: response.headers.get("location") }
124
125
  };
125
126
  }
126
127
  async function buildHeaders(args) {
@@ -178,6 +179,7 @@ function defaultGenerateIdempotencyKey() {
178
179
  function isTerminalStatus(status, wwwAuthenticate) {
179
180
  if (status === 429) return false;
180
181
  if (status >= 500) return false;
182
+ if (status >= 300 && status < 400) return true;
181
183
  if (status === 401 && wwwAuthenticate && TERMINAL_SIGNATURE_WWW_AUTH_RE.test(wwwAuthenticate)) return true;
182
184
  if (status >= 400 && status < 500) return true;
183
185
  return false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/server/webhook-emitter.ts"],"sourcesContent":["/**\n * Publisher-side webhook emitter — the symmetric counterpart to PR #629's\n * receiver-side dedup. A seller / governance agent / rights agent building\n * with `@adcp/sdk` gets a one-call API that handles:\n *\n * - RFC 9421 webhook signing on every attempt (adcp#2423).\n * - A stable `idempotency_key` per logical event, reused across retries\n * (adcp#2417) — regenerating on retry is the #1 at-least-once-delivery\n * bug the runner-side conformance suite catches.\n * - Compact-separator JSON serialization once, signed once, posted once\n * (adcp#2478) — prevents the serialization-mismatch trap where a\n * signer's byte view differs from what the HTTP client writes.\n * - Retry/backoff on 5xx and 429. Terminal on 4xx and on 401 responses\n * carrying `WWW-Authenticate: Signature error=\"webhook_signature_*\"`\n * (spec says retrying a signature failure just fails identically).\n * - HMAC-SHA256 fallback for legacy buyers that registered\n * `push_notification_config.authentication.credentials` — still pinned\n * to compact separators per adcp#2478.\n *\n * Handler authors using `createAdcpServer` call `ctx.emitWebhook(...)` with\n * a `url`, `payload`, and `operation_id` — everything else is wired in.\n */\n\nimport { signWebhook, type SignerKey } from '../signing/signer';\nimport { signWebhookAsync } from '../signing/signer-async';\nimport type { SigningProvider } from '../signing/provider';\nimport type { RequestLike } from '../signing/canonicalize';\nimport { createPinAndBindFetch } from './pin-and-bind-fetch';\nimport { createHmac, randomUUID } from 'node:crypto';\n\n/**\n * Minimum pattern per adcp#2417 / core/mcp-webhook-payload.json.\n * Publisher-side check — catches `generateIdempotencyKey` overrides that\n * produce keys too short for a conformant receiver to accept.\n */\nconst IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_.:-]{16,255}$/;\n\n/** WWW-Authenticate header pattern signaling a signature-layer reject. */\nconst TERMINAL_SIGNATURE_WWW_AUTH_RE = /Signature\\s+error=\"webhook_signature_/i;\n\n/**\n * Per-(operation_id) idempotency-key store. The key MUST be stable across\n * every retry of the same logical event — that's the load-bearing invariant\n * the receiver-side dedup depends on.\n *\n * Defaults to an in-memory Map. Production publishers with multi-replica\n * emitters SHOULD inject a durable backend (the same way\n * `AsyncHandlerConfig.webhookDedup` accepts a pluggable store on the\n * receiver side).\n */\nexport interface WebhookIdempotencyKeyStore {\n get(operation_id: string): Promise<string | undefined> | string | undefined;\n /** Called only on first-seen operation_id. Subsequent emits read via get(). */\n set(operation_id: string, key: string): Promise<void> | void;\n}\n\nexport function memoryWebhookKeyStore(): WebhookIdempotencyKeyStore {\n const m = new Map<string, string>();\n return {\n get: id => m.get(id),\n set: (id, key) => {\n m.set(id, key);\n },\n };\n}\n\n/**\n * Authentication mode for a single delivery. Omit / pass `null` to use the\n * 9421 baseline. `bearer` / `hmac_sha256` drop back to legacy flows for\n * buyers who populated `push_notification_config.authentication.credentials`.\n *\n * @deprecated The `hmac_sha256` variant is deprecated. HMAC remains in the\n * AdCP spec as a legacy fallback for buyers that registered\n * `push_notification_config.authentication.credentials`, so the SDK keeps\n * supporting it — but the spec-current path is RFC 9421 webhook signatures.\n * See docs/migration-4.x-to-5.x.md#webhook-hmac-legacy-deprecation.\n */\nexport type WebhookAuthentication = { type: 'bearer'; token: string } | { type: 'hmac_sha256'; secret: string } | null;\n\nlet hmacWarningFired = false;\n\nfunction maybeWarnHmacDeprecation(suppressLegacyWarnings?: boolean): void {\n if (hmacWarningFired) return;\n if (suppressLegacyWarnings) return;\n if (process.env.ADCP_SUPPRESS_HMAC_WARNING === '1') return;\n hmacWarningFired = true;\n console.warn(\n '[adcp] Warning: webhook HMAC-SHA256 authentication is deprecated. ' +\n 'HMAC remains supported in the AdCP spec as a legacy fallback but RFC ' +\n '9421 is the spec-current path; migrate when your counterparties are ' +\n 'ready. See docs/migration-4.x-to-5.x.md#webhook-hmac-legacy-deprecation. ' +\n 'Suppress with ADCP_SUPPRESS_HMAC_WARNING=1 (env) or ' +\n 'createWebhookEmitter({ suppressLegacyWarnings: true }) (programmatic).'\n );\n}\n\nexport interface WebhookRetryOptions {\n /** Max delivery attempts (≥1). Default 5. */\n maxAttempts?: number;\n /** Initial backoff in ms. Default 1000. */\n initialDelayMs?: number;\n /** Cap per-attempt backoff. Default 60000. */\n maxDelayMs?: number;\n /** Jitter factor ∈ [0,1]: 0 = none, 0.5 = ±50%. Default 0.25. */\n jitter?: number;\n}\n\nexport interface WebhookEmitterOptions {\n /**\n * In-process JWK signing key. Its JWKS entry SHOULD carry\n * `adcp_use: \"request-signing\"` — webhooks are signed with the agent's\n * request-signing key (the deprecated `\"webhook-signing\"` value is still\n * accepted by verifiers for backward compatibility). Mutually exclusive\n * with `signerProvider` — exactly one must be provided.\n */\n signerKey?: SignerKey;\n /**\n * Async KMS-backed signing provider (GCP KMS, AWS KMS, Azure Key Vault, etc.).\n * Routes webhook signing through `signWebhookAsync` so the private key never\n * enters process memory. Mutually exclusive with `signerKey` — exactly one\n * must be provided.\n *\n * **Key purpose.** Webhooks are signed with a `request-signing` key; domain\n * separation between requests and webhooks is carried by the signature\n * `tag`, not the `adcp_use` discriminator. The same `SigningProvider` used\n * for `request_signing.provider` MAY be reused here. To isolate webhook key\n * material (so a webhook-key compromise does not extend to request signing,\n * or to rotate independently), wrap a second `request-signing`\n * `cryptoKeyVersion` published under a distinct `kid` — isolation comes from\n * the `kid`, not a distinct `adcp_use`. (The deprecated `\"webhook-signing\"`\n * purpose is still accepted by verifiers for backward compatibility.)\n */\n signerProvider?: SigningProvider;\n retries?: WebhookRetryOptions;\n idempotencyKeyStore?: WebhookIdempotencyKeyStore;\n /**\n * Override the default idempotency-key generator. Must return a value\n * matching `/^[A-Za-z0-9_.:-]{16,255}$/` — the emitter rejects anything\n * else (a malformed key would fail the receiver's schema check, which\n * would report as a conformance violation of the publisher).\n */\n generateIdempotencyKey?: () => string;\n /**\n * Override the HTTP client. Defaults to `createPinAndBindFetch()`, which\n * defeats DNS-rebinding / SSRF attacks against the buyer-supplied\n * `push_notification_config.url` (https-only; loopback, private, and cloud\n * metadata ranges denied). See `docs/guides/SIGNING-GUIDE.md` § Webhook\n * SSRF defense.\n *\n * In-process storyboard / test harnesses that deliver to a loopback\n * `http://127.0.0.1:port` receiver must opt into the relaxed policy\n * explicitly: `fetch: createPinAndBindFetch({ policy:\n * LOOPBACK_OK_WEBHOOK_SSRF_POLICY })`. Passing `globalThis.fetch` here\n * restores the unguarded pre-9.8 behavior — only do so behind your own\n * URL validation.\n */\n fetch?: typeof fetch;\n /** Default `User-Agent` header. */\n userAgent?: string;\n /** Signing tag override. Defaults to `adcp/webhook-signing/v1`. */\n tag?: string;\n /** Observability hook called BEFORE each attempt. */\n onAttempt?: (info: WebhookEmitAttempt) => void;\n /** Observability hook called AFTER each attempt completes. */\n onAttemptResult?: (info: WebhookEmitAttemptResult) => void;\n /**\n * Sleeper override. Production uses `setTimeout`; tests inject a stub to\n * skip real backoff. Takes (ms, abortSignal) and resolves when slept.\n */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Suppress the one-time `console.warn` emitted on first HMAC-SHA256\n * webhook delivery. Programmatic equivalent of\n * `ADCP_SUPPRESS_HMAC_WARNING=1`, for libraries embedded in agents where\n * setting an env var is awkward. Does not affect the `@deprecated` JSDoc\n * flag on `WebhookAuthentication` — the type-level deprecation signal\n * always shows up in IDEs.\n */\n suppressLegacyWarnings?: boolean;\n}\n\nexport interface WebhookEmitParams {\n /** Full destination URL. Typically from `push_notification_config.url`. */\n url: string;\n /** Object body. Serialized with compact separators (adcp#2478). */\n payload: Record<string, unknown>;\n /**\n * Stable logical event id. Two emits with the same operation_id reuse\n * the same `idempotency_key` — this is the cross-attempt AND\n * cross-process invariant the receiver dedups on.\n */\n operation_id: string;\n /**\n * Per-emit override of the delivery's authentication mode. Omit for the\n * 9421 default.\n */\n authentication?: WebhookAuthentication;\n /** Per-emit retries override. */\n retries?: WebhookRetryOptions;\n}\n\nexport interface WebhookEmitAttempt {\n operation_id: string;\n idempotency_key: string;\n attempt: number;\n url: string;\n}\n\nexport interface WebhookEmitAttemptResult extends WebhookEmitAttempt {\n status?: number;\n durationMs: number;\n error?: string;\n willRetry: boolean;\n}\n\nexport interface WebhookEmitResult {\n operation_id: string;\n idempotency_key: string;\n attempts: number;\n delivered: boolean;\n final_status?: number;\n /**\n * Per-attempt error messages (transport / signer / network failures).\n *\n * **Logging caution:** when a `signerProvider` rejection bubbles into\n * this array, the message text comes from the adapter and may include\n * infra-flavored detail (KMS resource names, IAM principals, project\n * IDs). Mirrors the same caution flagged on `SigningProvider.fingerprint`.\n * If you pipe `errors[]` into shared logs / observability pipelines,\n * sanitize or redact at your boundary — adapter messages aren't\n * guaranteed to be operator-safe.\n */\n errors: string[];\n}\n\nexport interface WebhookEmitter {\n emit(params: WebhookEmitParams): Promise<WebhookEmitResult>;\n}\n\n// ────────────────────────────────────────────────────────────\n// Factory\n// ────────────────────────────────────────────────────────────\n\nexport function createWebhookEmitter(options: WebhookEmitterOptions): WebhookEmitter {\n if (options.signerKey && options.signerProvider) {\n throw new TypeError('createWebhookEmitter: provide exactly one of signerKey or signerProvider, not both');\n }\n if (!options.signerKey && !options.signerProvider) {\n throw new TypeError('createWebhookEmitter: one of signerKey or signerProvider is required');\n }\n const store = options.idempotencyKeyStore ?? memoryWebhookKeyStore();\n const generateKey = options.generateIdempotencyKey ?? defaultGenerateIdempotencyKey;\n const fetchImpl = options.fetch ?? createPinAndBindFetch();\n const sleep = options.sleep ?? defaultSleep;\n\n return {\n async emit(params: WebhookEmitParams): Promise<WebhookEmitResult> {\n const retries = resolveRetries(params.retries ?? options.retries);\n const idempotency_key = await resolveIdempotencyKey(store, params.operation_id, generateKey);\n\n // Serialize ONCE with compact separators — the same bytes feed both\n // the content-digest input and the HTTP body on every attempt. This\n // is the load-bearing rule from adcp#2478.\n const bodyPayload = { ...params.payload, idempotency_key };\n const bodyBytes = JSON.stringify(bodyPayload);\n\n const errors: string[] = [];\n let lastStatus: number | undefined;\n\n for (let attempt = 1; attempt <= retries.maxAttempts; attempt++) {\n const attemptInfo: WebhookEmitAttempt = {\n operation_id: params.operation_id,\n idempotency_key,\n attempt,\n url: params.url,\n };\n options.onAttempt?.(attemptInfo);\n\n const started = Date.now();\n let status: number | undefined;\n let error: string | undefined;\n let terminal = false;\n\n try {\n const response = await deliverOnce({\n url: params.url,\n bodyBytes,\n signerKey: options.signerKey,\n signerProvider: options.signerProvider,\n authentication: params.authentication ?? null,\n tag: options.tag,\n userAgent: options.userAgent,\n fetch: fetchImpl,\n suppressLegacyWarnings: options.suppressLegacyWarnings,\n });\n status = response.status;\n lastStatus = status;\n\n if (status >= 200 && status < 300) {\n const durationMs = Date.now() - started;\n options.onAttemptResult?.({ ...attemptInfo, status, durationMs, willRetry: false });\n return {\n operation_id: params.operation_id,\n idempotency_key,\n attempts: attempt,\n delivered: true,\n final_status: status,\n errors,\n };\n }\n\n terminal = isTerminalStatus(status, response.wwwAuthenticate);\n error = `HTTP ${status}${response.wwwAuthenticate ? ` (${response.wwwAuthenticate})` : ''}`;\n } catch (err) {\n error = formatTransportError(err);\n // Network / transport errors are retryable — the delivery didn't\n // reach the receiver, so no risk of double-processing.\n // Pin-and-bind SSRF blocks are themselves terminal: the URL\n // (or its DNS resolution) violates policy and won't change on retry.\n if (errorContainsCode(err, 'EADCP_SSRF_BLOCKED')) {\n terminal = true;\n }\n }\n\n if (error) errors.push(`attempt ${attempt}: ${error}`);\n\n const willRetry = !terminal && attempt < retries.maxAttempts;\n options.onAttemptResult?.({\n ...attemptInfo,\n ...(status !== undefined && { status }),\n durationMs: Date.now() - started,\n ...(error !== undefined && { error }),\n willRetry,\n });\n\n if (!willRetry) break;\n\n await sleep(backoffDelay(attempt, retries));\n }\n\n return {\n operation_id: params.operation_id,\n idempotency_key,\n attempts: errors.length,\n delivered: false,\n ...(lastStatus !== undefined && { final_status: lastStatus }),\n errors,\n };\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Delivery primitives\n// ────────────────────────────────────────────────────────────\n\ninterface DeliveryResponse {\n status: number;\n wwwAuthenticate?: string;\n}\n\nasync function deliverOnce(args: {\n url: string;\n bodyBytes: string;\n signerKey?: SignerKey;\n signerProvider?: SigningProvider;\n authentication: WebhookAuthentication;\n tag?: string;\n userAgent?: string;\n fetch: typeof fetch;\n suppressLegacyWarnings?: boolean;\n}): Promise<DeliveryResponse> {\n const headers = await buildHeaders(args);\n const response = await args.fetch(args.url, {\n method: 'POST',\n headers,\n body: args.bodyBytes,\n });\n return {\n status: response.status,\n ...(response.headers.get('www-authenticate') && { wwwAuthenticate: response.headers.get('www-authenticate')! }),\n };\n}\n\nasync function buildHeaders(args: {\n url: string;\n bodyBytes: string;\n signerKey?: SignerKey;\n signerProvider?: SigningProvider;\n authentication: WebhookAuthentication;\n tag?: string;\n userAgent?: string;\n suppressLegacyWarnings?: boolean;\n}): Promise<Record<string, string>> {\n const baseHeaders: Record<string, string> = {\n 'content-type': 'application/json',\n };\n if (args.userAgent) baseHeaders['user-agent'] = args.userAgent;\n\n // Legacy HMAC-SHA256 path. Matches docs/building/implementation/webhooks.mdx\n // §3.0 legacy section: X-ADCP-Signature + X-ADCP-Timestamp over\n // `${ts}.${raw_body_bytes}`.\n if (args.authentication?.type === 'hmac_sha256') {\n maybeWarnHmacDeprecation(args.suppressLegacyWarnings);\n const ts = Math.floor(Date.now() / 1000).toString();\n const hmac = createHmac('sha256', args.authentication.secret);\n hmac.update(`${ts}.${args.bodyBytes}`, 'utf8');\n return {\n ...baseHeaders,\n 'x-adcp-timestamp': ts,\n 'x-adcp-signature': `sha256=${hmac.digest('hex')}`,\n };\n }\n\n // Bearer fallback — the legacy path for buyers that registered an API\n // key in push_notification_config.authentication. No body signing —\n // just a header. Not recommended; strictly for interop with 2.x buyers.\n if (args.authentication?.type === 'bearer') {\n return { ...baseHeaders, authorization: `Bearer ${args.authentication.token}` };\n }\n\n // Default: 9421 webhook signing. Fresh nonce + fresh created/expires per\n // attempt, but the `idempotency_key` inside the body stays stable — the\n // signature covers the body bytes, which include the key; multiple\n // retries of the same logical event produce different signatures over\n // the same body, which is exactly what the receiver expects.\n const request: RequestLike = {\n method: 'POST',\n url: args.url,\n headers: baseHeaders,\n body: args.bodyBytes,\n };\n const signOpts = args.tag !== undefined ? { tag: args.tag } : {};\n const signed = args.signerProvider\n ? await signWebhookAsync(request, args.signerProvider, signOpts)\n : signWebhook(request, args.signerKey!, signOpts);\n return { ...baseHeaders, ...signed.headers };\n}\n\n// ────────────────────────────────────────────────────────────\n// Helpers\n// ────────────────────────────────────────────────────────────\n\nasync function resolveIdempotencyKey(\n store: WebhookIdempotencyKeyStore,\n operation_id: string,\n generate: () => string\n): Promise<string> {\n const existing = await store.get(operation_id);\n if (existing) {\n if (!IDEMPOTENCY_KEY_PATTERN.test(existing)) {\n throw new Error(\n `idempotency-key store returned \"${existing}\" for operation_id \"${operation_id}\"; ` +\n `does not match required pattern ${IDEMPOTENCY_KEY_PATTERN.source}`\n );\n }\n return existing;\n }\n const fresh = generate();\n if (!IDEMPOTENCY_KEY_PATTERN.test(fresh)) {\n throw new Error(\n `generateIdempotencyKey produced \"${fresh}\" for operation_id \"${operation_id}\"; ` +\n `must match ${IDEMPOTENCY_KEY_PATTERN.source}`\n );\n }\n await store.set(operation_id, fresh);\n return fresh;\n}\n\n/**\n * Default key generator — `evt_` prefix + a base64url 18-byte random.\n * Length 27 (comfortably within 16–255), only base64url-safe characters,\n * obviously-webhook-scoped prefix for log grepping.\n */\nfunction defaultGenerateIdempotencyKey(): string {\n const uuid = randomUUID().replace(/-/g, '');\n return `evt_${uuid.slice(0, 24)}`;\n}\n\nfunction isTerminalStatus(status: number, wwwAuthenticate?: string): boolean {\n if (status === 429) return false;\n if (status >= 500) return false;\n // 401 with a signature-layer reject is terminal per adcp#2423 —\n // retrying a signature failure produces identical bytes and identical\n // rejection. Non-signature 401s (opaque auth failures) are also\n // terminal; there's nothing the publisher can do by retrying.\n if (status === 401 && wwwAuthenticate && TERMINAL_SIGNATURE_WWW_AUTH_RE.test(wwwAuthenticate)) return true;\n if (status >= 400 && status < 500) return true;\n return false;\n}\n\nfunction backoffDelay(attempt: number, retries: Required<WebhookRetryOptions>): number {\n const base = Math.min(retries.initialDelayMs * Math.pow(2, attempt - 1), retries.maxDelayMs);\n if (retries.jitter <= 0) return base;\n const jitterWindow = base * retries.jitter;\n const offset = Math.random() * jitterWindow * 2 - jitterWindow;\n return Math.max(0, Math.floor(base + offset));\n}\n\nfunction resolveRetries(opts?: WebhookRetryOptions): Required<WebhookRetryOptions> {\n return {\n maxAttempts: Math.max(1, opts?.maxAttempts ?? 5),\n initialDelayMs: Math.max(0, opts?.initialDelayMs ?? 1000),\n maxDelayMs: Math.max(0, opts?.maxDelayMs ?? 60_000),\n jitter: Math.max(0, Math.min(1, opts?.jitter ?? 0.25)),\n };\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise(r => {\n const t = setTimeout(r, ms);\n t.unref?.();\n });\n}\n\n/**\n * Format a transport-layer error for `result.errors[]`. Walks `cause` chains\n * (undici wraps the connector failure under a generic \"fetch failed\") so\n * operators see the actual rule that fired (e.g. SSRF policy block) instead\n * of an opaque outer message.\n */\nfunction formatTransportError(err: unknown): string {\n if (!(err instanceof Error)) return String(err);\n const parts: string[] = [];\n let cur: Error | undefined = err;\n let depth = 0;\n while (cur && depth < 5) {\n const code = (cur as NodeJS.ErrnoException).code;\n parts.push(code ? `${code}: ${cur.message}` : cur.message);\n cur = cur.cause instanceof Error ? cur.cause : undefined;\n depth++;\n }\n return parts.join(' — ');\n}\n\nfunction errorContainsCode(err: unknown, code: string): boolean {\n let cur: unknown = err;\n let depth = 0;\n while (cur instanceof Error && depth < 5) {\n if ((cur as NodeJS.ErrnoException).code === code) return true;\n cur = (cur as { cause?: unknown }).cause;\n depth++;\n }\n return false;\n}\n"],"mappings":"AAuBA,SAAS,mBAAmC;AAC5C,SAAS,wBAAwB;AAGjC,SAAS,6BAA6B;AACtC,SAAS,YAAY,kBAAkB;AAOvC,MAAM,0BAA0B;AAGhC,MAAM,iCAAiC;AAkBhC,SAAS,wBAAoD;AAClE,QAAM,IAAI,oBAAI,IAAoB;AAClC,SAAO;AAAA,IACL,KAAK,QAAM,EAAE,IAAI,EAAE;AAAA,IACnB,KAAK,CAAC,IAAI,QAAQ;AAChB,QAAE,IAAI,IAAI,GAAG;AAAA,IACf;AAAA,EACF;AACF;AAeA,IAAI,mBAAmB;AAEvB,SAAS,yBAAyB,wBAAwC;AACxE,MAAI,iBAAkB;AACtB,MAAI,uBAAwB;AAC5B,MAAI,QAAQ,IAAI,+BAA+B,IAAK;AACpD,qBAAmB;AACnB,UAAQ;AAAA,IACN;AAAA,EAMF;AACF;AAqJO,SAAS,qBAAqB,SAAgD;AACnF,MAAI,QAAQ,aAAa,QAAQ,gBAAgB;AAC/C,UAAM,IAAI,UAAU,oFAAoF;AAAA,EAC1G;AACA,MAAI,CAAC,QAAQ,aAAa,CAAC,QAAQ,gBAAgB;AACjD,UAAM,IAAI,UAAU,sEAAsE;AAAA,EAC5F;AACA,QAAM,QAAQ,QAAQ,uBAAuB,sBAAsB;AACnE,QAAM,cAAc,QAAQ,0BAA0B;AACtD,QAAM,YAAY,QAAQ,SAAS,sBAAsB;AACzD,QAAM,QAAQ,QAAQ,SAAS;AAE/B,SAAO;AAAA,IACL,MAAM,KAAK,QAAuD;AAChE,YAAM,UAAU,eAAe,OAAO,WAAW,QAAQ,OAAO;AAChE,YAAM,kBAAkB,MAAM,sBAAsB,OAAO,OAAO,cAAc,WAAW;AAK3F,YAAM,cAAc,EAAE,GAAG,OAAO,SAAS,gBAAgB;AACzD,YAAM,YAAY,KAAK,UAAU,WAAW;AAE5C,YAAM,SAAmB,CAAC;AAC1B,UAAI;AAEJ,eAAS,UAAU,GAAG,WAAW,QAAQ,aAAa,WAAW;AAC/D,cAAM,cAAkC;AAAA,UACtC,cAAc,OAAO;AAAA,UACrB;AAAA,UACA;AAAA,UACA,KAAK,OAAO;AAAA,QACd;AACA,gBAAQ,YAAY,WAAW;AAE/B,cAAM,UAAU,KAAK,IAAI;AACzB,YAAI;AACJ,YAAI;AACJ,YAAI,WAAW;AAEf,YAAI;AACF,gBAAM,WAAW,MAAM,YAAY;AAAA,YACjC,KAAK,OAAO;AAAA,YACZ;AAAA,YACA,WAAW,QAAQ;AAAA,YACnB,gBAAgB,QAAQ;AAAA,YACxB,gBAAgB,OAAO,kBAAkB;AAAA,YACzC,KAAK,QAAQ;AAAA,YACb,WAAW,QAAQ;AAAA,YACnB,OAAO;AAAA,YACP,wBAAwB,QAAQ;AAAA,UAClC,CAAC;AACD,mBAAS,SAAS;AAClB,uBAAa;AAEb,cAAI,UAAU,OAAO,SAAS,KAAK;AACjC,kBAAM,aAAa,KAAK,IAAI,IAAI;AAChC,oBAAQ,kBAAkB,EAAE,GAAG,aAAa,QAAQ,YAAY,WAAW,MAAM,CAAC;AAClF,mBAAO;AAAA,cACL,cAAc,OAAO;AAAA,cACrB;AAAA,cACA,UAAU;AAAA,cACV,WAAW;AAAA,cACX,cAAc;AAAA,cACd;AAAA,YACF;AAAA,UACF;AAEA,qBAAW,iBAAiB,QAAQ,SAAS,eAAe;AAC5D,kBAAQ,QAAQ,MAAM,GAAG,SAAS,kBAAkB,KAAK,SAAS,eAAe,MAAM,EAAE;AAAA,QAC3F,SAAS,KAAK;AACZ,kBAAQ,qBAAqB,GAAG;AAKhC,cAAI,kBAAkB,KAAK,oBAAoB,GAAG;AAChD,uBAAW;AAAA,UACb;AAAA,QACF;AAEA,YAAI,MAAO,QAAO,KAAK,WAAW,OAAO,KAAK,KAAK,EAAE;AAErD,cAAM,YAAY,CAAC,YAAY,UAAU,QAAQ;AACjD,gBAAQ,kBAAkB;AAAA,UACxB,GAAG;AAAA,UACH,GAAI,WAAW,UAAa,EAAE,OAAO;AAAA,UACrC,YAAY,KAAK,IAAI,IAAI;AAAA,UACzB,GAAI,UAAU,UAAa,EAAE,MAAM;AAAA,UACnC;AAAA,QACF,CAAC;AAED,YAAI,CAAC,UAAW;AAEhB,cAAM,MAAM,aAAa,SAAS,OAAO,CAAC;AAAA,MAC5C;AAEA,aAAO;AAAA,QACL,cAAc,OAAO;AAAA,QACrB;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,WAAW;AAAA,QACX,GAAI,eAAe,UAAa,EAAE,cAAc,WAAW;AAAA,QAC3D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAWA,eAAe,YAAY,MAUG;AAC5B,QAAM,UAAU,MAAM,aAAa,IAAI;AACvC,QAAM,WAAW,MAAM,KAAK,MAAM,KAAK,KAAK;AAAA,IAC1C,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,KAAK;AAAA,EACb,CAAC;AACD,SAAO;AAAA,IACL,QAAQ,SAAS;AAAA,IACjB,GAAI,SAAS,QAAQ,IAAI,kBAAkB,KAAK,EAAE,iBAAiB,SAAS,QAAQ,IAAI,kBAAkB,EAAG;AAAA,EAC/G;AACF;AAEA,eAAe,aAAa,MASQ;AAClC,QAAM,cAAsC;AAAA,IAC1C,gBAAgB;AAAA,EAClB;AACA,MAAI,KAAK,UAAW,aAAY,YAAY,IAAI,KAAK;AAKrD,MAAI,KAAK,gBAAgB,SAAS,eAAe;AAC/C,6BAAyB,KAAK,sBAAsB;AACpD,UAAM,KAAK,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,EAAE,SAAS;AAClD,UAAM,OAAO,WAAW,UAAU,KAAK,eAAe,MAAM;AAC5D,SAAK,OAAO,GAAG,EAAE,IAAI,KAAK,SAAS,IAAI,MAAM;AAC7C,WAAO;AAAA,MACL,GAAG;AAAA,MACH,oBAAoB;AAAA,MACpB,oBAAoB,UAAU,KAAK,OAAO,KAAK,CAAC;AAAA,IAClD;AAAA,EACF;AAKA,MAAI,KAAK,gBAAgB,SAAS,UAAU;AAC1C,WAAO,EAAE,GAAG,aAAa,eAAe,UAAU,KAAK,eAAe,KAAK,GAAG;AAAA,EAChF;AAOA,QAAM,UAAuB;AAAA,IAC3B,QAAQ;AAAA,IACR,KAAK,KAAK;AAAA,IACV,SAAS;AAAA,IACT,MAAM,KAAK;AAAA,EACb;AACA,QAAM,WAAW,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAC/D,QAAM,SAAS,KAAK,iBAChB,MAAM,iBAAiB,SAAS,KAAK,gBAAgB,QAAQ,IAC7D,YAAY,SAAS,KAAK,WAAY,QAAQ;AAClD,SAAO,EAAE,GAAG,aAAa,GAAG,OAAO,QAAQ;AAC7C;AAMA,eAAe,sBACb,OACA,cACA,UACiB;AACjB,QAAM,WAAW,MAAM,MAAM,IAAI,YAAY;AAC7C,MAAI,UAAU;AACZ,QAAI,CAAC,wBAAwB,KAAK,QAAQ,GAAG;AAC3C,YAAM,IAAI;AAAA,QACR,mCAAmC,QAAQ,uBAAuB,YAAY,sCACzC,wBAAwB,MAAM;AAAA,MACrE;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,SAAS;AACvB,MAAI,CAAC,wBAAwB,KAAK,KAAK,GAAG;AACxC,UAAM,IAAI;AAAA,MACR,oCAAoC,KAAK,uBAAuB,YAAY,iBAC5D,wBAAwB,MAAM;AAAA,IAChD;AAAA,EACF;AACA,QAAM,MAAM,IAAI,cAAc,KAAK;AACnC,SAAO;AACT;AAOA,SAAS,gCAAwC;AAC/C,QAAM,OAAO,WAAW,EAAE,QAAQ,MAAM,EAAE;AAC1C,SAAO,OAAO,KAAK,MAAM,GAAG,EAAE,CAAC;AACjC;AAEA,SAAS,iBAAiB,QAAgB,iBAAmC;AAC3E,MAAI,WAAW,IAAK,QAAO;AAC3B,MAAI,UAAU,IAAK,QAAO;AAK1B,MAAI,WAAW,OAAO,mBAAmB,+BAA+B,KAAK,eAAe,EAAG,QAAO;AACtG,MAAI,UAAU,OAAO,SAAS,IAAK,QAAO;AAC1C,SAAO;AACT;AAEA,SAAS,aAAa,SAAiB,SAAgD;AACrF,QAAM,OAAO,KAAK,IAAI,QAAQ,iBAAiB,KAAK,IAAI,GAAG,UAAU,CAAC,GAAG,QAAQ,UAAU;AAC3F,MAAI,QAAQ,UAAU,EAAG,QAAO;AAChC,QAAM,eAAe,OAAO,QAAQ;AACpC,QAAM,SAAS,KAAK,OAAO,IAAI,eAAe,IAAI;AAClD,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,MAAM,CAAC;AAC9C;AAEA,SAAS,eAAe,MAA2D;AACjF,SAAO;AAAA,IACL,aAAa,KAAK,IAAI,GAAG,MAAM,eAAe,CAAC;AAAA,IAC/C,gBAAgB,KAAK,IAAI,GAAG,MAAM,kBAAkB,GAAI;AAAA,IACxD,YAAY,KAAK,IAAI,GAAG,MAAM,cAAc,GAAM;AAAA,IAClD,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,UAAU,IAAI,CAAC;AAAA,EACvD;AACF;AAEA,SAAS,aAAa,IAA2B;AAC/C,SAAO,IAAI,QAAQ,OAAK;AACtB,UAAM,IAAI,WAAW,GAAG,EAAE;AAC1B,MAAE,QAAQ;AAAA,EACZ,CAAC;AACH;AAQA,SAAS,qBAAqB,KAAsB;AAClD,MAAI,EAAE,eAAe,OAAQ,QAAO,OAAO,GAAG;AAC9C,QAAM,QAAkB,CAAC;AACzB,MAAI,MAAyB;AAC7B,MAAI,QAAQ;AACZ,SAAO,OAAO,QAAQ,GAAG;AACvB,UAAM,OAAQ,IAA8B;AAC5C,UAAM,KAAK,OAAO,GAAG,IAAI,KAAK,IAAI,OAAO,KAAK,IAAI,OAAO;AACzD,UAAM,IAAI,iBAAiB,QAAQ,IAAI,QAAQ;AAC/C;AAAA,EACF;AACA,SAAO,MAAM,KAAK,UAAK;AACzB;AAEA,SAAS,kBAAkB,KAAc,MAAuB;AAC9D,MAAI,MAAe;AACnB,MAAI,QAAQ;AACZ,SAAO,eAAe,SAAS,QAAQ,GAAG;AACxC,QAAK,IAA8B,SAAS,KAAM,QAAO;AACzD,UAAO,IAA4B;AACnC;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/lib/server/webhook-emitter.ts"],"sourcesContent":["/**\n * Publisher-side webhook emitter — the symmetric counterpart to PR #629's\n * receiver-side dedup. A seller / governance agent / rights agent building\n * with `@adcp/sdk` gets a one-call API that handles:\n *\n * - RFC 9421 webhook signing on every attempt (adcp#2423).\n * - A stable `idempotency_key` per logical event, reused across retries\n * (adcp#2417) — regenerating on retry is the #1 at-least-once-delivery\n * bug the runner-side conformance suite catches.\n * - Compact-separator JSON serialization once, signed once, posted once\n * (adcp#2478) — prevents the serialization-mismatch trap where a\n * signer's byte view differs from what the HTTP client writes.\n * - Retry/backoff on 5xx and 429. Terminal on 4xx and on 401 responses\n * carrying `WWW-Authenticate: Signature error=\"webhook_signature_*\"`\n * (spec says retrying a signature failure just fails identically).\n * - HMAC-SHA256 fallback for legacy buyers that registered\n * `push_notification_config.authentication.credentials` — still pinned\n * to compact separators per adcp#2478.\n *\n * Handler authors using `createAdcpServer` call `ctx.emitWebhook(...)` with\n * a `url`, `payload`, and `operation_id` — everything else is wired in.\n */\n\nimport { signWebhook, type SignerKey } from '../signing/signer';\nimport { signWebhookAsync } from '../signing/signer-async';\nimport type { SigningProvider } from '../signing/provider';\nimport type { RequestLike } from '../signing/canonicalize';\nimport { createPinAndBindFetch } from './pin-and-bind-fetch';\nimport { createHmac, randomUUID } from 'node:crypto';\n\n/**\n * Minimum pattern per adcp#2417 / core/mcp-webhook-payload.json.\n * Publisher-side check — catches `generateIdempotencyKey` overrides that\n * produce keys too short for a conformant receiver to accept.\n */\nconst IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_.:-]{16,255}$/;\n\n/** WWW-Authenticate header pattern signaling a signature-layer reject. */\nconst TERMINAL_SIGNATURE_WWW_AUTH_RE = /Signature\\s+error=\"webhook_signature_/i;\n\n/**\n * Per-(operation_id) idempotency-key store. The key MUST be stable across\n * every retry of the same logical event — that's the load-bearing invariant\n * the receiver-side dedup depends on.\n *\n * Defaults to an in-memory Map. Production publishers with multi-replica\n * emitters SHOULD inject a durable backend (the same way\n * `AsyncHandlerConfig.webhookDedup` accepts a pluggable store on the\n * receiver side).\n */\nexport interface WebhookIdempotencyKeyStore {\n get(operation_id: string): Promise<string | undefined> | string | undefined;\n /** Called only on first-seen operation_id. Subsequent emits read via get(). */\n set(operation_id: string, key: string): Promise<void> | void;\n}\n\nexport function memoryWebhookKeyStore(): WebhookIdempotencyKeyStore {\n const m = new Map<string, string>();\n return {\n get: id => m.get(id),\n set: (id, key) => {\n m.set(id, key);\n },\n };\n}\n\n/**\n * Authentication mode for a single delivery. Omit / pass `null` to use the\n * 9421 baseline. `bearer` / `hmac_sha256` drop back to legacy flows for\n * buyers who populated `push_notification_config.authentication.credentials`.\n *\n * @deprecated The `hmac_sha256` variant is deprecated. HMAC remains in the\n * AdCP spec as a legacy fallback for buyers that registered\n * `push_notification_config.authentication.credentials`, so the SDK keeps\n * supporting it — but the spec-current path is RFC 9421 webhook signatures.\n * See docs/migration-4.x-to-5.x.md#webhook-hmac-legacy-deprecation.\n */\nexport type WebhookAuthentication = { type: 'bearer'; token: string } | { type: 'hmac_sha256'; secret: string } | null;\n\nlet hmacWarningFired = false;\n\nfunction maybeWarnHmacDeprecation(suppressLegacyWarnings?: boolean): void {\n if (hmacWarningFired) return;\n if (suppressLegacyWarnings) return;\n if (process.env.ADCP_SUPPRESS_HMAC_WARNING === '1') return;\n hmacWarningFired = true;\n console.warn(\n '[adcp] Warning: webhook HMAC-SHA256 authentication is deprecated. ' +\n 'HMAC remains supported in the AdCP spec as a legacy fallback but RFC ' +\n '9421 is the spec-current path; migrate when your counterparties are ' +\n 'ready. See docs/migration-4.x-to-5.x.md#webhook-hmac-legacy-deprecation. ' +\n 'Suppress with ADCP_SUPPRESS_HMAC_WARNING=1 (env) or ' +\n 'createWebhookEmitter({ suppressLegacyWarnings: true }) (programmatic).'\n );\n}\n\nexport interface WebhookRetryOptions {\n /** Max delivery attempts (≥1). Default 5. */\n maxAttempts?: number;\n /** Initial backoff in ms. Default 1000. */\n initialDelayMs?: number;\n /** Cap per-attempt backoff. Default 60000. */\n maxDelayMs?: number;\n /** Jitter factor ∈ [0,1]: 0 = none, 0.5 = ±50%. Default 0.25. */\n jitter?: number;\n}\n\nexport interface WebhookEmitterOptions {\n /**\n * In-process JWK signing key. Its JWKS entry SHOULD carry\n * `adcp_use: \"request-signing\"` — webhooks are signed with the agent's\n * request-signing key (the deprecated `\"webhook-signing\"` value is still\n * accepted by verifiers for backward compatibility). Mutually exclusive\n * with `signerProvider` — exactly one must be provided.\n */\n signerKey?: SignerKey;\n /**\n * Async KMS-backed signing provider (GCP KMS, AWS KMS, Azure Key Vault, etc.).\n * Routes webhook signing through `signWebhookAsync` so the private key never\n * enters process memory. Mutually exclusive with `signerKey` — exactly one\n * must be provided.\n *\n * **Key purpose.** Webhooks are signed with a `request-signing` key; domain\n * separation between requests and webhooks is carried by the signature\n * `tag`, not the `adcp_use` discriminator. The same `SigningProvider` used\n * for `request_signing.provider` MAY be reused here. To isolate webhook key\n * material (so a webhook-key compromise does not extend to request signing,\n * or to rotate independently), wrap a second `request-signing`\n * `cryptoKeyVersion` published under a distinct `kid` — isolation comes from\n * the `kid`, not a distinct `adcp_use`. (The deprecated `\"webhook-signing\"`\n * purpose is still accepted by verifiers for backward compatibility.)\n */\n signerProvider?: SigningProvider;\n retries?: WebhookRetryOptions;\n idempotencyKeyStore?: WebhookIdempotencyKeyStore;\n /**\n * Override the default idempotency-key generator. Must return a value\n * matching `/^[A-Za-z0-9_.:-]{16,255}$/` — the emitter rejects anything\n * else (a malformed key would fail the receiver's schema check, which\n * would report as a conformance violation of the publisher).\n */\n generateIdempotencyKey?: () => string;\n /**\n * Override the HTTP client. Defaults to `createPinAndBindFetch()`, which\n * defeats DNS-rebinding / SSRF attacks against the buyer-supplied\n * `push_notification_config.url` (https-only; loopback, private, and cloud\n * metadata ranges denied). See `docs/guides/SIGNING-GUIDE.md` § Webhook\n * SSRF defense.\n *\n * In-process storyboard / test harnesses that deliver to a loopback\n * `http://127.0.0.1:port` receiver must opt into the relaxed policy\n * explicitly: `fetch: createPinAndBindFetch({ policy:\n * LOOPBACK_OK_WEBHOOK_SSRF_POLICY })`. Passing `globalThis.fetch` here\n * restores the unguarded pre-9.8 behavior — only do so behind your own\n * URL validation.\n */\n fetch?: typeof fetch;\n /** Default `User-Agent` header. */\n userAgent?: string;\n /** Signing tag override. Defaults to `adcp/webhook-signing/v1`. */\n tag?: string;\n /** Observability hook called BEFORE each attempt. */\n onAttempt?: (info: WebhookEmitAttempt) => void;\n /** Observability hook called AFTER each attempt completes. */\n onAttemptResult?: (info: WebhookEmitAttemptResult) => void;\n /**\n * Sleeper override. Production uses `setTimeout`; tests inject a stub to\n * skip real backoff. Takes (ms, abortSignal) and resolves when slept.\n */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Suppress the one-time `console.warn` emitted on first HMAC-SHA256\n * webhook delivery. Programmatic equivalent of\n * `ADCP_SUPPRESS_HMAC_WARNING=1`, for libraries embedded in agents where\n * setting an env var is awkward. Does not affect the `@deprecated` JSDoc\n * flag on `WebhookAuthentication` — the type-level deprecation signal\n * always shows up in IDEs.\n */\n suppressLegacyWarnings?: boolean;\n}\n\nexport interface WebhookEmitParams {\n /** Full destination URL. Typically from `push_notification_config.url`. */\n url: string;\n /** Object body. Serialized with compact separators (adcp#2478). */\n payload: Record<string, unknown>;\n /**\n * Stable logical event id. Two emits with the same operation_id reuse\n * the same `idempotency_key` — this is the cross-attempt AND\n * cross-process invariant the receiver dedups on.\n */\n operation_id: string;\n /**\n * Per-emit override of the delivery's authentication mode. Omit for the\n * 9421 default.\n */\n authentication?: WebhookAuthentication;\n /** Per-emit retries override. */\n retries?: WebhookRetryOptions;\n}\n\nexport interface WebhookEmitAttempt {\n operation_id: string;\n idempotency_key: string;\n attempt: number;\n url: string;\n}\n\nexport interface WebhookEmitAttemptResult extends WebhookEmitAttempt {\n status?: number;\n durationMs: number;\n error?: string;\n willRetry: boolean;\n}\n\nexport interface WebhookEmitResult {\n operation_id: string;\n idempotency_key: string;\n attempts: number;\n delivered: boolean;\n final_status?: number;\n /**\n * Per-attempt error messages (transport / signer / network failures).\n *\n * **Logging caution:** when a `signerProvider` rejection bubbles into\n * this array, the message text comes from the adapter and may include\n * infra-flavored detail (KMS resource names, IAM principals, project\n * IDs). Mirrors the same caution flagged on `SigningProvider.fingerprint`.\n * If you pipe `errors[]` into shared logs / observability pipelines,\n * sanitize or redact at your boundary — adapter messages aren't\n * guaranteed to be operator-safe.\n */\n errors: string[];\n}\n\nexport interface WebhookEmitter {\n emit(params: WebhookEmitParams): Promise<WebhookEmitResult>;\n}\n\n// ────────────────────────────────────────────────────────────\n// Factory\n// ────────────────────────────────────────────────────────────\n\nexport function createWebhookEmitter(options: WebhookEmitterOptions): WebhookEmitter {\n if (options.signerKey && options.signerProvider) {\n throw new TypeError('createWebhookEmitter: provide exactly one of signerKey or signerProvider, not both');\n }\n if (!options.signerKey && !options.signerProvider) {\n throw new TypeError('createWebhookEmitter: one of signerKey or signerProvider is required');\n }\n const store = options.idempotencyKeyStore ?? memoryWebhookKeyStore();\n const generateKey = options.generateIdempotencyKey ?? defaultGenerateIdempotencyKey;\n const fetchImpl = options.fetch ?? createPinAndBindFetch();\n const sleep = options.sleep ?? defaultSleep;\n\n return {\n async emit(params: WebhookEmitParams): Promise<WebhookEmitResult> {\n const retries = resolveRetries(params.retries ?? options.retries);\n const idempotency_key = await resolveIdempotencyKey(store, params.operation_id, generateKey);\n\n // Serialize ONCE with compact separators — the same bytes feed both\n // the content-digest input and the HTTP body on every attempt. This\n // is the load-bearing rule from adcp#2478.\n const bodyPayload = { ...params.payload, idempotency_key };\n const bodyBytes = JSON.stringify(bodyPayload);\n\n const errors: string[] = [];\n let lastStatus: number | undefined;\n\n for (let attempt = 1; attempt <= retries.maxAttempts; attempt++) {\n const attemptInfo: WebhookEmitAttempt = {\n operation_id: params.operation_id,\n idempotency_key,\n attempt,\n url: params.url,\n };\n options.onAttempt?.(attemptInfo);\n\n const started = Date.now();\n let status: number | undefined;\n let error: string | undefined;\n let terminal = false;\n\n try {\n const response = await deliverOnce({\n url: params.url,\n bodyBytes,\n signerKey: options.signerKey,\n signerProvider: options.signerProvider,\n authentication: params.authentication ?? null,\n tag: options.tag,\n userAgent: options.userAgent,\n fetch: fetchImpl,\n suppressLegacyWarnings: options.suppressLegacyWarnings,\n });\n status = response.status;\n lastStatus = status;\n\n if (status >= 200 && status < 300) {\n const durationMs = Date.now() - started;\n options.onAttemptResult?.({ ...attemptInfo, status, durationMs, willRetry: false });\n return {\n operation_id: params.operation_id,\n idempotency_key,\n attempts: attempt,\n delivered: true,\n final_status: status,\n errors,\n };\n }\n\n terminal = isTerminalStatus(status, response.wwwAuthenticate);\n error =\n status >= 300 && status < 400\n ? `HTTP ${status} redirect${response.location ? ` to ${response.location}` : ''} — redirects are ` +\n `never followed for signed webhook delivery, because the signature covers @target-uri and ` +\n `would not verify at the redirect target. Re-register the webhook with the final URL.`\n : `HTTP ${status}${response.wwwAuthenticate ? ` (${response.wwwAuthenticate})` : ''}`;\n } catch (err) {\n error = formatTransportError(err);\n // Network / transport errors are retryable — the delivery didn't\n // reach the receiver, so no risk of double-processing.\n // Pin-and-bind SSRF blocks are themselves terminal: the URL\n // (or its DNS resolution) violates policy and won't change on retry.\n if (errorContainsCode(err, 'EADCP_SSRF_BLOCKED')) {\n terminal = true;\n }\n }\n\n if (error) errors.push(`attempt ${attempt}: ${error}`);\n\n const willRetry = !terminal && attempt < retries.maxAttempts;\n options.onAttemptResult?.({\n ...attemptInfo,\n ...(status !== undefined && { status }),\n durationMs: Date.now() - started,\n ...(error !== undefined && { error }),\n willRetry,\n });\n\n if (!willRetry) break;\n\n await sleep(backoffDelay(attempt, retries));\n }\n\n return {\n operation_id: params.operation_id,\n idempotency_key,\n attempts: errors.length,\n delivered: false,\n ...(lastStatus !== undefined && { final_status: lastStatus }),\n errors,\n };\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Delivery primitives\n// ────────────────────────────────────────────────────────────\n\ninterface DeliveryResponse {\n status: number;\n wwwAuthenticate?: string;\n /** `Location` on a 3xx, so the diagnostic can name the redirect target. */\n location?: string;\n}\n\nasync function deliverOnce(args: {\n url: string;\n bodyBytes: string;\n signerKey?: SignerKey;\n signerProvider?: SigningProvider;\n authentication: WebhookAuthentication;\n tag?: string;\n userAgent?: string;\n fetch: typeof fetch;\n suppressLegacyWarnings?: boolean;\n}): Promise<DeliveryResponse> {\n const headers = await buildHeaders(args);\n const response = await args.fetch(args.url, {\n method: 'POST',\n headers,\n body: args.bodyBytes,\n });\n return {\n status: response.status,\n ...(response.headers.get('www-authenticate') && { wwwAuthenticate: response.headers.get('www-authenticate')! }),\n ...(response.headers.get('location') && { location: response.headers.get('location')! }),\n };\n}\n\nasync function buildHeaders(args: {\n url: string;\n bodyBytes: string;\n signerKey?: SignerKey;\n signerProvider?: SigningProvider;\n authentication: WebhookAuthentication;\n tag?: string;\n userAgent?: string;\n suppressLegacyWarnings?: boolean;\n}): Promise<Record<string, string>> {\n const baseHeaders: Record<string, string> = {\n 'content-type': 'application/json',\n };\n if (args.userAgent) baseHeaders['user-agent'] = args.userAgent;\n\n // Legacy HMAC-SHA256 path. Matches docs/building/implementation/webhooks.mdx\n // §3.0 legacy section: X-ADCP-Signature + X-ADCP-Timestamp over\n // `${ts}.${raw_body_bytes}`.\n if (args.authentication?.type === 'hmac_sha256') {\n maybeWarnHmacDeprecation(args.suppressLegacyWarnings);\n const ts = Math.floor(Date.now() / 1000).toString();\n const hmac = createHmac('sha256', args.authentication.secret);\n hmac.update(`${ts}.${args.bodyBytes}`, 'utf8');\n return {\n ...baseHeaders,\n 'x-adcp-timestamp': ts,\n 'x-adcp-signature': `sha256=${hmac.digest('hex')}`,\n };\n }\n\n // Bearer fallback — the legacy path for buyers that registered an API\n // key in push_notification_config.authentication. No body signing —\n // just a header. Not recommended; strictly for interop with 2.x buyers.\n if (args.authentication?.type === 'bearer') {\n return { ...baseHeaders, authorization: `Bearer ${args.authentication.token}` };\n }\n\n // Default: 9421 webhook signing. Fresh nonce + fresh created/expires per\n // attempt, but the `idempotency_key` inside the body stays stable — the\n // signature covers the body bytes, which include the key; multiple\n // retries of the same logical event produce different signatures over\n // the same body, which is exactly what the receiver expects.\n const request: RequestLike = {\n method: 'POST',\n url: args.url,\n headers: baseHeaders,\n body: args.bodyBytes,\n };\n const signOpts = args.tag !== undefined ? { tag: args.tag } : {};\n const signed = args.signerProvider\n ? await signWebhookAsync(request, args.signerProvider, signOpts)\n : signWebhook(request, args.signerKey!, signOpts);\n return { ...baseHeaders, ...signed.headers };\n}\n\n// ────────────────────────────────────────────────────────────\n// Helpers\n// ────────────────────────────────────────────────────────────\n\nasync function resolveIdempotencyKey(\n store: WebhookIdempotencyKeyStore,\n operation_id: string,\n generate: () => string\n): Promise<string> {\n const existing = await store.get(operation_id);\n if (existing) {\n if (!IDEMPOTENCY_KEY_PATTERN.test(existing)) {\n throw new Error(\n `idempotency-key store returned \"${existing}\" for operation_id \"${operation_id}\"; ` +\n `does not match required pattern ${IDEMPOTENCY_KEY_PATTERN.source}`\n );\n }\n return existing;\n }\n const fresh = generate();\n if (!IDEMPOTENCY_KEY_PATTERN.test(fresh)) {\n throw new Error(\n `generateIdempotencyKey produced \"${fresh}\" for operation_id \"${operation_id}\"; ` +\n `must match ${IDEMPOTENCY_KEY_PATTERN.source}`\n );\n }\n await store.set(operation_id, fresh);\n return fresh;\n}\n\n/**\n * Default key generator — `evt_` prefix + a base64url 18-byte random.\n * Length 27 (comfortably within 16–255), only base64url-safe characters,\n * obviously-webhook-scoped prefix for log grepping.\n */\nfunction defaultGenerateIdempotencyKey(): string {\n const uuid = randomUUID().replace(/-/g, '');\n return `evt_${uuid.slice(0, 24)}`;\n}\n\nfunction isTerminalStatus(status: number, wwwAuthenticate?: string): boolean {\n if (status === 429) return false;\n if (status >= 500) return false;\n // A redirect is a configuration error, not a transient one — retrying it\n // produces the same redirect. `createPinAndBindFetch` deliberately does not\n // follow redirects (the signature covers `@target-uri`, so a replayed hop\n // would not verify at the target anyway), so a 3xx means the registered URL\n // is not the final one.\n if (status >= 300 && status < 400) return true;\n // 401 with a signature-layer reject is terminal per adcp#2423 —\n // retrying a signature failure produces identical bytes and identical\n // rejection. Non-signature 401s (opaque auth failures) are also\n // terminal; there's nothing the publisher can do by retrying.\n if (status === 401 && wwwAuthenticate && TERMINAL_SIGNATURE_WWW_AUTH_RE.test(wwwAuthenticate)) return true;\n if (status >= 400 && status < 500) return true;\n return false;\n}\n\nfunction backoffDelay(attempt: number, retries: Required<WebhookRetryOptions>): number {\n const base = Math.min(retries.initialDelayMs * Math.pow(2, attempt - 1), retries.maxDelayMs);\n if (retries.jitter <= 0) return base;\n const jitterWindow = base * retries.jitter;\n const offset = Math.random() * jitterWindow * 2 - jitterWindow;\n return Math.max(0, Math.floor(base + offset));\n}\n\nfunction resolveRetries(opts?: WebhookRetryOptions): Required<WebhookRetryOptions> {\n return {\n maxAttempts: Math.max(1, opts?.maxAttempts ?? 5),\n initialDelayMs: Math.max(0, opts?.initialDelayMs ?? 1000),\n maxDelayMs: Math.max(0, opts?.maxDelayMs ?? 60_000),\n jitter: Math.max(0, Math.min(1, opts?.jitter ?? 0.25)),\n };\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise(r => {\n const t = setTimeout(r, ms);\n t.unref?.();\n });\n}\n\n/**\n * Format a transport-layer error for `result.errors[]`. Walks `cause` chains\n * (undici wraps the connector failure under a generic \"fetch failed\") so\n * operators see the actual rule that fired (e.g. SSRF policy block) instead\n * of an opaque outer message.\n */\nfunction formatTransportError(err: unknown): string {\n if (!(err instanceof Error)) return String(err);\n const parts: string[] = [];\n let cur: Error | undefined = err;\n let depth = 0;\n while (cur && depth < 5) {\n const code = (cur as NodeJS.ErrnoException).code;\n parts.push(code ? `${code}: ${cur.message}` : cur.message);\n cur = cur.cause instanceof Error ? cur.cause : undefined;\n depth++;\n }\n return parts.join(' — ');\n}\n\nfunction errorContainsCode(err: unknown, code: string): boolean {\n let cur: unknown = err;\n let depth = 0;\n while (cur instanceof Error && depth < 5) {\n if ((cur as NodeJS.ErrnoException).code === code) return true;\n cur = (cur as { cause?: unknown }).cause;\n depth++;\n }\n return false;\n}\n"],"mappings":"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","names":[]}
@@ -17,7 +17,8 @@ export interface ExpressLike {
17
17
  originalUrl?: string;
18
18
  url: string;
19
19
  headers: Record<string, string | string[] | undefined>;
20
- rawBody?: string;
20
+ /** Raw request bytes captured before any body parser. String or Buffer. */
21
+ rawBody?: string | Buffer;
21
22
  body?: unknown;
22
23
  protocol?: string;
23
24
  get?(header: string): string | undefined;
@@ -17,7 +17,8 @@ export interface ExpressLike {
17
17
  originalUrl?: string;
18
18
  url: string;
19
19
  headers: Record<string, string | string[] | undefined>;
20
- rawBody?: string;
20
+ /** Raw request bytes captured before any body parser. String or Buffer. */
21
+ rawBody?: string | Buffer;
21
22
  body?: unknown;
22
23
  protocol?: string;
23
24
  get?(header: string): string | undefined;