@adcp/sdk 14.0.0 → 14.2.0

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 (234) hide show
  1. package/dist/lib/adapters/implicit-account-store.d.mts +12 -7
  2. package/dist/lib/adapters/implicit-account-store.d.ts +12 -7
  3. package/dist/lib/adapters/implicit-account-store.js +69 -15
  4. package/dist/lib/adapters/implicit-account-store.mjs +69 -15
  5. package/dist/lib/core/AgentClient.d.mts +1 -0
  6. package/dist/lib/core/AgentClient.d.ts +1 -0
  7. package/dist/lib/core/AgentClient.js +3 -0
  8. package/dist/lib/core/AgentClient.mjs +3 -0
  9. package/dist/lib/core/SingleAgentClient.d.mts +35 -2
  10. package/dist/lib/core/SingleAgentClient.d.ts +35 -2
  11. package/dist/lib/core/SingleAgentClient.js +377 -36
  12. package/dist/lib/core/SingleAgentClient.mjs +387 -38
  13. package/dist/lib/core/TaskExecutor.d.mts +3 -1
  14. package/dist/lib/core/TaskExecutor.d.ts +3 -1
  15. package/dist/lib/core/TaskExecutor.js +17 -12
  16. package/dist/lib/core/TaskExecutor.mjs +17 -12
  17. package/dist/lib/core/account-key.d.mts +3 -0
  18. package/dist/lib/core/account-key.d.ts +3 -0
  19. package/dist/lib/core/account-key.js +41 -0
  20. package/dist/lib/core/account-key.mjs +17 -0
  21. package/dist/lib/core/account-resolution.d.mts +2 -0
  22. package/dist/lib/core/account-resolution.d.ts +2 -0
  23. package/dist/lib/core/buyer-account-registry.d.mts +93 -0
  24. package/dist/lib/core/buyer-account-registry.d.ts +93 -0
  25. package/dist/lib/core/buyer-account-registry.js +602 -0
  26. package/dist/lib/core/buyer-account-registry.mjs +578 -0
  27. package/dist/lib/core/product-cache.d.mts +18 -0
  28. package/dist/lib/core/product-cache.d.ts +18 -0
  29. package/dist/lib/core/product-cache.js +137 -0
  30. package/dist/lib/core/product-cache.mjs +112 -0
  31. package/dist/lib/errors/index.d.mts +40 -1
  32. package/dist/lib/errors/index.d.ts +40 -1
  33. package/dist/lib/errors/index.js +69 -3
  34. package/dist/lib/errors/index.mjs +64 -3
  35. package/dist/lib/governance/authorization.d.mts +17 -1
  36. package/dist/lib/governance/authorization.d.ts +17 -1
  37. package/dist/lib/governance/authorization.js +55 -7
  38. package/dist/lib/governance/authorization.mjs +59 -7
  39. package/dist/lib/governance/index.d.mts +2 -2
  40. package/dist/lib/governance/index.d.ts +2 -2
  41. package/dist/lib/governance/index.js +2 -0
  42. package/dist/lib/governance/index.mjs +3 -1
  43. package/dist/lib/index.d.mts +7 -4
  44. package/dist/lib/index.d.ts +7 -4
  45. package/dist/lib/index.js +29 -0
  46. package/dist/lib/index.mjs +31 -1
  47. package/dist/lib/net/agent-transport-fetch.d.mts +4 -0
  48. package/dist/lib/net/agent-transport-fetch.d.ts +4 -0
  49. package/dist/lib/net/agent-transport-fetch.js +18 -5
  50. package/dist/lib/net/agent-transport-fetch.mjs +17 -5
  51. package/dist/lib/protocols/a2a.js +9 -1
  52. package/dist/lib/protocols/a2a.mjs +9 -1
  53. package/dist/lib/protocols/index.js +9 -2
  54. package/dist/lib/protocols/index.mjs +9 -2
  55. package/dist/lib/protocols/mcp-modern.js +2 -1
  56. package/dist/lib/protocols/mcp-modern.mjs +2 -1
  57. package/dist/lib/protocols/mcp.js +5 -2
  58. package/dist/lib/protocols/mcp.mjs +5 -2
  59. package/dist/lib/protocols/rawResponseCapture.d.mts +6 -0
  60. package/dist/lib/protocols/rawResponseCapture.d.ts +6 -0
  61. package/dist/lib/protocols/rawResponseCapture.js +41 -29
  62. package/dist/lib/protocols/rawResponseCapture.mjs +40 -29
  63. package/dist/lib/protocols/signedRequestRejection.d.mts +9 -0
  64. package/dist/lib/protocols/signedRequestRejection.d.ts +9 -0
  65. package/dist/lib/protocols/signedRequestRejection.js +209 -0
  66. package/dist/lib/protocols/signedRequestRejection.mjs +189 -0
  67. package/dist/lib/protocols/transportDiagnostics.d.mts +1 -0
  68. package/dist/lib/protocols/transportDiagnostics.d.ts +1 -0
  69. package/dist/lib/protocols/transportDiagnostics.js +2 -0
  70. package/dist/lib/protocols/transportDiagnostics.mjs +1 -0
  71. package/dist/lib/registry/types.generated.d.mts +112 -45
  72. package/dist/lib/registry/types.generated.d.ts +112 -45
  73. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  74. package/dist/lib/server/account-provisioning.d.mts +2 -0
  75. package/dist/lib/server/account-provisioning.d.ts +2 -0
  76. package/dist/lib/server/account-provisioning.js +30 -0
  77. package/dist/lib/server/account-provisioning.mjs +6 -0
  78. package/dist/lib/server/account-reference-warnings.d.mts +12 -0
  79. package/dist/lib/server/account-reference-warnings.d.ts +12 -0
  80. package/dist/lib/server/account-reference-warnings.js +48 -0
  81. package/dist/lib/server/account-reference-warnings.mjs +23 -0
  82. package/dist/lib/server/auth-signature.js +1 -0
  83. package/dist/lib/server/auth-signature.mjs +1 -0
  84. package/dist/lib/server/create-adcp-server.d.mts +34 -0
  85. package/dist/lib/server/create-adcp-server.d.ts +34 -0
  86. package/dist/lib/server/create-adcp-server.js +225 -14
  87. package/dist/lib/server/create-adcp-server.mjs +225 -14
  88. package/dist/lib/server/decisioning/account.d.mts +2 -0
  89. package/dist/lib/server/decisioning/account.d.ts +2 -0
  90. package/dist/lib/server/decisioning/runtime/from-platform.js +49 -10
  91. package/dist/lib/server/decisioning/runtime/from-platform.mjs +49 -10
  92. package/dist/lib/server/index.d.mts +2 -2
  93. package/dist/lib/server/index.d.ts +2 -2
  94. package/dist/lib/server/index.js +2 -0
  95. package/dist/lib/server/index.mjs +3 -1
  96. package/dist/lib/signing/agent-resolver/consistency.d.mts +6 -13
  97. package/dist/lib/signing/agent-resolver/consistency.d.ts +6 -13
  98. package/dist/lib/signing/agent-resolver/consistency.js +0 -1
  99. package/dist/lib/signing/agent-resolver/consistency.mjs +0 -1
  100. package/dist/lib/signing/agent-resolver/errors.d.mts +3 -1
  101. package/dist/lib/signing/agent-resolver/errors.d.ts +3 -1
  102. package/dist/lib/signing/agent-resolver/errors.js +6 -0
  103. package/dist/lib/signing/agent-resolver/errors.mjs +6 -0
  104. package/dist/lib/signing/agent-resolver/fetch-helpers.d.mts +11 -0
  105. package/dist/lib/signing/agent-resolver/fetch-helpers.d.ts +11 -0
  106. package/dist/lib/signing/agent-resolver/fetch-helpers.js +37 -2
  107. package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +40 -3
  108. package/dist/lib/signing/agent-resolver/jwks-set.js +24 -3
  109. package/dist/lib/signing/agent-resolver/jwks-set.mjs +24 -3
  110. package/dist/lib/signing/agent-resolver/legacy-brand.d.mts +15 -0
  111. package/dist/lib/signing/agent-resolver/legacy-brand.d.ts +15 -0
  112. package/dist/lib/signing/agent-resolver/legacy-brand.js +60 -0
  113. package/dist/lib/signing/agent-resolver/legacy-brand.mjs +36 -0
  114. package/dist/lib/signing/agent-resolver/operator-authorization.d.mts +13 -0
  115. package/dist/lib/signing/agent-resolver/operator-authorization.d.ts +13 -0
  116. package/dist/lib/signing/agent-resolver/operator-authorization.js +108 -0
  117. package/dist/lib/signing/agent-resolver/operator-authorization.mjs +84 -0
  118. package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +14 -4
  119. package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +14 -4
  120. package/dist/lib/signing/agent-resolver/resolve-agent.js +148 -137
  121. package/dist/lib/signing/agent-resolver/resolve-agent.mjs +157 -139
  122. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.mts +7 -1
  123. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.ts +7 -1
  124. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.js +50 -17
  125. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.mjs +50 -17
  126. package/dist/lib/signing/agent-resolver/select-agent.d.mts +14 -15
  127. package/dist/lib/signing/agent-resolver/select-agent.d.ts +14 -15
  128. package/dist/lib/signing/agent-resolver/select-agent.js +98 -13
  129. package/dist/lib/signing/agent-resolver/select-agent.mjs +95 -13
  130. package/dist/lib/signing/brand-jwks.d.mts +29 -75
  131. package/dist/lib/signing/brand-jwks.d.ts +29 -75
  132. package/dist/lib/signing/brand-jwks.js +120 -182
  133. package/dist/lib/signing/brand-jwks.mjs +120 -182
  134. package/dist/lib/signing/errors.d.mts +7 -3
  135. package/dist/lib/signing/errors.d.ts +7 -3
  136. package/dist/lib/signing/errors.js +7 -2
  137. package/dist/lib/signing/errors.mjs +7 -2
  138. package/dist/lib/signing/jwks-https.d.mts +7 -0
  139. package/dist/lib/signing/jwks-https.d.ts +7 -0
  140. package/dist/lib/signing/jwks-https.js +31 -8
  141. package/dist/lib/signing/jwks-https.mjs +31 -8
  142. package/dist/lib/signing/jwks.d.mts +8 -0
  143. package/dist/lib/signing/jwks.d.ts +8 -0
  144. package/dist/lib/signing/middleware.js +2 -1
  145. package/dist/lib/signing/middleware.mjs +2 -1
  146. package/dist/lib/signing/publisher-pins.d.mts +11 -0
  147. package/dist/lib/signing/publisher-pins.d.ts +11 -0
  148. package/dist/lib/signing/publisher-pins.js +125 -0
  149. package/dist/lib/signing/publisher-pins.mjs +101 -0
  150. package/dist/lib/signing/server.d.mts +1 -0
  151. package/dist/lib/signing/server.d.ts +1 -0
  152. package/dist/lib/signing/types.d.mts +5 -0
  153. package/dist/lib/signing/types.d.ts +5 -0
  154. package/dist/lib/signing/verifier.js +50 -5
  155. package/dist/lib/signing/verifier.mjs +50 -5
  156. package/dist/lib/signing/webhook-verifier.d.mts +7 -2
  157. package/dist/lib/signing/webhook-verifier.d.ts +7 -2
  158. package/dist/lib/signing/webhook-verifier.js +42 -2
  159. package/dist/lib/signing/webhook-verifier.mjs +42 -2
  160. package/dist/lib/testing/storyboard/account-policy.d.mts +2 -0
  161. package/dist/lib/testing/storyboard/account-policy.d.ts +2 -0
  162. package/dist/lib/testing/storyboard/account-policy.js +35 -0
  163. package/dist/lib/testing/storyboard/account-policy.mjs +11 -0
  164. package/dist/lib/testing/storyboard/context.js +6 -0
  165. package/dist/lib/testing/storyboard/context.mjs +6 -0
  166. package/dist/lib/testing/storyboard/request-builder.js +12 -2
  167. package/dist/lib/testing/storyboard/request-builder.mjs +12 -2
  168. package/dist/lib/testing/storyboard/runner.js +3 -2
  169. package/dist/lib/testing/storyboard/runner.mjs +3 -2
  170. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  171. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  172. package/dist/lib/types/accept-proposal.d.ts +19 -1
  173. package/dist/lib/types/buy-products.d.ts +19 -1
  174. package/dist/lib/types/check-governance.d.ts +19 -1
  175. package/dist/lib/types/comply-test-controller.d.ts +19 -1
  176. package/dist/lib/types/control-media-buy.d.ts +19 -1
  177. package/dist/lib/types/core.generated.d.mts +14 -1
  178. package/dist/lib/types/core.generated.d.ts +14 -1
  179. package/dist/lib/types/create-media-buy.d.ts +14 -1
  180. package/dist/lib/types/get-media-buys.d.ts +14 -1
  181. package/dist/lib/types/get-products.d.ts +19 -1
  182. package/dist/lib/types/list-products.d.ts +19 -1
  183. package/dist/lib/types/refine-proposals.d.ts +19 -1
  184. package/dist/lib/types/request-proposals.d.ts +19 -1
  185. package/dist/lib/types/schemas.generated.d.ts +12 -3
  186. package/dist/lib/types/schemas.generated.js +4 -1
  187. package/dist/lib/types/schemas.generated.mjs +4 -1
  188. package/dist/lib/types/tools.generated.d.mts +14 -1
  189. package/dist/lib/types/tools.generated.d.ts +14 -1
  190. package/dist/lib/types/update-media-buy.d.ts +14 -1
  191. package/dist/lib/version.d.mts +3 -3
  192. package/dist/lib/version.d.ts +3 -3
  193. package/dist/lib/version.js +3 -3
  194. package/dist/lib/version.mjs +3 -3
  195. package/dist/lib/webhooks/index.d.mts +24 -0
  196. package/dist/lib/webhooks/index.d.ts +24 -0
  197. package/dist/lib/webhooks/index.js +50 -24
  198. package/dist/lib/webhooks/index.mjs +49 -24
  199. package/dist/lib/wholesale-feed-sync/index.d.mts +2 -0
  200. package/dist/lib/wholesale-feed-sync/index.d.ts +2 -0
  201. package/dist/lib/wholesale-feed-sync/index.js +7 -0
  202. package/dist/lib/wholesale-feed-sync/index.mjs +4 -0
  203. package/dist/lib/wholesale-feed-sync/mirror.d.mts +97 -0
  204. package/dist/lib/wholesale-feed-sync/mirror.d.ts +97 -0
  205. package/dist/lib/wholesale-feed-sync/mirror.js +350 -0
  206. package/dist/lib/wholesale-feed-sync/mirror.mjs +322 -0
  207. package/dist/lib/wholesale-feed-sync/sync.d.mts +13 -29
  208. package/dist/lib/wholesale-feed-sync/sync.d.ts +13 -29
  209. package/dist/lib/wholesale-feed-sync/sync.js +208 -281
  210. package/dist/lib/wholesale-feed-sync/sync.mjs +213 -281
  211. package/dist/lib/wholesale-feed-sync/types.d.mts +4 -3
  212. package/dist/lib/wholesale-feed-sync/types.d.ts +4 -3
  213. package/docs/README.md +6 -0
  214. package/docs/TYPE-SUMMARY.md +2 -2
  215. package/docs/guides/BUILD-AN-AGENT.md +2 -2
  216. package/docs/guides/BUYER-QUICKSTART-3.2.md +2 -0
  217. package/docs/guides/BUYER-STORAGE.md +3 -0
  218. package/docs/guides/FIRST-CALL-TO-A-SELLER.md +106 -0
  219. package/docs/guides/SIGNING-GUIDE.md +16 -7
  220. package/docs/guides/account-resolution.md +132 -10
  221. package/docs/llms.txt +3 -2
  222. package/docs/migration-14.0-to-14.1.md +85 -0
  223. package/docs/migration-14.x-rc-worksheet.md +4 -4
  224. package/docs/migration-4.x-to-5.x.md +1 -0
  225. package/docs/migration-agent-resolution-3.3.md +125 -0
  226. package/docs/recipes/verifying-inbound-webhooks.md +60 -15
  227. package/package.json +3 -2
  228. package/skills/adcp-brand.previous/SKILL.md +0 -200
  229. package/skills/adcp-creative.previous/SKILL.md +0 -305
  230. package/skills/adcp-governance.previous/SKILL.md +0 -566
  231. package/skills/adcp-measurement.previous/SKILL.md +0 -136
  232. package/skills/adcp-media-buy.previous/SKILL.md +0 -556
  233. package/skills/adcp-si.previous/SKILL.md +0 -206
  234. package/skills/adcp-signals.previous/SKILL.md +0 -204
