@adcp/sdk 14.0.0-beta.14 → 14.0.0-beta.15

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 (127) hide show
  1. package/dist/lib/auth/oauth/authorization-required.d.mts +25 -11
  2. package/dist/lib/auth/oauth/authorization-required.d.ts +25 -11
  3. package/dist/lib/auth/oauth/authorization-required.js +18 -4
  4. package/dist/lib/auth/oauth/authorization-required.mjs +17 -4
  5. package/dist/lib/auth/oauth/index.d.mts +1 -1
  6. package/dist/lib/auth/oauth/index.d.ts +1 -1
  7. package/dist/lib/auth/oauth/index.js +2 -0
  8. package/dist/lib/auth/oauth/index.mjs +2 -0
  9. package/dist/lib/auth/oauth/resource-url.d.mts +8 -0
  10. package/dist/lib/auth/oauth/resource-url.d.ts +8 -0
  11. package/dist/lib/auth/oauth/resource-url.js +42 -0
  12. package/dist/lib/auth/oauth/resource-url.mjs +40 -0
  13. package/dist/lib/core/ConversationTypes.d.mts +10 -0
  14. package/dist/lib/core/ConversationTypes.d.ts +10 -0
  15. package/dist/lib/core/SingleAgentClient.d.mts +5 -2
  16. package/dist/lib/core/SingleAgentClient.d.ts +5 -2
  17. package/dist/lib/core/SingleAgentClient.js +133 -28
  18. package/dist/lib/core/SingleAgentClient.mjs +138 -29
  19. package/dist/lib/core/TaskExecutor.d.mts +1 -0
  20. package/dist/lib/core/TaskExecutor.d.ts +1 -0
  21. package/dist/lib/core/TaskExecutor.js +7 -1
  22. package/dist/lib/core/TaskExecutor.mjs +7 -1
  23. package/dist/lib/core/webhook-registration.d.mts +8 -1
  24. package/dist/lib/core/webhook-registration.d.ts +8 -1
  25. package/dist/lib/core/webhook-registration.js +65 -5
  26. package/dist/lib/core/webhook-registration.mjs +61 -4
  27. package/dist/lib/errors/index.d.mts +9 -0
  28. package/dist/lib/errors/index.d.ts +9 -0
  29. package/dist/lib/errors/index.js +33 -0
  30. package/dist/lib/errors/index.mjs +32 -0
  31. package/dist/lib/index.d.mts +3 -2
  32. package/dist/lib/index.d.ts +3 -2
  33. package/dist/lib/index.js +4 -0
  34. package/dist/lib/index.mjs +4 -0
  35. package/dist/lib/media-buy/compatibility.js +14 -2
  36. package/dist/lib/media-buy/compatibility.mjs +14 -2
  37. package/dist/lib/protocols/index.js +23 -7
  38. package/dist/lib/protocols/index.mjs +24 -8
  39. package/dist/lib/protocols/mcp.js +8 -6
  40. package/dist/lib/protocols/mcp.mjs +9 -7
  41. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  42. package/dist/lib/server/decisioning/async-outcome.d.mts +11 -0
  43. package/dist/lib/server/decisioning/async-outcome.d.ts +11 -0
  44. package/dist/lib/server/decisioning/context.d.mts +11 -1
  45. package/dist/lib/server/decisioning/context.d.ts +11 -1
  46. package/dist/lib/server/decisioning/index.d.mts +3 -2
  47. package/dist/lib/server/decisioning/index.d.ts +3 -2
  48. package/dist/lib/server/decisioning/index.js +9 -0
  49. package/dist/lib/server/decisioning/index.mjs +10 -0
  50. package/dist/lib/server/decisioning/runtime/from-platform.d.mts +5 -2
  51. package/dist/lib/server/decisioning/runtime/from-platform.d.ts +5 -2
  52. package/dist/lib/server/decisioning/runtime/from-platform.js +40 -8
  53. package/dist/lib/server/decisioning/runtime/from-platform.mjs +40 -8
  54. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.mts +8 -1
  55. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +8 -1
  56. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +10 -1
  57. package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs +9 -1
  58. package/dist/lib/server/decisioning/runtime/postgres-task-settlement.d.mts +79 -0
  59. package/dist/lib/server/decisioning/runtime/postgres-task-settlement.d.ts +79 -0
  60. package/dist/lib/server/decisioning/runtime/postgres-task-settlement.js +391 -0
  61. package/dist/lib/server/decisioning/runtime/postgres-task-settlement.mjs +369 -0
  62. package/dist/lib/server/decisioning/runtime/task-registry.d.mts +2 -0
  63. package/dist/lib/server/decisioning/runtime/task-registry.d.ts +2 -0
  64. package/dist/lib/server/decisioning/runtime/task-registry.js +2 -1
  65. package/dist/lib/server/decisioning/runtime/task-registry.mjs +2 -1
  66. package/dist/lib/server/decisioning/runtime/to-context.js +15 -11
  67. package/dist/lib/server/decisioning/runtime/to-context.mjs +15 -11
  68. package/dist/lib/server/index.d.mts +2 -2
  69. package/dist/lib/server/index.d.ts +2 -2
  70. package/dist/lib/server/index.js +4 -0
  71. package/dist/lib/server/index.mjs +4 -0
  72. package/dist/lib/server/webhook-delivery/index.d.mts +2 -2
  73. package/dist/lib/server/webhook-delivery/index.d.ts +2 -2
  74. package/dist/lib/server/webhook-delivery/index.js +4 -0
  75. package/dist/lib/server/webhook-delivery/index.mjs +5 -1
  76. package/dist/lib/server/webhook-delivery/pg.d.mts +3 -1
  77. package/dist/lib/server/webhook-delivery/pg.d.ts +3 -1
  78. package/dist/lib/server/webhook-delivery/pg.js +25 -2
  79. package/dist/lib/server/webhook-delivery/pg.mjs +25 -2
  80. package/dist/lib/server/webhook-delivery/recovery.d.mts +44 -0
  81. package/dist/lib/server/webhook-delivery/recovery.d.ts +44 -0
  82. package/dist/lib/server/webhook-delivery/recovery.js +146 -43
  83. package/dist/lib/server/webhook-delivery/recovery.mjs +144 -43
  84. package/dist/lib/signing/agent-resolver/index.d.mts +1 -1
  85. package/dist/lib/signing/agent-resolver/index.d.ts +1 -1
  86. package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +9 -0
  87. package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +9 -0
  88. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.mts +4 -1
  89. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.ts +4 -1
  90. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.js +17 -4
  91. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.mjs +17 -4
  92. package/dist/lib/signing/jwks.d.mts +7 -0
  93. package/dist/lib/signing/jwks.d.ts +7 -0
  94. package/dist/lib/signing/server.d.mts +2 -2
  95. package/dist/lib/signing/server.d.ts +2 -2
  96. package/dist/lib/signing/webhook-verifier.d.mts +6 -0
  97. package/dist/lib/signing/webhook-verifier.d.ts +6 -0
  98. package/dist/lib/signing/webhook-verifier.js +38 -4
  99. package/dist/lib/signing/webhook-verifier.mjs +38 -4
  100. package/dist/lib/testing/client.d.mts +1 -0
  101. package/dist/lib/testing/client.d.ts +1 -0
  102. package/dist/lib/testing/client.js +6 -2
  103. package/dist/lib/testing/client.mjs +6 -2
  104. package/dist/lib/testing/compliance/comply.d.mts +2 -2
  105. package/dist/lib/testing/compliance/comply.d.ts +2 -2
  106. package/dist/lib/testing/compliance/comply.js +41 -31
  107. package/dist/lib/testing/compliance/comply.mjs +44 -21
  108. package/dist/lib/testing/storyboard/oauth-metadata-graph/grader.d.mts +1 -7
  109. package/dist/lib/testing/storyboard/oauth-metadata-graph/grader.d.ts +1 -7
  110. package/dist/lib/testing/storyboard/oauth-metadata-graph/grader.js +6 -42
  111. package/dist/lib/testing/storyboard/oauth-metadata-graph/grader.mjs +10 -40
  112. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  113. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  114. package/dist/lib/utils/adcp-version-config.d.mts +2 -0
  115. package/dist/lib/utils/adcp-version-config.d.ts +2 -0
  116. package/dist/lib/utils/adcp-version-config.js +5 -0
  117. package/dist/lib/utils/adcp-version-config.mjs +4 -0
  118. package/dist/lib/version.d.mts +3 -3
  119. package/dist/lib/version.d.ts +3 -3
  120. package/dist/lib/version.js +3 -3
  121. package/dist/lib/version.mjs +3 -3
  122. package/docs/llms.txt +3 -1
  123. package/docs/migration-12-to-14.md +6 -5
  124. package/docs/migration-13-to-14.md +48 -7
  125. package/docs/migration-task-registry-scoping.md +164 -11
  126. package/package.json +1 -1
  127. package/skills/build-decisioning-platform/advanced/REFERENCE.md +1 -1
