@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
@@ -23,12 +23,17 @@ __export(sync_exports, {
23
23
  module.exports = __toCommonJS(sync_exports);
24
24
  var import_node_events = require("node:events");
25
25
  var import_node_util = require("node:util");
26
- var import_node_crypto = require("node:crypto");
26
+ var import_mirror = require('./mirror.js');
27
27
  var import_webhook_notification = require('./webhook-notification.js');
28
+ const bootstrapReadFailures = /* @__PURE__ */ new WeakSet();
29
+ function supersededRepairError() {
30
+ const error = new Error("WholesaleFeedSync: repair superseded repeatedly; retry webhook delivery.");
31
+ bootstrapReadFailures.add(error);
32
+ return error;
33
+ }
28
34
  const DEFAULT_PROBE_INTERVAL_MS = 6e5;
29
35
  const DEFAULT_CAPABILITY_REFRESH_INTERVAL_MS = 864e5;
30
36
  const DEFAULT_PERSISTENCE_TIMEOUT_MS = 3e4;
31
- const DEFAULT_BOOTSTRAP_PAGE_LIMIT = 100;
32
37
  const VERSION_MISMATCH_RECOVERY_ATTEMPTS = 3;
33
38
  const VERSION_MISMATCH_RECOVERY_BACKOFF_MS = 5;
34
39
  class WholesaleFeedSync extends import_node_events.EventEmitter {
@@ -69,11 +74,10 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
69
74
  probeTimer = null;
70
75
  capabilityTimer = null;
71
76
  lifecycleEpoch = 0;
77
+ mirrorRevision = 0;
72
78
  /**
73
- * Read-only view of the in-memory product index. The `mode` reflects the
74
- * sync strategy for product events specifically — in mixed-capability
75
- * agents (products-feed + signals-wholesale-only, or vice versa) this
76
- * may differ from `signals.mode`.
79
+ * Product index view. `mode` identifies the product sync strategy and may
80
+ * differ from `signals.mode` for agents with mixed feed capabilities.
77
81
  */
78
82
  products = {
79
83
  list: () => [...this.productIndex.values()],
@@ -108,19 +112,12 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
108
112
  },
109
113
  _mode: "manual",
110
114
  /**
111
- * `true` when the agent supports `discovery_mode: 'wholesale'` on
112
- * `get_signals` (i.e., signals are browsable). When `false`, the
113
- * agent only supports brief-mode discovery — `signals.list()` will
114
- * be empty until adopters call into the agent with their own briefs.
115
+ * True when `get_signals` supports `discovery_mode: 'wholesale'`.
116
+ * Otherwise `list()` is empty; use client brief discovery instead.
115
117
  */
116
118
  queryable: true
117
119
  };
118
- /**
119
- * One-shot console warning when adopters call `signals.list()` or
120
- * `signals.search()` against an agent that doesn't support wholesale
121
- * signal enumeration. Without this, empty results read as "no signals
122
- * match" rather than "the agent doesn't browse, only briefs."
123
- */
120
+ // Warn once when empty signal results reflect missing wholesale support.
124
121
  warnIfSignalsNotQueryable() {
125
122
  if (this.signals.queryable || this.signalsQueryableWarned) return;
126
123
  if (this._state === "idle" || this._state === "bootstrapping") return;
@@ -146,14 +143,9 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
146
143
  }
147
144
  // ====== Lifecycle ======
148
145
  /**
149
- * Probe the agent's capabilities, pick a sync mode, and bootstrap the
150
- * in-memory replica via wholesale enumeration. In `'auto-poll'` mode
151
- * starts the conditional wholesale-feed version probe loop.
152
- *
153
- * Safe to call repeatedly — concurrent calls await the in-flight
154
- * bootstrap and return when it completes (no duplicate bootstrap, no
155
- * silent drop). Sequential calls re-probe capabilities and re-bootstrap,
156
- * equivalent to calling `refresh()` after a mode upgrade.
146
+ * Discover capabilities, bootstrap the mirror, and schedule auto-poll probes
147
+ * for versioned sellers. Concurrent calls share the bootstrap; sequential
148
+ * calls re-probe capabilities and refresh the mirror.
157
149
  */
158
150
  async start() {
159
151
  if (this.startPromise && this.startPromiseEpoch === this.lifecycleEpoch) return this.startPromise;
@@ -172,12 +164,13 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
172
164
  const epoch = this.lifecycleEpoch;
173
165
  if (!await this.restorePersistedState(epoch)) return;
174
166
  if (!await this.resolveMode(epoch)) return;
175
- if (!await this.bootstrap({ epoch })) return;
176
- if (this._mode === "auto-poll") {
177
- this.scheduleProbe(epoch);
178
- }
179
- if (this.capabilityRefreshIntervalMs > 0) {
180
- this.scheduleCapabilityRefresh(epoch);
167
+ try {
168
+ await this.bootstrapWithRetries({ epoch });
169
+ } finally {
170
+ if (this.isLifecycleCurrent(epoch)) {
171
+ if (this._mode === "auto-poll") this.scheduleProbe(epoch);
172
+ if (this.capabilityRefreshIntervalMs > 0) this.scheduleCapabilityRefresh(epoch);
173
+ }
181
174
  }
182
175
  }
183
176
  /** Stop all background activity. Preserves in-memory state and version tokens. */
@@ -187,7 +180,8 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
187
180
  if (this.capabilityTimer) clearTimeout(this.capabilityTimer);
188
181
  this.probeTimer = null;
189
182
  this.capabilityTimer = null;
190
- if (this._state === "syncing" || this._state === "bootstrapping") this.setState("idle");
183
+ if (this._state === "syncing" || this._state === "bootstrapping" || this._state === "degraded")
184
+ this.setState("idle");
191
185
  }
192
186
  /**
193
187
  * Stop, clear all indexes and version tokens. Call `start()` again to
@@ -195,6 +189,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
195
189
  */
196
190
  async reset() {
197
191
  this.stop();
192
+ ++this.mirrorRevision;
198
193
  this.productIndex.clear();
199
194
  this.signalIndex.clear();
200
195
  this.productWholesaleFeedVersion = void 0;
@@ -220,7 +215,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
220
215
  async refresh() {
221
216
  const epoch = this.lifecycleEpoch;
222
217
  this.emit("resyncing", { reason: "manual" });
223
- await this.bootstrap({ emitDiffs: true, epoch });
218
+ await this.bootstrapWithRetries({ emitDiffs: true, epoch, propagateFailure: true });
224
219
  }
225
220
  /**
226
221
  * Apply one legacy-view account-level wholesale feed webhook to the local mirror.
@@ -269,6 +264,11 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
269
264
  this.rememberLastWebhookEventId(event.event_id);
270
265
  throw err;
271
266
  }
267
+ if (!this._lastSyncedAt && event.event_type !== "wholesale_feed.bulk_change") {
268
+ if (!await this.recoverFromVersionMismatch(event, epoch)) return;
269
+ await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
270
+ return;
271
+ }
272
272
  if (webhook.previous_wholesale_feed_version && currentVersion && webhook.previous_wholesale_feed_version !== currentVersion) {
273
273
  if (!await this.recoverFromVersionMismatch(event, epoch)) return;
274
274
  await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
@@ -279,8 +279,10 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
279
279
  try {
280
280
  if (!await this.recoverFromBulkChange(event, epoch)) return;
281
281
  } catch (err) {
282
- await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
283
- this.rememberLastWebhookEventId(event.event_id);
282
+ if (!(err instanceof Error && bootstrapReadFailures.has(err))) {
283
+ await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
284
+ this.rememberLastWebhookEventId(event.event_id);
285
+ }
284
286
  throw err;
285
287
  }
286
288
  await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
@@ -347,10 +349,24 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
347
349
  return true;
348
350
  }
349
351
  // ====== Private: bootstrap (wholesale enumeration) ======
352
+ async bootstrapWithRetries(options) {
353
+ for (let attempt = 0; attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
354
+ const revision = this.mirrorRevision;
355
+ if (await this.bootstrap({ ...options, conditional: attempt === 0 ? void 0 : false })) return;
356
+ if (!this.isLifecycleCurrent(options.epoch) || revision === this.mirrorRevision) return;
357
+ }
358
+ const error = new Error("WholesaleFeedSync: refresh superseded repeatedly; retry start or refresh.");
359
+ this.handleBootstrapFailure({ error });
360
+ throw error;
361
+ }
350
362
  async bootstrap(options = {}) {
351
363
  const epoch = options.epoch ?? this.lifecycleEpoch;
352
364
  if (!this.isLifecycleCurrent(epoch)) return false;
353
365
  this.setState("bootstrapping");
366
+ const previousLastSyncedAt = this._lastSyncedAt;
367
+ const expectedRevision = this.mirrorRevision;
368
+ let reportedFailure;
369
+ let committed = false;
354
370
  try {
355
371
  const previousProducts = new Map(this.productIndex);
356
372
  const previousSignals = new Map(this.signalIndex);
@@ -362,14 +378,50 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
362
378
  const refreshProducts = entities !== "signals";
363
379
  const refreshSignals = entities !== "products" && this.signals.queryable;
364
380
  if (refreshProducts) {
365
- productResult = await this.bootstrapProducts(epoch);
381
+ productResult = await this.bootstrapProducts(epoch, options.conditional);
366
382
  if (productResult.cancelled) return false;
383
+ if (this.mirrorRevision !== expectedRevision) {
384
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
385
+ return false;
386
+ }
387
+ if (productResult.failure) {
388
+ const failure = productResult.failure;
389
+ reportedFailure = failure.error;
390
+ this.handleBootstrapFailure(failure);
391
+ if (options.propagateFailure) {
392
+ bootstrapReadFailures.add(failure.error);
393
+ if (failure.adcpError) Object.assign(failure.error, { adcpError: failure.adcpError });
394
+ throw failure.error;
395
+ }
396
+ return false;
397
+ }
367
398
  }
368
399
  if (refreshSignals) {
369
- signalResult = await this.bootstrapSignals(epoch);
400
+ signalResult = await this.bootstrapSignals(epoch, options.conditional);
370
401
  if (signalResult.cancelled) return false;
402
+ if (this.mirrorRevision !== expectedRevision) {
403
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
404
+ return false;
405
+ }
406
+ if (signalResult.failure) {
407
+ const failure = signalResult.failure;
408
+ reportedFailure = failure.error;
409
+ this.handleBootstrapFailure(failure);
410
+ if (options.propagateFailure) {
411
+ bootstrapReadFailures.add(failure.error);
412
+ if (failure.adcpError) Object.assign(failure.error, { adcpError: failure.adcpError });
413
+ throw failure.error;
414
+ }
415
+ return false;
416
+ }
371
417
  }
372
418
  if (!this.isLifecycleCurrent(epoch)) return false;
419
+ if (this.mirrorRevision !== expectedRevision) {
420
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
421
+ return false;
422
+ }
423
+ ++this.mirrorRevision;
424
+ committed = true;
373
425
  if (refreshProducts && productResult) {
374
426
  this.commitProductMetadata(productResult.metadata);
375
427
  if (!productResult.unchanged) {
@@ -401,77 +453,91 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
401
453
  return true;
402
454
  } catch (err) {
403
455
  if (!this.isLifecycleCurrent(epoch)) return false;
404
- this.setState("error");
456
+ if (!committed && this.mirrorRevision !== expectedRevision && err !== reportedFailure) return false;
457
+ if (err === reportedFailure) throw err;
458
+ this._lastSyncedAt = previousLastSyncedAt;
459
+ this.setState(this._lastSyncedAt ? "degraded" : "error");
405
460
  const error = err instanceof Error ? err : new Error(String(err));
406
461
  this.errorHandler?.(error);
407
- this.emit("error", { error });
462
+ if (this.listenerCount("error") > 0) this.emit("error", { error });
463
+ if (options.propagateFailure) bootstrapReadFailures.add(error);
408
464
  throw error;
409
465
  }
410
466
  }
411
- /** Returns `true` when the seller short-circuited with `unchanged: true`. */
412
- async bootstrapProducts(epoch) {
413
- let cursor;
414
- const into = /* @__PURE__ */ new Map();
415
- let metadata = this.currentProductMetadata();
416
- do {
417
- const params = {
418
- buying_mode: "wholesale",
419
- pagination: { max_results: DEFAULT_BOOTSTRAP_PAGE_LIMIT, ...cursor && { cursor } },
420
- ...this.account && { account: this.account }
421
- };
422
- if (!cursor && metadata.wholesaleFeedVersion && this._capabilities.wholesaleFeedVersioning) {
423
- params.if_wholesale_feed_version = metadata.wholesaleFeedVersion;
424
- if (metadata.pricingVersion) params.if_pricing_version = metadata.pricingVersion;
425
- }
426
- const result = await this.client.getProducts(params);
427
- if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
428
- const body = result.data;
429
- if (!body) return { cancelled: false, unchanged: false, items: into, metadata };
430
- if (body.unchanged) {
431
- metadata = mergeFeedMetadata(metadata, body);
432
- return { cancelled: false, unchanged: true, items: into, metadata };
433
- }
434
- const products = Array.isArray(body.products) ? body.products : [];
435
- for (const product of products) {
436
- const id = product.product_id;
437
- if (typeof id === "string") into.set(id, product);
438
- }
439
- metadata = mergeFeedMetadata(metadata, body);
440
- cursor = body.pagination?.has_more ? body.pagination?.cursor : void 0;
441
- } while (cursor);
442
- return { cancelled: false, unchanged: false, items: into, metadata };
467
+ handleBootstrapFailure(failure) {
468
+ this.setState(this._lastSyncedAt ? "degraded" : "error");
469
+ this.errorHandler?.(failure.error);
470
+ if (this.listenerCount("error") > 0) this.emit("error", failure);
471
+ return false;
443
472
  }
444
- async bootstrapSignals(epoch) {
445
- let cursor;
446
- const into = /* @__PURE__ */ new Map();
447
- let metadata = this.currentSignalMetadata();
448
- do {
449
- const params = {
450
- discovery_mode: "wholesale",
451
- pagination: { max_results: DEFAULT_BOOTSTRAP_PAGE_LIMIT, ...cursor && { cursor } },
452
- ...this.account && { account: this.account }
453
- };
454
- if (!cursor && metadata.wholesaleFeedVersion && this._capabilities.wholesaleFeedVersioning) {
455
- params.if_wholesale_feed_version = metadata.wholesaleFeedVersion;
456
- if (metadata.pricingVersion) params.if_pricing_version = metadata.pricingVersion;
457
- }
458
- const result = await this.client.getSignals(params);
459
- if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
460
- const body = result.data;
461
- if (!body) return { cancelled: false, unchanged: false, items: into, metadata };
462
- if (body.unchanged) {
463
- metadata = mergeFeedMetadata(metadata, body);
464
- return { cancelled: false, unchanged: true, items: into, metadata };
473
+ bootstrapProducts(epoch, conditional) {
474
+ return this.bootstrapFeed("product", epoch, conditional);
475
+ }
476
+ bootstrapSignals(epoch, conditional) {
477
+ return this.bootstrapFeed("signal", epoch, conditional);
478
+ }
479
+ async bootstrapFeed(entity, epoch, conditional) {
480
+ const store = new import_mirror.InMemoryWholesaleFeedMirrorStore();
481
+ const scope = { agentUrl: "shell", accountKey: "shell", entity };
482
+ const metadata = entity === "product" ? this.currentProductMetadata() : this.currentSignalMetadata();
483
+ const previous = entity === "product" ? [...this.productIndex.values()] : [...this.signalIndex.values()];
484
+ store.restore(scope, { revision: 0, items: previous, ...metadata });
485
+ let readException;
486
+ const read = async (call) => {
487
+ try {
488
+ return await call();
489
+ } catch (cause) {
490
+ readException = cause instanceof Error ? cause : new Error(String(cause));
491
+ throw cause;
465
492
  }
466
- const signals = Array.isArray(body.signals) ? body.signals : [];
467
- for (const signal of signals) {
468
- const id = signal.signal_agent_segment_id;
469
- if (typeof id === "string") into.set(id, signal);
493
+ };
494
+ const outcome = await (0, import_mirror.refreshWholesaleFeed)({
495
+ client: {
496
+ getAdcpCapabilities: this.client.getAdcpCapabilities.bind(this.client),
497
+ getProducts: (params) => read(() => this.client.getProducts(params)),
498
+ getSignals: (params) => read(() => this.client.getSignals(params))
499
+ },
500
+ store,
501
+ scope,
502
+ account: this.account,
503
+ conditional: conditional ?? this._capabilities.wholesaleFeedVersioning,
504
+ isCurrent: () => this.isLifecycleCurrent(epoch)
505
+ });
506
+ if (outcome.outcome === "superseded") return { cancelled: true, unchanged: false, items: /* @__PURE__ */ new Map(), metadata };
507
+ if (outcome.outcome === "degraded") {
508
+ if (readException) throw readException;
509
+ return {
510
+ cancelled: false,
511
+ unchanged: false,
512
+ items: /* @__PURE__ */ new Map(),
513
+ metadata,
514
+ failure: {
515
+ error: new Error(
516
+ `${entity === "product" ? "Product" : "Signal"} bootstrap failed: ${outcome.cause.message}`,
517
+ {
518
+ cause: outcome.cause
519
+ }
520
+ ),
521
+ adcpError: outcome.error
522
+ }
523
+ };
524
+ }
525
+ const snapshot = outcome.snapshot;
526
+ return {
527
+ cancelled: false,
528
+ unchanged: outcome.outcome === "unchanged",
529
+ items: new Map(
530
+ snapshot.items.map((item) => [
531
+ entity === "product" ? item.product_id : item.signal_agent_segment_id,
532
+ item
533
+ ])
534
+ ),
535
+ metadata: {
536
+ wholesaleFeedVersion: snapshot.wholesaleFeedVersion,
537
+ pricingVersion: snapshot.pricingVersion,
538
+ cacheScope: snapshot.cacheScope
470
539
  }
471
- metadata = mergeFeedMetadata(metadata, body);
472
- cursor = body.pagination?.has_more ? body.pagination?.cursor : void 0;
473
- } while (cursor);
474
- return { cancelled: false, unchanged: false, items: into, metadata };
540
+ };
475
541
  }
476
542
  async recoverFromBulkChange(event, epoch = this.lifecycleEpoch) {
477
543
  this.emit("resyncing", { reason: "bulk_change" });
@@ -482,14 +548,22 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
482
548
  );
483
549
  }
484
550
  const entities = affected === "product" ? "products" : "signals";
485
- return this.bootstrap({ emitDiffs: true, entities, epoch });
551
+ for (let attempt = 0; attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
552
+ if (await this.bootstrap({ emitDiffs: true, entities, epoch, propagateFailure: true })) return true;
553
+ if (!this.isLifecycleCurrent(epoch)) return false;
554
+ }
555
+ throw supersededRepairError();
486
556
  }
487
557
  async recoverFromVersionMismatch(event, epoch = this.lifecycleEpoch) {
488
558
  this.emit("resyncing", { reason: "version_mismatch" });
489
559
  const beforeVersion = this.currentWholesaleFeedVersionForEvent(event);
490
560
  for (let attempt = 1; attempt <= VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
491
- const recovered = await this.bootstrap({ emitDiffs: true, epoch });
492
- if (!recovered) return false;
561
+ const recovered = await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
562
+ if (!recovered) {
563
+ if (!this.isLifecycleCurrent(epoch)) return false;
564
+ if (attempt === VERSION_MISMATCH_RECOVERY_ATTEMPTS) throw supersededRepairError();
565
+ continue;
566
+ }
493
567
  const afterVersion = this.currentWholesaleFeedVersionForEvent(event);
494
568
  if (afterVersion !== beforeVersion) return true;
495
569
  if (attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS) {
@@ -510,11 +584,12 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
510
584
  await this.probeVersion(epoch);
511
585
  } catch (err) {
512
586
  if (!this.isLifecycleCurrent(epoch)) return;
513
- const error = err instanceof Error ? err : new Error(String(err));
514
- this.emit("error", { error });
515
- this.errorHandler?.(error);
587
+ if (this._state !== "degraded" && this._state !== "error") {
588
+ const error = err instanceof Error ? err : new Error(String(err));
589
+ this.handleBootstrapFailure({ error });
590
+ }
516
591
  }
517
- if (this.isLifecycleCurrent(epoch) && this._state === "syncing" && this._mode === "auto-poll") {
592
+ if (this.isLifecycleCurrent(epoch) && (this._state === "syncing" || this._state === "degraded" || this._state === "error") && this._mode === "auto-poll") {
518
593
  this.scheduleProbe(epoch);
519
594
  }
520
595
  }
@@ -541,7 +616,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
541
616
  this.emit("error", { error });
542
617
  this.errorHandler?.(error);
543
618
  }
544
- if (this.isLifecycleCurrent(epoch) && this._state === "syncing" && this.capabilityRefreshIntervalMs > 0) {
619
+ if (this.isLifecycleCurrent(epoch) && (this._state === "syncing" || this._state === "degraded" || this._state === "error") && this.capabilityRefreshIntervalMs > 0) {
545
620
  this.scheduleCapabilityRefresh(epoch);
546
621
  }
547
622
  }
@@ -642,63 +717,16 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
642
717
  }
643
718
  }
644
719
  applyEvent(event) {
645
- switch (event.event_type) {
646
- case "product.created":
647
- case "product.updated": {
648
- const payload = event.payload;
649
- if (typeof payload.product_id !== "string") return;
650
- if (payload.product) {
651
- this.productIndex.set(payload.product_id, payload.product);
652
- return;
653
- }
654
- return;
655
- }
656
- case "product.priced": {
657
- const payload = event.payload;
658
- const existing = this.productIndex.get(payload.product_id);
659
- if (existing && Array.isArray(payload.pricing_options)) {
660
- this.productIndex.set(payload.product_id, {
661
- ...existing,
662
- pricing_options: payload.pricing_options
663
- });
664
- }
665
- return;
666
- }
667
- case "product.removed": {
668
- const payload = event.payload;
669
- if (typeof payload.product_id === "string") this.productIndex.delete(payload.product_id);
670
- return;
671
- }
672
- case "signal.created":
673
- case "signal.updated": {
674
- const payload = event.payload;
675
- if (typeof payload.signal_agent_segment_id !== "string") return;
676
- if (payload.signal) {
677
- this.signalIndex.set(payload.signal_agent_segment_id, payload.signal);
678
- }
679
- return;
680
- }
681
- case "signal.priced": {
682
- const payload = event.payload;
683
- const existing = this.signalIndex.get(payload.signal_agent_segment_id);
684
- if (existing && Array.isArray(payload.pricing_options)) {
685
- this.signalIndex.set(payload.signal_agent_segment_id, {
686
- ...existing,
687
- pricing_options: payload.pricing_options
688
- });
689
- }
690
- return;
691
- }
692
- case "signal.removed": {
693
- const payload = event.payload;
694
- if (typeof payload.signal_agent_segment_id === "string") {
695
- this.signalIndex.delete(payload.signal_agent_segment_id);
696
- }
697
- return;
698
- }
699
- case "wholesale_feed.bulk_change":
700
- return;
701
- }
720
+ ++this.mirrorRevision;
721
+ const entity = event.event_type.startsWith("product.") ? "product" : "signal";
722
+ const items = (0, import_mirror.applyWholesaleFeedEvent)(
723
+ entity,
724
+ entity === "product" ? [...this.productIndex.values()] : [...this.signalIndex.values()],
725
+ event
726
+ );
727
+ if (!items) return;
728
+ if (entity === "product") this.productIndex = new Map(items.map((item) => [item.product_id, item]));
729
+ else this.signalIndex = new Map(items.map((item) => [item.signal_agent_segment_id, item]));
702
730
  }
703
731
  emitTypedEvent(event) {
704
732
  switch (event.event_type) {
@@ -732,107 +760,23 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
732
760
  }
733
761
  // ====== Private: diff emission (auto-poll / manual refresh) ======
734
762
  emitDiffs(previousProducts, previousSignals) {
735
- const now = (/* @__PURE__ */ new Date()).toISOString();
736
- const makeEvent = (event_type, entity_type, entity_id, payload) => ({
737
- // crypto.randomUUID() emits a v4 UUID, NOT v7. Synthetic events
738
- // are flagged via `synthetic: true` on the emit envelope so
739
- // adopters writing event_ids to a dedupe table know not to
740
- // treat the ID as seller-authored. Real webhook events carry the
741
- // agent's authoritative event_id.
742
- event_id: (0, import_node_crypto.randomUUID)(),
743
- event_type,
744
- entity_type,
745
- entity_id,
746
- created_at: now,
747
- payload
748
- });
749
- const emit = (channel, event) => {
763
+ const events = [
764
+ ...(0, import_mirror.diffWholesaleFeed)(
765
+ "product",
766
+ [...previousProducts.values()],
767
+ [...this.productIndex.values()],
768
+ this.productCacheScope
769
+ ),
770
+ ...(0, import_mirror.diffWholesaleFeed)(
771
+ "signal",
772
+ [...previousSignals.values()],
773
+ [...this.signalIndex.values()],
774
+ this.signalCacheScope
775
+ )
776
+ ];
777
+ for (const event of events) {
750
778
  this.emit("event", { event, synthetic: true });
751
- this.emit(channel, { event, synthetic: true });
752
- };
753
- for (const [id, product] of this.productIndex) {
754
- const prev = previousProducts.get(id);
755
- if (!prev) {
756
- emit(
757
- "product.created",
758
- makeEvent("product.created", "product", id, {
759
- product_id: id,
760
- product,
761
- applies_to: { scope: this.productCacheScope }
762
- })
763
- );
764
- } else if (priceChanged(prev, product)) {
765
- emit(
766
- "product.priced",
767
- makeEvent("product.priced", "product", id, {
768
- product_id: id,
769
- pricing_options: product.pricing_options ?? [],
770
- applies_to: { scope: this.productCacheScope }
771
- })
772
- );
773
- } else if (!(0, import_node_util.isDeepStrictEqual)(prev, product)) {
774
- emit(
775
- "product.updated",
776
- makeEvent("product.updated", "product", id, {
777
- product_id: id,
778
- product,
779
- applies_to: { scope: this.productCacheScope }
780
- })
781
- );
782
- }
783
- }
784
- for (const [id] of previousProducts) {
785
- if (!this.productIndex.has(id)) {
786
- emit(
787
- "product.removed",
788
- makeEvent("product.removed", "product", id, {
789
- product_id: id,
790
- applies_to: { scope: this.productCacheScope }
791
- })
792
- );
793
- }
794
- }
795
- for (const [id, signal] of this.signalIndex) {
796
- const prev = previousSignals.get(id);
797
- if (!prev) {
798
- emit(
799
- "signal.created",
800
- makeEvent("signal.created", "signal", id, {
801
- signal_agent_segment_id: id,
802
- signal,
803
- applies_to: { scope: this.signalCacheScope }
804
- })
805
- );
806
- } else if (signalPriceChanged(prev, signal)) {
807
- emit(
808
- "signal.priced",
809
- makeEvent("signal.priced", "signal", id, {
810
- signal_agent_segment_id: id,
811
- pricing_options: signal.pricing_options ?? [],
812
- applies_to: { scope: this.signalCacheScope }
813
- })
814
- );
815
- } else if (!(0, import_node_util.isDeepStrictEqual)(prev, signal)) {
816
- emit(
817
- "signal.updated",
818
- makeEvent("signal.updated", "signal", id, {
819
- signal_agent_segment_id: id,
820
- signal,
821
- applies_to: { scope: this.signalCacheScope }
822
- })
823
- );
824
- }
825
- }
826
- for (const [id] of previousSignals) {
827
- if (!this.signalIndex.has(id)) {
828
- emit(
829
- "signal.removed",
830
- makeEvent("signal.removed", "signal", id, {
831
- signal_agent_segment_id: id,
832
- applies_to: { scope: this.signalCacheScope }
833
- })
834
- );
835
- }
779
+ this.emit(event.event_type, { event, synthetic: true });
836
780
  }
837
781
  }
838
782
  // ====== Private: search ======
@@ -1009,16 +953,6 @@ function assertOptionalDate(value, field, invalid) {
1009
953
  function parsePersistedDate(value) {
1010
954
  return value === void 0 ? void 0 : new Date(value);
1011
955
  }
1012
- function priceChanged(prev, next) {
1013
- const a = prev.pricing_options;
1014
- const b = next.pricing_options;
1015
- return !(0, import_node_util.isDeepStrictEqual)(a, b);
1016
- }
1017
- function signalPriceChanged(prev, next) {
1018
- const a = prev.pricing_options;
1019
- const b = next.pricing_options;
1020
- return !(0, import_node_util.isDeepStrictEqual)(a, b);
1021
- }
1022
956
  function isUuidV7(value) {
1023
957
  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);
1024
958
  }
@@ -1028,13 +962,6 @@ function compareUuidV7(a, b) {
1028
962
  const normalizedB = b.toLowerCase();
1029
963
  return normalizedA === normalizedB ? 0 : normalizedA > normalizedB ? 1 : -1;
1030
964
  }
1031
- function mergeFeedMetadata(current, body) {
1032
- return {
1033
- wholesaleFeedVersion: typeof body.wholesale_feed_version === "string" ? body.wholesale_feed_version : current.wholesaleFeedVersion,
1034
- pricingVersion: typeof body.pricing_version === "string" ? body.pricing_version : current.pricingVersion,
1035
- cacheScope: body.cache_scope === "public" || body.cache_scope === "account" ? body.cache_scope : current.cacheScope
1036
- };
1037
- }
1038
965
  async function sleep(ms) {
1039
966
  await new Promise((resolve) => setTimeout(resolve, ms));
1040
967
  }