@@ -1,11 +1,21 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { randomUUID } from "node:crypto";
3
+ import {
4
+ refreshWholesaleFeed,
5
+ InMemoryWholesaleFeedMirrorStore,
6
+ applyWholesaleFeedEvent,
7
+ diffWholesaleFeed
8
+ } from "./mirror.mjs";
4
9
  import { assertLegacyWholesaleFeedRepresentation } from "./webhook-notification.mjs";
10
+ const bootstrapReadFailures = /* @__PURE__ */ new WeakSet();
11
+ function supersededRepairError() {
12
+ const error = new Error("WholesaleFeedSync: repair superseded repeatedly; retry webhook delivery.");
13
+ bootstrapReadFailures.add(error);
14
+ return error;
15
+ }
5
16
  const DEFAULT_PROBE_INTERVAL_MS = 6e5;
6
17
  const DEFAULT_CAPABILITY_REFRESH_INTERVAL_MS = 864e5;
7
18
  const DEFAULT_PERSISTENCE_TIMEOUT_MS = 3e4;
8
- const DEFAULT_BOOTSTRAP_PAGE_LIMIT = 100;
9
19
  const VERSION_MISMATCH_RECOVERY_ATTEMPTS = 3;
10
20
  const VERSION_MISMATCH_RECOVERY_BACKOFF_MS = 5;