@@ -1,5 +1,13 @@
1
1
  import { isAlwaysBlocked, isPrivateIp } from "../../../net/address-guards.mjs";
2
2
  import { ssrfSafeFetch, SsrfRefusedError } from "../../../net/ssrf-fetch.mjs";
3
+ import {
4
+ buildProtectedResourceMetadataUrl,
5
+ normalizeOAuthResourceForComparison
6
+ } from "../../../auth/oauth/resource-url.mjs";
7
+ import {
8
+ buildProtectedResourceMetadataUrl as buildProtectedResourceMetadataUrl2,
9
+ normalizeOAuthResourceForComparison as normalizeOAuthResourceForComparison2
10
+ } from "../../../auth/oauth/resource-url.mjs";
3
11
  const LIMITS = Object.freeze({
4
12
  authorizationServers: 16,
5
13
  uniqueUrls: 64,
@@ -24,40 +32,6 @@ const DIAGNOSTIC_ORDER = Object.freeze([
24
32
  "oauth_endpoint_unreachable",
25
33
  "oauth_jwks_unavailable"
26
34
  ]);
27
- function normalizeOAuthResourceForComparison(value) {
28
- const match = /^([A-Za-z][A-Za-z\d+.-]*):\/\/([^/?#]*)([\s\S]*)$/.exec(value);
29
- if (!match) return value;
30
- const scheme = match[1].toLowerCase();
31
- const authority = match[2];
32
- const remainder = match[3];
33
- const at = authority.lastIndexOf("@");
34
- const userinfo = at >= 0 ? authority.slice(0, at + 1) : "";
35
- const hostAndPort = at >= 0 ? authority.slice(at + 1) : authority;
36
- let host;
37
- let port = "";
38
- if (hostAndPort.startsWith("[")) {
39
- const bracket = hostAndPort.indexOf("]");
40
- if (bracket < 0) return value;
41
- host = hostAndPort.slice(0, bracket + 1).toLowerCase();
42
- const suffix = hostAndPort.slice(bracket + 1);
43
- if (suffix && !/^:\d+$/.test(suffix)) return value;
44
- port = suffix.slice(1);
45
- } else {
46
- const colon = hostAndPort.lastIndexOf(":");
47
- if (colon >= 0) {
48
- const suffix = hostAndPort.slice(colon + 1);
49
- if (!/^\d+$/.test(suffix)) return value;
50
- host = hostAndPort.slice(0, colon).toLowerCase();
51
- port = suffix;
52
- } else {
53
- host = hostAndPort.toLowerCase();
54
- }
55
- }
56
- if (!host) return value;
57
- const defaultPort = scheme === "https" ? "443" : scheme === "http" ? "80" : void 0;
58
- const renderedPort = port && port !== defaultPort ? `:${port}` : "";
59
- return `${scheme}://${userinfo}${host}${renderedPort}${remainder}`;
60
- }
61
35
  function redactOAuthUrlForOutput(value) {
62
36
  try {
63
37
  const u = new URL(value);
@@ -75,10 +49,6 @@ function redactOAuthUrlForOutput(value) {
75
49
  function redactOAuthUrlsInText(value) {
76
50
  return value.replace(/https?:\/\/[^\s"'<>]+/gi, (url) => redactOAuthUrlForOutput(url));
77
51
  }
78
- function buildProtectedResourceMetadataUrl(agentUrl) {
79
- const u = new URL(agentUrl);
80
- return `${u.origin}/.well-known/oauth-protected-resource${u.pathname}`;
81
- }
82
52
  function buildAuthorizationServerMetadataUrl(issuer) {
83
53
  const u = new URL(issuer);
84
54
  const issuerPath = u.pathname === "/" ? "" : u.pathname;
@@ -103,7 +73,7 @@ async function gradeOAuthMetadataGraphWithTransport(agentUrl, options = {}) {
103
73
  const findings = [];
104
74
  let protectedResourceUrl;
105
75
  try {
106
- protectedResourceUrl = buildProtectedResourceMetadataUrl(agentUrl);
76
+ protectedResourceUrl = buildProtectedResourceMetadataUrl2(agentUrl);
107
77
  } catch {
108
78
  protectedResourceUrl = redactOAuthUrlForOutput(agentUrl);
109
79
  findings.push(finding("oauth_protected_resource_metadata_invalid", "Agent URL is not an absolute URL."));
@@ -148,7 +118,7 @@ async function gradeOAuthMetadataGraphWithTransport(agentUrl, options = {}) {
148
118
  "resource"
149
119
  )
150
120
  );
151
- } else if (normalizeOAuthResourceForComparison(resource) !== normalizeOAuthResourceForComparison(agentUrl)) {
121
+ } else if (normalizeOAuthResourceForComparison2(resource) !== normalizeOAuthResourceForComparison2(agentUrl)) {
152
122
  findings.push(
153
123
  finding(
154
124
  "oauth_resource_mismatch",
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
197
197
  */
198
198
  export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
199
199
  /** Capability semver emitted in run summaries and used for advisory expiry. */
200
- export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-beta.14";
200
+ export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-beta.15";
201
201
  /** True when a failed validation contributes to the owning step's grade. */
202
202
  export declare function validationFailsStep(result: ValidationResult): boolean;
203
203
  /**
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
197
197
  */
198
198
  export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
199
199
  /** Capability semver emitted in run summaries and used for advisory expiry. */
200
- export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-beta.14";
200
+ export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-beta.15";
201
201
  /** True when a failed validation contributes to the owning step's grade. */
202
202
  export declare function validationFailsStep(result: ValidationResult): boolean;
203
203
  /**
@@ -1,5 +1,7 @@
1
1
  /** Compare full-semver and release-precision AdCP identifiers safely. */
2
2
  export declare function isAdcpVersionAtLeast(version: string | undefined, minimum: string): boolean;
3
+ /** Whether an AdCP identifier is a parseable full or release-precision semver. */
4
+ export declare function isValidAdcpVersion(version: string | undefined): version is string;
3
5
  /**
4
6
  * Resolve and validate a configured `adcpVersion`. Returns the value to store
5
7
  * on the instance — either the caller's pin or the SDK default.
@@ -1,5 +1,7 @@
1
1
  /** Compare full-semver and release-precision AdCP identifiers safely. */
2
2
  export declare function isAdcpVersionAtLeast(version: string | undefined, minimum: string): boolean;
3
+ /** Whether an AdCP identifier is a parseable full or release-precision semver. */
4
+ export declare function isValidAdcpVersion(version: string | undefined): version is string;
3
5
  /**
4
6
  * Resolve and validate a configured `adcpVersion`. Returns the value to store
5
7
  * on the instance — either the caller's pin or the SDK default.
@@ -24,6 +24,7 @@ __export(adcp_version_config_exports, {
24
24
  isMovingAdcpPrereleaseFamilyAlias: () => isMovingAdcpPrereleaseFamilyAlias,
25
25
  isPre31AdcpVersion: () => isPre31AdcpVersion,
26
26
  isPre32AdcpVersion: () => isPre32AdcpVersion,
27
+ isValidAdcpVersion: () => isValidAdcpVersion,
27
28
  listBundledAdcpVersions: () => listBundledAdcpVersions,
28
29
  omit31BrandFields: () => omit31BrandFields,
29
30
  resolveAdcpVersion: () => resolveAdcpVersion,
@@ -47,6 +48,9 @@ function isAdcpVersionAtLeast(version, minimum) {
47
48
  const comparableMinimum = comparableAdcpSemver(minimum);
48
49
  return comparable !== void 0 && comparableMinimum !== void 0 && (0, import_semver.gte)(comparable, comparableMinimum);
49
50
  }
51
+ function isValidAdcpVersion(version) {
52
+ return version !== void 0 && comparableAdcpSemver(version) !== void 0;
53
+ }
50
54
  function resolveAdcpVersion(adcpVersion) {
51
55
  if (adcpVersion === void 0) return import_version.ADCP_VERSION;
52
56
  const currentReleasePrecision = (0, import_schema_loader.toReleasePrecisionWire)(import_version.ADCP_VERSION);
@@ -174,6 +178,7 @@ function listBundledAdcpVersions() {
174
178
  isMovingAdcpPrereleaseFamilyAlias,
175
179
  isPre31AdcpVersion,
176
180
  isPre32AdcpVersion,
181
+ isValidAdcpVersion,
177
182
  listBundledAdcpVersions,
178
183
  omit31BrandFields,
179
184
  resolveAdcpVersion,
@@ -14,6 +14,9 @@ function isAdcpVersionAtLeast(version, minimum) {
14
14
  const comparableMinimum = comparableAdcpSemver(minimum);
15
15
  return comparable !== void 0 && comparableMinimum !== void 0 && semverGte(comparable, comparableMinimum);
16
16
  }
17
+ function isValidAdcpVersion(version) {
18
+ return version !== void 0 && comparableAdcpSemver(version) !== void 0;
19
+ }
17
20
  function resolveAdcpVersion(adcpVersion) {
18
21
  if (adcpVersion === void 0) return ADCP_VERSION;
19
22
  const currentReleasePrecision = toReleasePrecisionWire(ADCP_VERSION);
@@ -140,6 +143,7 @@ export {
140
143
  isMovingAdcpPrereleaseFamilyAlias,
141
144
  isPre31AdcpVersion,
142
145
  isPre32AdcpVersion,
146
+ isValidAdcpVersion,
143
147
  listBundledAdcpVersions,
144
148
  omit31BrandFields,
145
149
  resolveAdcpVersion,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AdCP SDK library version
3
3
  */
4
- export declare const LIBRARY_VERSION = "14.0.0-beta.14";
4
+ export declare const LIBRARY_VERSION = "14.0.0-beta.15";
5
5
  /**
6
6
  * AdCP specification version this library is built for
7
7
  */
@@ -33,10 +33,10 @@ export type AdcpVersion = (typeof COMPATIBLE_ADCP_VERSIONS)[number];
33
33
  * Full version information
34
34
  */
35
35
  export declare const VERSION_INFO: {
36
- readonly library: "14.0.0-beta.14";
36
+ readonly library: "14.0.0-beta.15";
37
37
  readonly adcp: "3.2.0-beta.8";
38
38
  readonly compatibleVersions: readonly ["v2.5", "v2.6", "v3", "3.0.0-beta.1", "3.0-beta.1", "3.0-beta", "3.0.0-beta.3", "3.0-beta.3", "3.0.0", "3.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6", "3.0.7", "3.0.8", "3.0.9", "3.0.10", "3.0.11", "3.0.12", "3.0.13", "3.0.14", "3.0.15", "3.0.16", "3.0.17", "3.0.18", "3.0.19", "3.0.20", "3.0.21", "3.0.22", "3.0.23", "3.0.24", "3.0.25", "3.1.0", "3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6", "3.1.7", "3.1.8", "3.1.9", "3.1.10", "3.1.11", "3.1.12", "3.1.13", "3.1.14", "3.1.15", "3.1.16", "3.1.17", "3.1.18", "3.2.0-beta.8", "3.2-beta.8"];
39
- readonly generatedAt: "2026-08-28T11:11:52.288Z";
39
+ readonly generatedAt: "2026-08-28T20:29:38.378Z";
40
40
  };
41
41
  /**
42
42
  * Get the AdCP specification version this library is built for
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AdCP SDK library version
3
3
  */
4
- export declare const LIBRARY_VERSION = "14.0.0-beta.14";
4
+ export declare const LIBRARY_VERSION = "14.0.0-beta.15";
5
5
  /**
6
6
  * AdCP specification version this library is built for
7
7
  */
@@ -33,10 +33,10 @@ export type AdcpVersion = (typeof COMPATIBLE_ADCP_VERSIONS)[number];
33
33
  * Full version information
34
34
  */
35
35
  export declare const VERSION_INFO: {
36
- readonly library: "14.0.0-beta.14";
36
+ readonly library: "14.0.0-beta.15";
37
37
  readonly adcp: "3.2.0-beta.8";
38
38
  readonly compatibleVersions: readonly ["v2.5", "v2.6", "v3", "3.0.0-beta.1", "3.0-beta.1", "3.0-beta", "3.0.0-beta.3", "3.0-beta.3", "3.0.0", "3.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6", "3.0.7", "3.0.8", "3.0.9", "3.0.10", "3.0.11", "3.0.12", "3.0.13", "3.0.14", "3.0.15", "3.0.16", "3.0.17", "3.0.18", "3.0.19", "3.0.20", "3.0.21", "3.0.22", "3.0.23", "3.0.24", "3.0.25", "3.1.0", "3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6", "3.1.7", "3.1.8", "3.1.9", "3.1.10", "3.1.11", "3.1.12", "3.1.13", "3.1.14", "3.1.15", "3.1.16", "3.1.17", "3.1.18", "3.2.0-beta.8", "3.2-beta.8"];
39
- readonly generatedAt: "2026-08-28T11:11:52.288Z";
39
+ readonly generatedAt: "2026-08-28T20:29:38.378Z";
40
40
  };
41
41
  /**
42
42
  * Get the AdCP specification version this library is built for
@@ -31,7 +31,7 @@ __export(version_exports, {
31
31
  toReleasePrecisionVersion: () => toReleasePrecisionVersion
32
32
  });
33
33
  module.exports = __toCommonJS(version_exports);
34
- const LIBRARY_VERSION = "14.0.0-beta.14";
34
+ const LIBRARY_VERSION = "14.0.0-beta.15";
35
35
  const ADCP_VERSION = "3.2.0-beta.8";
36
36
  const ADCP_MAJOR_VERSION = 3;
37
37
  const COMPATIBLE_ADCP_VERSIONS = [
@@ -94,10 +94,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
94
94
  "3.2-beta.8"
95
95
  ];
96
96
  const VERSION_INFO = {
97
- library: "14.0.0-beta.14",
97
+ library: "14.0.0-beta.15",
98
98
  adcp: "3.2.0-beta.8",
99
99
  compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
100
- generatedAt: "2026-08-28T11:11:52.288Z"
100
+ generatedAt: "2026-08-28T20:29:38.378Z"
101
101
  };
102
102
  function getAdcpVersion() {
103
103
  return ADCP_VERSION;
@@ -1,4 +1,4 @@
1
- const LIBRARY_VERSION = "14.0.0-beta.14";
1
+ const LIBRARY_VERSION = "14.0.0-beta.15";
2
2
  const ADCP_VERSION = "3.2.0-beta.8";
3
3
  const ADCP_MAJOR_VERSION = 3;
4
4
  const COMPATIBLE_ADCP_VERSIONS = [
@@ -61,10 +61,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
61
61
  "3.2-beta.8"
62
62
  ];
63
63
  const VERSION_INFO = {
64
- library: "14.0.0-beta.14",
64
+ library: "14.0.0-beta.15",
65
65
  adcp: "3.2.0-beta.8",
66
66
  compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
67
- generatedAt: "2026-08-28T11:11:52.288Z"
67
+ generatedAt: "2026-08-28T20:29:38.378Z"
68
68
  };
69
69
  function getAdcpVersion() {
70
70
  return ADCP_VERSION;
package/docs/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # Ad Context Protocol (AdCP)
2
2
 
3
3
  > Generated at: 2026-08-28
4
- > Library: @adcp/sdk v14.0.0-beta.14
4
+ > Library: @adcp/sdk v14.0.0-beta.15
5
5
  > AdCP major version: 3
6
6
  > Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt
7
7
  > Note: the `Library` stamp reflects the package.json version at doc-generation time. The narrative below describes the surface that lands on the next-published minor — including any 6.7 helpers documented here ahead of the release tag.
@@ -1848,8 +1848,10 @@ See docs/TYPE-SUMMARY.md for field-level detail. Key types at a glance:
1848
1848
  | `GovernanceConfig` | Buyer-side governance middleware config |
1849
1849
  | `EstablishedProposalStore` | Durable 3.0/3.1 proposal snapshots, atomic mutation fences, seven-day completion proofs, pruning, and submitted-task reconciliation |
1850
1850
  | `WebhooksConfig.tenantScope` | Explicit trusted webhook namespace for a genuinely single-tenant server; multi-tenant servers derive scope per request |
1851
+ | `PostgresTaskSettlementCoordinator` | Atomically commits a push task terminal state and PostgreSQL recovery-outbox checkpoint for different-process workers |
1851
1852
 
1852
1853
  Production webhook publishers may construct an unbound emitter and call `forTenantScope(trustedTenant)` before every delivery. Direct unbound `emit()` fails before checkpointing or network access. `createAdcpServer` derives scope from trusted request context; configure `webhooks.tenantScope` only for a genuinely single-tenant factory.
1854
+ Push-enabled decisioning tasks settled by another process must return `ctx.handoffToTask(producer, { settlement: 'external' })`; the framework withholds `submitted` until the producer durably queues the complete scoped handle and encrypted route. Workers use `createPostgresTaskSettlementCoordinator()` with `completeScopedPushTask()` / `failScopedPushTask()`; the task mutation and encrypted recovery outbox checkpoint commit together. Acknowledge work only for `applied` or compatible `already_terminal`; retry or dead-letter scope misses and conflicts.
1853
1855
 
1854
1856
  ## Task Statuses
1855
1857
 
@@ -90,10 +90,11 @@ OAuth flow handlers must preserve `state`, verify callback binding, and pass an
90
90
 
91
91
  Cross-origin signing-key discovery now requires a schema-shaped, active
92
92
  `authorized_operators[]` grant. Broad grants use `brands: ['*']`, omitted
93
- scopes (or `['all']`), and omitted countries. For narrower grants, configure
94
- the matching trusted `requiredOperatorBrand`, `requiredOperatorScope`, and/or
95
- `requiredOperatorCountry` under `webhookVerification.resolverOptions` (or the
96
- corresponding low-level resolver options); constrained dimensions fail closed
93
+ scopes (or `['all']`), and omitted countries. For narrower grants, pass
94
+ `TaskOptions.delegatedOperatorAuthorization` per dispatch, or configure the
95
+ matching trusted client-wide `requiredOperatorBrand`, `requiredOperatorScope`,
96
+ and/or `requiredOperatorCountry` fallback under
97
+ `webhookVerification.resolverOptions`. Constrained dimensions fail closed
97
98
  without context. Cached delegated keys expire no later than `valid_until`. See
98
99
  [the 13→14 migration detail](migration-13-to-14.md#cross-origin-signing-key-delegation).
99
100
 
@@ -103,7 +104,7 @@ without context. Cached delegated keys expire no later than `valid_until`. See
103
104
  - Add `zip`, `published_post`, `card`, `pixel_tracker`, `vast_tracker`, and `daast_tracker` to exhaustive `AssetInstance` handling.
104
105
  - Read canonical compliance scenarios from `ComplianceResult.tracks`; `tested_tracks` contains compact reference entries.
105
106
  - `createAdcpServerFromPlatform()` never emits a task webhook for an inline terminal result. The deprecated `autoEmitCompletionWebhooks` option is ignored under AdCP 3.2.
106
- - Rename raw platform hooks to explicit forms such as `buildCreativeLegacy`, `previewCreativeLegacy`, `listCreativeFormatsLegacy`, and the corresponding content-standard and brand-rights names. Canonical AdCP 3.2 previews now use `previewCreative` with `target_capability_id`, `creative_id`, or `creative_manifest`; retain `previewCreativeLegacy` only for `format_id` callers. Custom `WebhookRegistrationStore` implementations must round-trip the optional `previewMode` field so callback routing survives restarts.
107
+ - Rename raw platform hooks to explicit forms such as `buildCreativeLegacy`, `previewCreativeLegacy`, `listCreativeFormatsLegacy`, and the corresponding content-standard and brand-rights names. Canonical AdCP 3.2 previews now use `previewCreative` with `target_capability_id`, `creative_id`, or `creative_manifest`; retain `previewCreativeLegacy` only for `format_id` callers. Custom `WebhookRegistrationStore` implementations must round-trip `previewMode`, `authorizationContextVersion`, and `delegatedOperatorAuthorization` so callback routing and delegated authority survive restarts; dispatch now fails closed if an immediate read-back loses the versioned authorization fields, and automatic key discovery rejects pre-upgrade RFC 9421 rows that lack the version marker.
107
108
  - Put incrementally migrated raw handler groups under `legacyHandlers`.
108
109
  - Replace removed registry hierarchy calls with `lookupBrand()`/`lookupBrands()` and inspect relationship evidence; use `{ fresh: true }` when a live origin check is required.
109
110
  - Use `PayloadDigestOptions` for `computePayloadDigestSha256()` rather than the removed bare `RegExp` or `false` overloads.
@@ -2,6 +2,12 @@
2
2
 
3
3
  SDK 14 adopts AdCP `3.2.0-beta.8` while preserving the canonical creative boundary introduced in SDK 13. Most SDK 13 applications can install the beta and continue using the established 3.x tools unchanged; adopt the compact 3.2 lifecycle only after the remote agent advertises it.
4
4
 
5
+ Legacy signal-discovery adapters may keep supplying `opts.signals.getSignals`
6
+ (or `legacyHandlers.signals.getSignals`) while declaring the truthful
7
+ `signal-marketplace` or `signal-owned` specialism. That compatibility handler
8
+ now satisfies platform validation without requiring adopters to invent an
9
+ `activate_signal` implementation during an incremental migration.
10
+
5
11
  AdCP 3.2 prereleases are exact protocol pins: beta.6 replaces beta.5 in the
6
12
  SDK's compatible-version list rather than extending a rolling 3.2-beta range.
7
13
  Beta.1 restored `adcp_major_version` on `buy_products`,
@@ -57,11 +63,35 @@ const client = new SingleAgentClient(agent, {
57
63
  });
58
64
  ```
59
65
 
60
- These resolver options are trusted client-wide key-discovery policy, not
61
- per-operation authorization inferred from tool arguments. Use a separate
62
- client/resolver for each constrained brand, scope, and country tuple; do not
63
- reuse the example client for operations outside `brand_a` / `media_buying` /
64
- `GB`.
66
+ These resolver options remain a trusted client-wide fallback for a client that
67
+ uses one tuple. Shared clients can now select and durably persist the trusted
68
+ tuple per dispatch:
69
+
70
+ ```ts
71
+ await client.createMediaBuy(request, undefined, {
72
+ delegatedOperatorAuthorization: {
73
+ brand: 'brand_b',
74
+ scope: 'media_buying',
75
+ country: 'US',
76
+ },
77
+ });
78
+ ```
79
+
80
+ The per-call object takes whole-object precedence and is local receiver policy;
81
+ the SDK neither infers it from request fields nor sends it on the wire. Custom
82
+ `WebhookRegistrationStore` implementations must round-trip the versioned
83
+ authorization fields so restart-time verification can revalidate the tuple
84
+ against live `brand.json`, with immediate read-your-writes consistency after
85
+ `putIfAbsent()`. The SDK reads the row back before seller dispatch and fails
86
+ closed if a legacy projection drops either field. Do not persist a prior allow
87
+ decision as authority.
88
+
89
+ Pre-upgrade RFC 9421 rows without `authorizationContextVersion` cannot be
90
+ safely backfilled from the receiver's current configuration. Automatic key
91
+ discovery rejects them after upgrade; drain them first or re-dispatch the
92
+ operation to create a versioned registration. A caller-supplied
93
+ `webhookVerification.jwks` may support legacy rows only when it independently
94
+ preserves their original trust boundary.
65
95
 
66
96
  The same options are accepted by `resolveAgent()`, `getAgentJwks()`,
67
97
  `createAgentJwksSet()`, and `ResolvedAgentJwksResolver`. A constrained list with
@@ -69,6 +99,8 @@ no corresponding trusted option fails closed. Built-in JWKS caches now expire
69
99
  at the earlier of their configured TTL and the accepted delegation's
70
100
  `valid_until` boundary. Low-level `getAgentJwks()` callers receive that boundary
71
101
  as `operatorAuthorizationValidUntil` and must apply it to any custom cache.
102
+ Webhook verification also rechecks that boundary immediately before accepting a
103
+ delivery and committing its replay nonce.
72
104
 
73
105
  ### Modern MCP validation errors
74
106
 
@@ -152,8 +184,9 @@ loading; keep using `requires_capability` for a singular predicate.
152
184
  12. Replace webhook emitter `operation_id` arguments with SDK-local `delivery_id` values and upgrade custom stores to `WebhookDeliveryStore`. One delivery ID binds one canonical payload and key; use a fresh delivery ID for each changed status observation while retaining the AdCP `operation_id` inside the payload.
153
185
  13. Ensure custom 3.2 buyers include `push_notification_config.operation_id`, and update A2A integrations to keep the AdCP registration in skill parameters even when native A2A push configuration is also present.
154
186
  14. Treat failed/rejected task results as canonical terminal artifacts when `include_result` is requested; do not discard them while preserving only the summary error.
155
- 15. Persist the complete `ScopedTaskRef` for out-of-process task settlement and acknowledge durable queue items only after `applied` or `already_terminal`. Upgrade populated PostgreSQL task registries with the phased [`getDecisioningTaskRegistryScopeV1Upgrade()` runbook](./migration-task-registry-scoping.md#populated-postgresql-upgrade), not application-boot bootstrap DDL.
156
- 16. Upgrade to Node `^20.19.0 || >=22.12.0`, whose two boundaries enable the `require(esm)` support needed by the SDK's CommonJS dependency graph. Node 21 and Node 22.0–22.11 are not supported. Keep Undici 6 for the fully supported configuration, or use the tested best-effort Undici 7 override on Node 20.19+. See the [Node/Undici compatibility policy](./guides/NODE-UNDICI-COMPATIBILITY.md).
187
+ 15. Persist the complete `ScopedTaskRef` for out-of-process task settlement and acknowledge durable queue items only after `applied` or a compatible `already_terminal` outcome with the intended status. Retry or dead-letter scoped misses and conflicting terminal outcomes. Upgrade populated PostgreSQL task registries with the phased [`getDecisioningTaskRegistryScopeV1Upgrade()` runbook](./migration-task-registry-scoping.md#populated-postgresql-upgrade), not application-boot bootstrap DDL.
188
+ 16. For out-of-process settlement, return `ctx.handoffToTask(producer, { settlement: 'external' })`; the producer must durably queue the complete handle before returning, and the framework withholds `submitted` until that commit succeeds. For a push-enabled task, configure `createPostgresTaskSettlementCoordinator()` on the same PostgreSQL pool as the task registry and use `completeScopedPushTask()` / `failScopedPushTask()`. Run the webhook recovery outbox migration and recovery worker; the polling-only scoped helpers still reject push tasks. See [task registry scope migration](./migration-task-registry-scoping.md#out-of-process-settlement).
189
+ 17. Upgrade to Node `^20.19.0 || >=22.12.0`, whose two boundaries enable the `require(esm)` support needed by the SDK's CommonJS dependency graph. Node 21 and Node 22.0–22.11 are not supported. Keep Undici 6 for the fully supported configuration, or use the tested best-effort Undici 7 override on Node 20.19+. See the [Node/Undici compatibility policy](./guides/NODE-UNDICI-COMPATIBILITY.md).
157
190
 
158
191
  ### Webhook delivery identity and retry horizons
159
192
 
@@ -227,6 +260,14 @@ plus a stable non-secret equality fingerprint. The adapter must authenticate
227
260
  the supplied tenant/destination/snapshot context. Settled records redact payload
228
261
  and protected secret references. The application still owns KMS
229
262
  keys, secret management, tenant authorization/RBAC, and management APIs or UI.
263
+ For crash-safe task settlement, `createPostgresTaskSettlementCoordinator()`
264
+ explicitly removes the top-level validation `token` from the persisted payload
265
+ and protects it with the same adapter under the distinct `payload_token`
266
+ purpose before writing the outbox. Generic recovery checkpoints preserve
267
+ payload fields named `token`; use `recovery.prepare(...,
268
+ { protectPayloadToken: true })` only for a protocol field known to be secret.
269
+ The legacy transport-authentication adapter context keeps `purpose` undefined
270
+ for upgrade-compatible KMS AAD.
230
271
 
231
272
  `deliveryRetryHorizonSeconds` defaults to 86,400 seconds and accepts 86,400
232
273
  through 604,800. `createAdcpServer()` advertises the configured value under
@@ -48,10 +48,9 @@ import {
48
48
 
49
49
  return ctx.handoffToTask(async taskCtx => {
50
50
  // The queue transaction must durably store the complete taskRef before the
51
- // producer acknowledges this write. Persisting only taskCtx.id is unsafe.
51
+ // submitted response is sent. Persisting only taskCtx.id is unsafe.
52
52
  await approvals.enqueue({ taskRef: taskCtx.taskRef, request: approvalInput });
53
- return await approvals.waitForResult(taskCtx.taskRef.taskId);
54
- });
53
+ }, { settlement: 'external' });
55
54
 
56
55
  // A different process after restart:
57
56
  const item = await approvals.claim();
@@ -78,14 +77,168 @@ if (outcome.outcome === 'not_found_in_scope') {
78
77
  }
79
78
  ```
80
79
 
81
- This direct helper path is polling-only. It rejects tasks created with buyer
82
- push notifications because a registry write alone cannot durably deliver the
83
- terminal webhook after the request process restarts. For a request carrying
84
- `push_notification_config`, keep the handoff live and return the eventual
85
- result through its function so the framework owns both settlement and webhook
86
- delivery. Do not race a terminal scoped worker helper against that live
87
- handoff. `updateScopedTaskProgress()` remains available for push-enabled tasks
88
- because progress does not create a terminal delivery obligation.
80
+ The direct `completeScopedTask()` / `failScopedTask()` path is polling-only and
81
+ continues to reject push-enabled tasks. A registry write by itself cannot
82
+ durably promise the terminal webhook.
83
+
84
+ For PostgreSQL deployments, use the transactional push coordinator. It writes
85
+ the terminal task row and the webhook recovery outbox entry in one transaction,
86
+ then lets the ordinary webhook recovery worker publish it. Both tables must use
87
+ the same `pg.Pool` and database/schema:
88
+
89
+ ```ts
90
+ import {
91
+ completeScopedPushTask,
92
+ createPostgresTaskRegistry,
93
+ createPostgresTaskSettlementCoordinator,
94
+ createWebhookEmitter,
95
+ getWebhookDeliveryMigration,
96
+ getWebhookDeliveryRecoveryMigration,
97
+ pgWebhookDeliveryStore,
98
+ pollWebhookDeliveryRecovery,
99
+ TaskPushSettlementConfigurationError,
100
+ } from '@adcp/sdk/server';
101
+
102
+ // Integration sketch: pool, approvals, taskCtx, request, approvalInput,
103
+ // seal/openPushRoute, the KMS adapter, and signerKey are application-owned.
104
+ // The queue must persist encrypted push state before the request process exits.
105
+
106
+ await pool.query(getWebhookDeliveryRecoveryMigration({
107
+ tableName: 'my_agent_task_webhook_outbox',
108
+ }));
109
+ await pool.query(getWebhookDeliveryMigration({
110
+ tableName: 'my_agent_webhook_bindings',
111
+ }));
112
+
113
+ const registry = createPostgresTaskRegistry({
114
+ pool,
115
+ namespace: 'tenant:my-agent',
116
+ storageId: 'prod-eu1:primary-db',
117
+ });
118
+ const settlements = createPostgresTaskSettlementCoordinator({
119
+ registry,
120
+ publisherScope: 'my-agent',
121
+ outbox: { tableName: 'my_agent_task_webhook_outbox' },
122
+ authenticationAdapter: kmsWebhookAuthenticationAdapter,
123
+ });
124
+
125
+ // In the specialism handler, the submitted response waits until the queue
126
+ // transaction durably stores the complete taskRef plus encrypted push route.
127
+ // The callback's return leaves the task submitted; only the worker below may
128
+ // make it terminal. A callback rejection fails the initial invocation.
129
+ return ctx.handoffToTask(async taskCtx => {
130
+ await approvals.enqueue({
131
+ taskRef: taskCtx.taskRef,
132
+ push: await sealPushRoute({
133
+ url: request.push_notification_config.url,
134
+ operationId: request.push_notification_config.operation_id,
135
+ // Always persist the negotiated version. A valid pre-3.2 version is
136
+ // required when operationId is absent and the stable fallback is used.
137
+ servedAdcpVersion,
138
+ token: request.push_notification_config.token,
139
+ authentication: request.push_notification_config.authentication,
140
+ }),
141
+ request: approvalInput,
142
+ });
143
+ }, { settlement: 'external' });
144
+
145
+ // A different process, including after the request process restarted:
146
+ const item = await approvals.claim();
147
+ const openedPush = await openPushRoute(item.push);
148
+ const legacyScheme = openedPush.authentication?.schemes[0];
149
+ let outcome;
150
+ try {
151
+ outcome = await completeScopedPushTask(
152
+ settlements,
153
+ item.taskRef,
154
+ {
155
+ url: openedPush.url,
156
+ operationId: openedPush.operationId,
157
+ servedAdcpVersion: openedPush.servedAdcpVersion,
158
+ token: openedPush.token,
159
+ authentication:
160
+ legacyScheme === 'Bearer'
161
+ ? { type: 'bearer', token: openedPush.authentication.credentials }
162
+ : legacyScheme === 'HMAC-SHA256'
163
+ ? { type: 'hmac_sha256', secret: openedPush.authentication.credentials }
164
+ : null,
165
+ },
166
+ item.result,
167
+ );
168
+ } catch (error) {
169
+ if (error instanceof TaskPushSettlementConfigurationError || error instanceof TypeError) {
170
+ await approvals.deadLetterAndAlert(item, error); // immutable bad route/config
171
+ } else {
172
+ await approvals.retry(item, error); // transaction/infrastructure failure
173
+ }
174
+ return;
175
+ }
176
+
177
+ if (
178
+ outcome.outcome === 'applied' ||
179
+ (outcome.outcome === 'already_terminal' && outcome.compatibility === 'compatible')
180
+ ) {
181
+ await approvals.ack(item);
182
+ } else {
183
+ // A scope miss or conflicting terminal result must not be acknowledged as
184
+ // success. Apply the queue's bounded retry/dead-letter policy and alert.
185
+ await approvals.retryOrDeadLetter(item, outcome);
186
+ }
187
+
188
+ // Publish/recover outside the database transaction. The outbox lease is
189
+ // fenced; a crash before publish or before acknowledgement is retryable.
190
+ await pollWebhookDeliveryRecovery({
191
+ recovery: settlements.recovery,
192
+ deliver: async lease => {
193
+ const emitter = createWebhookEmitter({
194
+ signerKey,
195
+ publisherScope: lease.key.publisherScope,
196
+ tenantScope: lease.key.tenantScope,
197
+ deliveryStore: pgWebhookDeliveryStore(pool, {
198
+ tableName: 'my_agent_webhook_bindings',
199
+ }),
200
+ deliveryRecovery: settlements.recovery,
201
+ });
202
+ const result = await emitter.emitRecovered(lease);
203
+ return result.delivered
204
+ ? { disposition: 'delivered' }
205
+ : result.terminal
206
+ ? { disposition: 'terminal' }
207
+ : { disposition: 'retry', retryAfterMs: 1_000 };
208
+ },
209
+ });
210
+ ```
211
+
212
+ `TaskPushSettlementOutcome` reports the two independent state machines. Task
213
+ mutation is `applied`, compatible/conflicting `already_terminal`, or the
214
+ non-enumerating `not_found_in_scope`. Delivery is `durably_bound`,
215
+ `recoverable`, `delivered`, or `terminal`; scope misses and conflicting
216
+ settlements report `not_applicable` and never create an outbox entry. Retries
217
+ reuse one deterministic delivery identity and the first committed payload.
218
+
219
+ The settlement coordinator explicitly removes the top-level task-webhook
220
+ `token` from the JSON payload before persistence and protects it through
221
+ `authenticationAdapter`; generic recovery snapshots do not reinterpret a
222
+ field merely because it is named `token`. The adapter context has
223
+ `purpose: 'payload_token'` for that validation token. The context for legacy
224
+ transport authentication intentionally leaves `purpose` undefined so pending
225
+ snapshots encrypted by older SDK versions keep the same KMS AAD.
226
+ Include a key version in `protectedValue`; retain old decrypt-only key versions
227
+ until every pending delivery using them has settled and the configured retry
228
+ horizon has elapsed. Rotate by writing with the new version while continuing
229
+ to resolve old versions. Never store cleartext push tokens in the approval
230
+ queue, task result, logs, metrics, or dead-letter metadata.
231
+
232
+ `updateScopedTaskProgress()` remains available for push-enabled tasks because
233
+ progress does not create a terminal delivery obligation. Use
234
+ `{ settlement: 'external' }` for every handoff owned by these helpers. Its
235
+ registry must declare `durability: 'durable'` and issue a stable `registryId`;
236
+ the built-in in-memory registry is process-local and is rejected before the
237
+ producer runs or the buyer receives `submitted`. Its
238
+ producer callback may enqueue work but cannot return a terminal artifact; even
239
+ if that callback throws, the framework rejects the initial invocation, leaves
240
+ the task internally submitted, and writes no webhook. The buyer never receives
241
+ an acknowledgment before recoverable work exists.
89
242
 
90
243
  The framework owns normal in-process handoff settlement. The direct registry
91
244
  surface is specifically for trusted webhook/queue workers and explicit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcp/sdk",
3
- "version": "14.0.0-beta.14",
3
+ "version": "14.0.0-beta.15",
4
4
  "description": "AdCP SDK — client, server, and compliance harnesses for the AdContext Protocol (MCP + A2A)",
5
5
  "workspaces": [
6
6
  ".",
@@ -563,7 +563,7 @@ const server = createAdcpServerFromPlatform(platform, {
563
563
  });
564
564
  ```
565
565
 
566
- Cross-instance reads work — process A allocates the task, process B reads the lifecycle for `tasks_get`. Terminal-state idempotency is enforced atomically so concurrent settlements cannot overwrite each other. Background-completion tracking (`_registerBackground`) is process-local — promises do not serialize. Persist the complete `taskCtx.taskRef` before acknowledging an out-of-process queue write, then use `completeScopedTask()` / `failScopedTask()` and acknowledge only `applied` or an `already_terminal` result with the intended status. A scoped miss must retry or dead-letter. Direct worker settlement is polling-only and rejects tasks with buyer push notifications; those must return through the live framework handoff so terminal webhook delivery remains framework-owned.
566
+ Cross-instance reads work — process A allocates the task, process B reads the lifecycle for `tasks_get`. Terminal-state idempotency is enforced atomically so concurrent settlements cannot overwrite each other. Background-completion tracking (`_registerBackground`) is process-local — promises do not serialize. Return `ctx.handoffToTask(producer, { settlement: 'external' })` and persist the complete `taskCtx.taskRef` before the producer returns; the framework withholds `submitted` until that durable write succeeds. Then use `completeScopedTask()` / `failScopedTask()` for polling-only tasks and acknowledge only `applied` or an `already_terminal` result with the intended status. A scoped miss must retry or dead-letter. For tasks with buyer push notifications, use `createPostgresTaskSettlementCoordinator()` with `completeScopedPushTask()` / `failScopedPushTask()` so the task transition and recovery outbox checkpoint commit atomically; registry-only helpers still reject these tasks.
567
567
 
568
568
  Custom backend? Implement the scoped `TaskRegistry` interface for Redis / DynamoDB / Spanner / etc. The framework awaits every storage method; apply both `accountId` and `ownerScope` on every read and write, then set `scopeVersion: 1`. Keep the explicitly named unsafe methods limited to trusted administrative and test paths.
569
569