11
21
  class WholesaleFeedSync extends EventEmitter {
@@ -46,11 +56,10 @@ class WholesaleFeedSync extends EventEmitter {
46
56
  probeTimer = null;
47
57
  capabilityTimer = null;
48
58
  lifecycleEpoch = 0;
59
+ mirrorRevision = 0;
49
60
  /**
50
- * Read-only view of the in-memory product index. The `mode` reflects the
51
- * sync strategy for product events specifically — in mixed-capability
52
- * agents (products-feed + signals-wholesale-only, or vice versa) this
53
- * may differ from `signals.mode`.
61
+ * Product index view. `mode` identifies the product sync strategy and may
62
+ * differ from `signals.mode` for agents with mixed feed capabilities.
54
63
  */
55
64
  products = {
56
65
  list: () => [...this.productIndex.values()],
@@ -85,19 +94,12 @@ class WholesaleFeedSync extends EventEmitter {
85
94
  },
86
95
  _mode: "manual",
87
96
  /**
88
- * `true` when the agent supports `discovery_mode: 'wholesale'` on
89
- * `get_signals` (i.e., signals are browsable). When `false`, the
90
- * agent only supports brief-mode discovery — `signals.list()` will
91
- * be empty until adopters call into the agent with their own briefs.
97
+ * True when `get_signals` supports `discovery_mode: 'wholesale'`.
98
+ * Otherwise `list()` is empty; use client brief discovery instead.
92
99
  */
93
100
  queryable: true
94
101
  };
95
- /**
96
- * One-shot console warning when adopters call `signals.list()` or
97
- * `signals.search()` against an agent that doesn't support wholesale
98
- * signal enumeration. Without this, empty results read as "no signals
99
- * match" rather than "the agent doesn't browse, only briefs."
100
- */
102
+ // Warn once when empty signal results reflect missing wholesale support.
101
103
  warnIfSignalsNotQueryable() {
102
104
  if (this.signals.queryable || this.signalsQueryableWarned) return;
103
105
  if (this._state === "idle" || this._state === "bootstrapping") return;
@@ -123,14 +125,9 @@ class WholesaleFeedSync extends EventEmitter {
123
125
  }
124
126
  // ====== Lifecycle ======
125
127
  /**
126
- * Probe the agent's capabilities, pick a sync mode, and bootstrap the
127
- * in-memory replica via wholesale enumeration. In `'auto-poll'` mode
128
- * starts the conditional wholesale-feed version probe loop.
129
- *
130
- * Safe to call repeatedly — concurrent calls await the in-flight
131
- * bootstrap and return when it completes (no duplicate bootstrap, no
132
- * silent drop). Sequential calls re-probe capabilities and re-bootstrap,
133
- * equivalent to calling `refresh()` after a mode upgrade.
128
+ * Discover capabilities, bootstrap the mirror, and schedule auto-poll probes
129
+ * for versioned sellers. Concurrent calls share the bootstrap; sequential
130
+ * calls re-probe capabilities and refresh the mirror.
134
131
  */
135
132
  async start() {
136
133
  if (this.startPromise && this.startPromiseEpoch === this.lifecycleEpoch) return this.startPromise;
@@ -149,12 +146,13 @@ class WholesaleFeedSync extends EventEmitter {
149
146
  const epoch = this.lifecycleEpoch;
150
147
  if (!await this.restorePersistedState(epoch)) return;
151
148
  if (!await this.resolveMode(epoch)) return;
152
- if (!await this.bootstrap({ epoch })) return;
153
- if (this._mode === "auto-poll") {
154
- this.scheduleProbe(epoch);
155
- }
156
- if (this.capabilityRefreshIntervalMs > 0) {
157
- this.scheduleCapabilityRefresh(epoch);
149
+ try {
150
+ await this.bootstrapWithRetries({ epoch });
151
+ } finally {
152
+ if (this.isLifecycleCurrent(epoch)) {
153
+ if (this._mode === "auto-poll") this.scheduleProbe(epoch);
154
+ if (this.capabilityRefreshIntervalMs > 0) this.scheduleCapabilityRefresh(epoch);
155
+ }
158
156
  }
159
157
  }
160
158
  /** Stop all background activity. Preserves in-memory state and version tokens. */
@@ -164,7 +162,8 @@ class WholesaleFeedSync extends EventEmitter {
164
162
  if (this.capabilityTimer) clearTimeout(this.capabilityTimer);
165
163
  this.probeTimer = null;
166
164
  this.capabilityTimer = null;
167
- if (this._state === "syncing" || this._state === "bootstrapping") this.setState("idle");
165
+ if (this._state === "syncing" || this._state === "bootstrapping" || this._state === "degraded")
166
+ this.setState("idle");
168
167
  }
169
168
  /**
170
169
  * Stop, clear all indexes and version tokens. Call `start()` again to
@@ -172,6 +171,7 @@ class WholesaleFeedSync extends EventEmitter {
172
171
  */
173
172
  async reset() {
174
173
  this.stop();
174
+ ++this.mirrorRevision;
175
175
  this.productIndex.clear();
176
176
  this.signalIndex.clear();
177
177
  this.productWholesaleFeedVersion = void 0;
@@ -197,7 +197,7 @@ class WholesaleFeedSync extends EventEmitter {
197
197
  async refresh() {
198
198
  const epoch = this.lifecycleEpoch;
199
199
  this.emit("resyncing", { reason: "manual" });
200
- await this.bootstrap({ emitDiffs: true, epoch });
200
+ await this.bootstrapWithRetries({ emitDiffs: true, epoch, propagateFailure: true });
201
201
  }
202
202
  /**
203
203
  * Apply one legacy-view account-level wholesale feed webhook to the local mirror.
@@ -246,6 +246,11 @@ class WholesaleFeedSync extends EventEmitter {
246
246
  this.rememberLastWebhookEventId(event.event_id);
247
247
  throw err;
248
248
  }
249
+ if (!this._lastSyncedAt && event.event_type !== "wholesale_feed.bulk_change") {
250
+ if (!await this.recoverFromVersionMismatch(event, epoch)) return;
251
+ await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
252
+ return;
253
+ }
249
254
  if (webhook.previous_wholesale_feed_version && currentVersion && webhook.previous_wholesale_feed_version !== currentVersion) {
250
255
  if (!await this.recoverFromVersionMismatch(event, epoch)) return;
251
256
  await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
@@ -256,8 +261,10 @@ class WholesaleFeedSync extends EventEmitter {
256
261
  try {
257
262
  if (!await this.recoverFromBulkChange(event, epoch)) return;
258
263
  } catch (err) {
259
- await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
260
- this.rememberLastWebhookEventId(event.event_id);
264
+ if (!(err instanceof Error && bootstrapReadFailures.has(err))) {
265
+ await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
266
+ this.rememberLastWebhookEventId(event.event_id);
267
+ }
261
268
  throw err;
262
269
  }
263
270
  await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
@@ -324,10 +331,24 @@ class WholesaleFeedSync extends EventEmitter {
324
331
  return true;
325
332
  }
326
333
  // ====== Private: bootstrap (wholesale enumeration) ======
334
+ async bootstrapWithRetries(options) {
335
+ for (let attempt = 0; attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
336
+ const revision = this.mirrorRevision;
337
+ if (await this.bootstrap({ ...options, conditional: attempt === 0 ? void 0 : false })) return;
338
+ if (!this.isLifecycleCurrent(options.epoch) || revision === this.mirrorRevision) return;
339
+ }
340
+ const error = new Error("WholesaleFeedSync: refresh superseded repeatedly; retry start or refresh.");
341
+ this.handleBootstrapFailure({ error });
342
+ throw error;
343
+ }
327
344
  async bootstrap(options = {}) {
328
345
  const epoch = options.epoch ?? this.lifecycleEpoch;
329
346
  if (!this.isLifecycleCurrent(epoch)) return false;
330
347
  this.setState("bootstrapping");
348
+ const previousLastSyncedAt = this._lastSyncedAt;
349
+ const expectedRevision = this.mirrorRevision;
350
+ let reportedFailure;
351
+ let committed = false;
331
352
  try {
332
353
  const previousProducts = new Map(this.productIndex);
333
354
  const previousSignals = new Map(this.signalIndex);
@@ -339,14 +360,50 @@ class WholesaleFeedSync extends EventEmitter {
339
360
  const refreshProducts = entities !== "signals";
340
361
  const refreshSignals = entities !== "products" && this.signals.queryable;
341
362
  if (refreshProducts) {
342
- productResult = await this.bootstrapProducts(epoch);
363
+ productResult = await this.bootstrapProducts(epoch, options.conditional);
343
364
  if (productResult.cancelled) return false;
365
+ if (this.mirrorRevision !== expectedRevision) {
366
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
367
+ return false;
368
+ }
369
+ if (productResult.failure) {
370
+ const failure = productResult.failure;
371
+ reportedFailure = failure.error;
372
+ this.handleBootstrapFailure(failure);
373
+ if (options.propagateFailure) {
374
+ bootstrapReadFailures.add(failure.error);
375
+ if (failure.adcpError) Object.assign(failure.error, { adcpError: failure.adcpError });
376
+ throw failure.error;
377
+ }
378
+ return false;
379
+ }
344
380
  }
345
381
  if (refreshSignals) {
346
- signalResult = await this.bootstrapSignals(epoch);
382
+ signalResult = await this.bootstrapSignals(epoch, options.conditional);
347
383
  if (signalResult.cancelled) return false;
384
+ if (this.mirrorRevision !== expectedRevision) {
385
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
386
+ return false;
387
+ }
388
+ if (signalResult.failure) {
389
+ const failure = signalResult.failure;
390
+ reportedFailure = failure.error;
391
+ this.handleBootstrapFailure(failure);
392
+ if (options.propagateFailure) {
393
+ bootstrapReadFailures.add(failure.error);
394
+ if (failure.adcpError) Object.assign(failure.error, { adcpError: failure.adcpError });
395
+ throw failure.error;
396
+ }
397
+ return false;
398
+ }
348
399
  }
349
400
  if (!this.isLifecycleCurrent(epoch)) return false;
401
+ if (this.mirrorRevision !== expectedRevision) {
402
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
403
+ return false;
404
+ }
405
+ ++this.mirrorRevision;
406
+ committed = true;
350
407
  if (refreshProducts && productResult) {
351
408
  this.commitProductMetadata(productResult.metadata);
352
409
  if (!productResult.unchanged) {
@@ -378,77 +435,91 @@ class WholesaleFeedSync extends EventEmitter {
378
435
  return true;
379
436
  } catch (err) {
380
437
  if (!this.isLifecycleCurrent(epoch)) return false;
381
- this.setState("error");
438
+ if (!committed && this.mirrorRevision !== expectedRevision && err !== reportedFailure) return false;
439
+ if (err === reportedFailure) throw err;
440
+ this._lastSyncedAt = previousLastSyncedAt;
441
+ this.setState(this._lastSyncedAt ? "degraded" : "error");
382
442
  const error = err instanceof Error ? err : new Error(String(err));
383
443
  this.errorHandler?.(error);
384
- this.emit("error", { error });
444
+ if (this.listenerCount("error") > 0) this.emit("error", { error });
445
+ if (options.propagateFailure) bootstrapReadFailures.add(error);
385
446
  throw error;
386
447
  }
387
448
  }
388
- /** Returns `true` when the seller short-circuited with `unchanged: true`. */
389
- async bootstrapProducts(epoch) {
390
- let cursor;
391
- const into = /* @__PURE__ */ new Map();
392
- let metadata = this.currentProductMetadata();
393
- do {
394
- const params = {
395
- buying_mode: "wholesale",
396
- pagination: { max_results: DEFAULT_BOOTSTRAP_PAGE_LIMIT, ...cursor && { cursor } },
397
- ...this.account && { account: this.account }
398
- };
399
- if (!cursor && metadata.wholesaleFeedVersion && this._capabilities.wholesaleFeedVersioning) {
400
- params.if_wholesale_feed_version = metadata.wholesaleFeedVersion;
401
- if (metadata.pricingVersion) params.if_pricing_version = metadata.pricingVersion;
402
- }
403
- const result = await this.client.getProducts(params);
404
- if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
405
- const body = result.data;
406
- if (!body) return { cancelled: false, unchanged: false, items: into, metadata };
407
- if (body.unchanged) {
408
- metadata = mergeFeedMetadata(metadata, body);
409
- return { cancelled: false, unchanged: true, items: into, metadata };
410
- }
411
- const products = Array.isArray(body.products) ? body.products : [];
412
- for (const product of products) {
413
- const id = product.product_id;
414
- if (typeof id === "string") into.set(id, product);
415
- }
416
- metadata = mergeFeedMetadata(metadata, body);
417
- cursor = body.pagination?.has_more ? body.pagination?.cursor : void 0;
418
- } while (cursor);
419
- return { cancelled: false, unchanged: false, items: into, metadata };
449
+ handleBootstrapFailure(failure) {
450
+ this.setState(this._lastSyncedAt ? "degraded" : "error");
451
+ this.errorHandler?.(failure.error);
452
+ if (this.listenerCount("error") > 0) this.emit("error", failure);
453
+ return false;
420
454
  }
421
- async bootstrapSignals(epoch) {
422
- let cursor;
423
- const into = /* @__PURE__ */ new Map();
424
- let metadata = this.currentSignalMetadata();
425
- do {
426
- const params = {
427
- discovery_mode: "wholesale",
428
- pagination: { max_results: DEFAULT_BOOTSTRAP_PAGE_LIMIT, ...cursor && { cursor } },
429
- ...this.account && { account: this.account }
430
- };
431
- if (!cursor && metadata.wholesaleFeedVersion && this._capabilities.wholesaleFeedVersioning) {
432
- params.if_wholesale_feed_version = metadata.wholesaleFeedVersion;
433
- if (metadata.pricingVersion) params.if_pricing_version = metadata.pricingVersion;
434
- }
435
- const result = await this.client.getSignals(params);
436
- if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
437
- const body = result.data;
438
- if (!body) return { cancelled: false, unchanged: false, items: into, metadata };
439
- if (body.unchanged) {
440
- metadata = mergeFeedMetadata(metadata, body);
441
- return { cancelled: false, unchanged: true, items: into, metadata };
455
+ bootstrapProducts(epoch, conditional) {
456
+ return this.bootstrapFeed("product", epoch, conditional);
457
+ }
458
+ bootstrapSignals(epoch, conditional) {
459
+ return this.bootstrapFeed("signal", epoch, conditional);
460
+ }
461
+ async bootstrapFeed(entity, epoch, conditional) {
462
+ const store = new InMemoryWholesaleFeedMirrorStore();
463
+ const scope = { agentUrl: "shell", accountKey: "shell", entity };
464
+ const metadata = entity === "product" ? this.currentProductMetadata() : this.currentSignalMetadata();
465
+ const previous = entity === "product" ? [...this.productIndex.values()] : [...this.signalIndex.values()];
466
+ store.restore(scope, { revision: 0, items: previous, ...metadata });
467
+ let readException;
468
+ const read = async (call) => {
469
+ try {
470
+ return await call();
471
+ } catch (cause) {
472
+ readException = cause instanceof Error ? cause : new Error(String(cause));
473
+ throw cause;
442
474
  }
443
- const signals = Array.isArray(body.signals) ? body.signals : [];
444
- for (const signal of signals) {
445
- const id = signal.signal_agent_segment_id;
446
- if (typeof id === "string") into.set(id, signal);
475
+ };
476
+ const outcome = await refreshWholesaleFeed({
477
+ client: {
478
+ getAdcpCapabilities: this.client.getAdcpCapabilities.bind(this.client),
479
+ getProducts: (params) => read(() => this.client.getProducts(params)),
480
+ getSignals: (params) => read(() => this.client.getSignals(params))
481
+ },
482
+ store,
483
+ scope,
484
+ account: this.account,
485
+ conditional: conditional ?? this._capabilities.wholesaleFeedVersioning,
486
+ isCurrent: () => this.isLifecycleCurrent(epoch)
487
+ });
488
+ if (outcome.outcome === "superseded") return { cancelled: true, unchanged: false, items: /* @__PURE__ */ new Map(), metadata };
489
+ if (outcome.outcome === "degraded") {
490
+ if (readException) throw readException;
491
+ return {
492
+ cancelled: false,
493
+ unchanged: false,
494
+ items: /* @__PURE__ */ new Map(),
495
+ metadata,
496
+ failure: {
497
+ error: new Error(
498
+ `${entity === "product" ? "Product" : "Signal"} bootstrap failed: ${outcome.cause.message}`,
499
+ {
500
+ cause: outcome.cause
501
+ }
502
+ ),
503
+ adcpError: outcome.error
504
+ }
505
+ };
506
+ }
507
+ const snapshot = outcome.snapshot;
508
+ return {
509
+ cancelled: false,
510
+ unchanged: outcome.outcome === "unchanged",
511
+ items: new Map(
512
+ snapshot.items.map((item) => [
513
+ entity === "product" ? item.product_id : item.signal_agent_segment_id,
514
+ item
515
+ ])
516
+ ),
517
+ metadata: {
518
+ wholesaleFeedVersion: snapshot.wholesaleFeedVersion,
519
+ pricingVersion: snapshot.pricingVersion,
520
+ cacheScope: snapshot.cacheScope
447
521
  }
448
- metadata = mergeFeedMetadata(metadata, body);
449
- cursor = body.pagination?.has_more ? body.pagination?.cursor : void 0;
450
- } while (cursor);
451
- return { cancelled: false, unchanged: false, items: into, metadata };
522
+ };
452
523
  }
453
524
  async recoverFromBulkChange(event, epoch = this.lifecycleEpoch) {
454
525
  this.emit("resyncing", { reason: "bulk_change" });
@@ -459,14 +530,22 @@ class WholesaleFeedSync extends EventEmitter {
459
530
  );
460
531
  }
461
532
  const entities = affected === "product" ? "products" : "signals";
462
- return this.bootstrap({ emitDiffs: true, entities, epoch });
533
+ for (let attempt = 0; attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
534
+ if (await this.bootstrap({ emitDiffs: true, entities, epoch, propagateFailure: true })) return true;
535
+ if (!this.isLifecycleCurrent(epoch)) return false;
536
+ }
537
+ throw supersededRepairError();
463
538
  }
464
539
  async recoverFromVersionMismatch(event, epoch = this.lifecycleEpoch) {
465
540
  this.emit("resyncing", { reason: "version_mismatch" });
466
541
  const beforeVersion = this.currentWholesaleFeedVersionForEvent(event);
467
542
  for (let attempt = 1; attempt <= VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
468
- const recovered = await this.bootstrap({ emitDiffs: true, epoch });
469
- if (!recovered) return false;
543
+ const recovered = await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
544
+ if (!recovered) {
545
+ if (!this.isLifecycleCurrent(epoch)) return false;
546
+ if (attempt === VERSION_MISMATCH_RECOVERY_ATTEMPTS) throw supersededRepairError();
547
+ continue;
548
+ }
470
549
  const afterVersion = this.currentWholesaleFeedVersionForEvent(event);
471
550
  if (afterVersion !== beforeVersion) return true;
472
551
  if (attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS) {
@@ -487,11 +566,12 @@ class WholesaleFeedSync extends EventEmitter {
487
566
  await this.probeVersion(epoch);
488
567
  } catch (err) {
489
568
  if (!this.isLifecycleCurrent(epoch)) return;
490
- const error = err instanceof Error ? err : new Error(String(err));
491
- this.emit("error", { error });
492
- this.errorHandler?.(error);
569
+ if (this._state !== "degraded" && this._state !== "error") {
570
+ const error = err instanceof Error ? err : new Error(String(err));
571
+ this.handleBootstrapFailure({ error });
572
+ }
493
573
  }
494
- if (this.isLifecycleCurrent(epoch) && this._state === "syncing" && this._mode === "auto-poll") {
574
+ if (this.isLifecycleCurrent(epoch) && (this._state === "syncing" || this._state === "degraded" || this._state === "error") && this._mode === "auto-poll") {
495
575
  this.scheduleProbe(epoch);
496
576
  }
497
577
  }
@@ -518,7 +598,7 @@ class WholesaleFeedSync extends EventEmitter {
518
598
  this.emit("error", { error });
519
599
  this.errorHandler?.(error);
520
600
  }
521
- if (this.isLifecycleCurrent(epoch) && this._state === "syncing" && this.capabilityRefreshIntervalMs > 0) {
601
+ if (this.isLifecycleCurrent(epoch) && (this._state === "syncing" || this._state === "degraded" || this._state === "error") && this.capabilityRefreshIntervalMs > 0) {
522
602
  this.scheduleCapabilityRefresh(epoch);
523
603
  }
524
604
  }
@@ -619,63 +699,16 @@ class WholesaleFeedSync extends EventEmitter {
619
699
  }
620
700
  }
621
701
  applyEvent(event) {
622
- switch (event.event_type) {
623
- case "product.created":
624
- case "product.updated": {
625
- const payload = event.payload;
626
- if (typeof payload.product_id !== "string") return;
627
- if (payload.product) {
628
- this.productIndex.set(payload.product_id, payload.product);
629
- return;
630
- }
631
- return;
632
- }
633
- case "product.priced": {
634
- const payload = event.payload;
635
- const existing = this.productIndex.get(payload.product_id);
636
- if (existing && Array.isArray(payload.pricing_options)) {
637
- this.productIndex.set(payload.product_id, {
638
- ...existing,
639
- pricing_options: payload.pricing_options
640
- });
641
- }
642
- return;
643
- }
644
- case "product.removed": {
645
- const payload = event.payload;
646
- if (typeof payload.product_id === "string") this.productIndex.delete(payload.product_id);
647
- return;
648
- }
649
- case "signal.created":
650
- case "signal.updated": {
651
- const payload = event.payload;
652
- if (typeof payload.signal_agent_segment_id !== "string") return;
653
- if (payload.signal) {
654
- this.signalIndex.set(payload.signal_agent_segment_id, payload.signal);
655
- }
656
- return;
657
- }
658
- case "signal.priced": {
659
- const payload = event.payload;
660
- const existing = this.signalIndex.get(payload.signal_agent_segment_id);
661
- if (existing && Array.isArray(payload.pricing_options)) {
662
- this.signalIndex.set(payload.signal_agent_segment_id, {
663
- ...existing,
664
- pricing_options: payload.pricing_options
665
- });
666
- }
667
- return;
668
- }
669
- case "signal.removed": {
670
- const payload = event.payload;
671
- if (typeof payload.signal_agent_segment_id === "string") {
672
- this.signalIndex.delete(payload.signal_agent_segment_id);
673
- }
674
- return;
675
- }
676
- case "wholesale_feed.bulk_change":
677
- return;
678
- }
702
+ ++this.mirrorRevision;
703
+ const entity = event.event_type.startsWith("product.") ? "product" : "signal";
704
+ const items = applyWholesaleFeedEvent(
705
+ entity,
706
+ entity === "product" ? [...this.productIndex.values()] : [...this.signalIndex.values()],
707
+ event
708
+ );
709
+ if (!items) return;
710
+ if (entity === "product") this.productIndex = new Map(items.map((item) => [item.product_id, item]));
711
+ else this.signalIndex = new Map(items.map((item) => [item.signal_agent_segment_id, item]));
679
712
  }
680
713
  emitTypedEvent(event) {
681
714
  switch (event.event_type) {
@@ -709,107 +742,23 @@ class WholesaleFeedSync extends EventEmitter {
709
742
  }
710
743
  // ====== Private: diff emission (auto-poll / manual refresh) ======
711
744
  emitDiffs(previousProducts, previousSignals) {
712
- const now = (/* @__PURE__ */ new Date()).toISOString();
713
- const makeEvent = (event_type, entity_type, entity_id, payload) => ({
714
- // crypto.randomUUID() emits a v4 UUID, NOT v7. Synthetic events
715
- // are flagged via `synthetic: true` on the emit envelope so
716
- // adopters writing event_ids to a dedupe table know not to
717
- // treat the ID as seller-authored. Real webhook events carry the
718
- // agent's authoritative event_id.
719
- event_id: randomUUID(),
720
- event_type,
721
- entity_type,
722
- entity_id,
723
- created_at: now,
724
- payload
725
- });
726
- const emit = (channel, event) => {
745
+ const events = [
746
+ ...diffWholesaleFeed(
747
+ "product",
748
+ [...previousProducts.values()],
749
+ [...this.productIndex.values()],
750
+ this.productCacheScope
751
+ ),
752
+ ...diffWholesaleFeed(
753
+ "signal",
754
+ [...previousSignals.values()],
755
+ [...this.signalIndex.values()],
756
+ this.signalCacheScope
757
+ )
758
+ ];
759
+ for (const event of events) {
727
760
  this.emit("event", { event, synthetic: true });
728
- this.emit(channel, { event, synthetic: true });
729
- };
730
- for (const [id, product] of this.productIndex) {
731
- const prev = previousProducts.get(id);
732
- if (!prev) {
733
- emit(
734
- "product.created",
735
- makeEvent("product.created", "product", id, {
736
- product_id: id,
737
- product,
738
- applies_to: { scope: this.productCacheScope }
739
- })
740
- );
741
- } else if (priceChanged(prev, product)) {
742
- emit(
743
- "product.priced",
744
- makeEvent("product.priced", "product", id, {
745
- product_id: id,
746
- pricing_options: product.pricing_options ?? [],
747
- applies_to: { scope: this.productCacheScope }
748
- })
749
- );
750
- } else if (!isDeepStrictEqual(prev, product)) {
751
- emit(
752
- "product.updated",
753
- makeEvent("product.updated", "product", id, {
754
- product_id: id,
755
- product,
756
- applies_to: { scope: this.productCacheScope }
757
- })
758
- );
759
- }
760
- }
761
- for (const [id] of previousProducts) {
762
- if (!this.productIndex.has(id)) {
763
- emit(
764
- "product.removed",
765
- makeEvent("product.removed", "product", id, {
766
- product_id: id,
767
- applies_to: { scope: this.productCacheScope }
768
- })
769
- );
770
- }
771
- }
772
- for (const [id, signal] of this.signalIndex) {
773
- const prev = previousSignals.get(id);
774
- if (!prev) {
775
- emit(
776
- "signal.created",
777
- makeEvent("signal.created", "signal", id, {
778
- signal_agent_segment_id: id,
779
- signal,
780
- applies_to: { scope: this.signalCacheScope }
781
- })
782
- );
783
- } else if (signalPriceChanged(prev, signal)) {
784
- emit(
785
- "signal.priced",
786
- makeEvent("signal.priced", "signal", id, {
787
- signal_agent_segment_id: id,
788
- pricing_options: signal.pricing_options ?? [],
789
- applies_to: { scope: this.signalCacheScope }
790
- })
791
- );
792
- } else if (!isDeepStrictEqual(prev, signal)) {
793
- emit(
794
- "signal.updated",
795
- makeEvent("signal.updated", "signal", id, {
796
- signal_agent_segment_id: id,
797
- signal,
798
- applies_to: { scope: this.signalCacheScope }
799
- })
800
- );
801
- }
802
- }
803
- for (const [id] of previousSignals) {
804
- if (!this.signalIndex.has(id)) {
805
- emit(
806
- "signal.removed",
807
- makeEvent("signal.removed", "signal", id, {
808
- signal_agent_segment_id: id,
809
- applies_to: { scope: this.signalCacheScope }
810
- })
811
- );
812
- }
761
+ this.emit(event.event_type, { event, synthetic: true });
813
762
  }
814
763
  }
815
764
  // ====== Private: search ======
@@ -986,16 +935,6 @@ function assertOptionalDate(value, field, invalid) {
986
935
  function parsePersistedDate(value) {
987
936
  return value === void 0 ? void 0 : new Date(value);
988
937
  }
989
- function priceChanged(prev, next) {
990
- const a = prev.pricing_options;
991
- const b = next.pricing_options;
992
- return !isDeepStrictEqual(a, b);
993
- }
994
- function signalPriceChanged(prev, next) {
995
- const a = prev.pricing_options;
996
- const b = next.pricing_options;
997
- return !isDeepStrictEqual(a, b);
998
- }
999
938
  function isUuidV7(value) {
1000
939
  return /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
1001
940
  }
@@ -1005,13 +944,6 @@ function compareUuidV7(a, b) {
1005
944
  const normalizedB = b.toLowerCase();
1006
945
  return normalizedA === normalizedB ? 0 : normalizedA > normalizedB ? 1 : -1;
1007
946
  }
1008
- function mergeFeedMetadata(current, body) {
1009
- return {
1010
- wholesaleFeedVersion: typeof body.wholesale_feed_version === "string" ? body.wholesale_feed_version : current.wholesaleFeedVersion,
1011
- pricingVersion: typeof body.pricing_version === "string" ? body.pricing_version : current.pricingVersion,
1012
- cacheScope: body.cache_scope === "public" || body.cache_scope === "account" ? body.cache_scope : current.cacheScope
1013
- };
1014
- }
1015
947
  async function sleep(ms) {
1016
948
  await new Promise((resolve) => setTimeout(resolve, ms));
1017
949
  }