@adcp/sdk 14.1.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 (106) hide show
  1. package/dist/lib/core/SingleAgentClient.d.mts +4 -1
  2. package/dist/lib/core/SingleAgentClient.d.ts +4 -1
  3. package/dist/lib/core/SingleAgentClient.js +25 -4
  4. package/dist/lib/core/SingleAgentClient.mjs +25 -4
  5. package/dist/lib/core/TaskExecutor.d.mts +3 -1
  6. package/dist/lib/core/TaskExecutor.d.ts +3 -1
  7. package/dist/lib/core/TaskExecutor.js +15 -11
  8. package/dist/lib/core/TaskExecutor.mjs +15 -11
  9. package/dist/lib/core/buyer-account-registry.d.mts +30 -2
  10. package/dist/lib/core/buyer-account-registry.d.ts +30 -2
  11. package/dist/lib/core/buyer-account-registry.js +126 -42
  12. package/dist/lib/core/buyer-account-registry.mjs +127 -43
  13. package/dist/lib/index.d.mts +3 -2
  14. package/dist/lib/index.d.ts +3 -2
  15. package/dist/lib/index.js +9 -0
  16. package/dist/lib/index.mjs +12 -1
  17. package/dist/lib/net/agent-transport-fetch.d.mts +4 -0
  18. package/dist/lib/net/agent-transport-fetch.d.ts +4 -0
  19. package/dist/lib/net/agent-transport-fetch.js +18 -5
  20. package/dist/lib/net/agent-transport-fetch.mjs +17 -5
  21. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  22. package/dist/lib/signing/agent-resolver/errors.d.mts +2 -0
  23. package/dist/lib/signing/agent-resolver/errors.d.ts +2 -0
  24. package/dist/lib/signing/agent-resolver/errors.js +6 -0
  25. package/dist/lib/signing/agent-resolver/errors.mjs +6 -0
  26. package/dist/lib/signing/agent-resolver/fetch-helpers.d.mts +9 -0
  27. package/dist/lib/signing/agent-resolver/fetch-helpers.d.ts +9 -0
  28. package/dist/lib/signing/agent-resolver/fetch-helpers.js +35 -1
  29. package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +38 -2
  30. package/dist/lib/signing/agent-resolver/legacy-brand.js +1 -1
  31. package/dist/lib/signing/agent-resolver/legacy-brand.mjs +2 -2
  32. package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +1 -1
  33. package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +1 -1
  34. package/dist/lib/signing/agent-resolver/resolve-agent.js +47 -5
  35. package/dist/lib/signing/agent-resolver/resolve-agent.mjs +55 -6
  36. package/dist/lib/signing/brand-jwks.d.mts +2 -0
  37. package/dist/lib/signing/brand-jwks.d.ts +2 -0
  38. package/dist/lib/signing/brand-jwks.js +10 -2
  39. package/dist/lib/signing/brand-jwks.mjs +10 -2
  40. package/dist/lib/signing/errors.d.mts +4 -2
  41. package/dist/lib/signing/errors.d.ts +4 -2
  42. package/dist/lib/signing/errors.js +3 -1
  43. package/dist/lib/signing/errors.mjs +3 -1
  44. package/dist/lib/signing/verifier.js +1 -1
  45. package/dist/lib/signing/verifier.mjs +1 -1
  46. package/dist/lib/signing/webhook-verifier.js +2 -2
  47. package/dist/lib/signing/webhook-verifier.mjs +3 -3
  48. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  49. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  50. package/dist/lib/types/accept-proposal.d.ts +19 -1
  51. package/dist/lib/types/buy-products.d.ts +19 -1
  52. package/dist/lib/types/check-governance.d.ts +19 -1
  53. package/dist/lib/types/comply-test-controller.d.ts +19 -1
  54. package/dist/lib/types/control-media-buy.d.ts +19 -1
  55. package/dist/lib/types/core.generated.d.mts +14 -1
  56. package/dist/lib/types/core.generated.d.ts +14 -1
  57. package/dist/lib/types/create-media-buy.d.ts +14 -1
  58. package/dist/lib/types/get-media-buys.d.ts +14 -1
  59. package/dist/lib/types/get-products.d.ts +19 -1
  60. package/dist/lib/types/list-products.d.ts +19 -1
  61. package/dist/lib/types/refine-proposals.d.ts +19 -1
  62. package/dist/lib/types/request-proposals.d.ts +19 -1
  63. package/dist/lib/types/schemas.generated.d.ts +12 -3
  64. package/dist/lib/types/schemas.generated.js +4 -1
  65. package/dist/lib/types/schemas.generated.mjs +4 -1
  66. package/dist/lib/types/tools.generated.d.mts +14 -1
  67. package/dist/lib/types/tools.generated.d.ts +14 -1
  68. package/dist/lib/types/update-media-buy.d.ts +14 -1
  69. package/dist/lib/version.d.mts +3 -3
  70. package/dist/lib/version.d.ts +3 -3
  71. package/dist/lib/version.js +3 -3
  72. package/dist/lib/version.mjs +3 -3
  73. package/dist/lib/webhooks/index.d.mts +24 -0
  74. package/dist/lib/webhooks/index.d.ts +24 -0
  75. package/dist/lib/webhooks/index.js +50 -24
  76. package/dist/lib/webhooks/index.mjs +49 -24
  77. package/dist/lib/wholesale-feed-sync/index.d.mts +2 -0
  78. package/dist/lib/wholesale-feed-sync/index.d.ts +2 -0
  79. package/dist/lib/wholesale-feed-sync/index.js +7 -0
  80. package/dist/lib/wholesale-feed-sync/index.mjs +4 -0
  81. package/dist/lib/wholesale-feed-sync/mirror.d.mts +97 -0
  82. package/dist/lib/wholesale-feed-sync/mirror.d.ts +97 -0
  83. package/dist/lib/wholesale-feed-sync/mirror.js +350 -0
  84. package/dist/lib/wholesale-feed-sync/mirror.mjs +322 -0
  85. package/dist/lib/wholesale-feed-sync/sync.d.mts +12 -29
  86. package/dist/lib/wholesale-feed-sync/sync.d.ts +12 -29
  87. package/dist/lib/wholesale-feed-sync/sync.js +153 -297
  88. package/dist/lib/wholesale-feed-sync/sync.mjs +158 -297
  89. package/docs/README.md +6 -0
  90. package/docs/TYPE-SUMMARY.md +2 -2
  91. package/docs/guides/BUYER-STORAGE.md +3 -0
  92. package/docs/guides/FIRST-CALL-TO-A-SELLER.md +3 -1
  93. package/docs/guides/account-resolution.md +46 -10
  94. package/docs/llms.txt +2 -2
  95. package/docs/migration-14.0-to-14.1.md +85 -0
  96. package/docs/migration-14.x-rc-worksheet.md +4 -4
  97. package/docs/migration-agent-resolution-3.3.md +5 -3
  98. package/docs/recipes/verifying-inbound-webhooks.md +4 -0
  99. package/package.json +2 -1
  100. package/skills/adcp-brand.previous/SKILL.md +0 -200
  101. package/skills/adcp-creative.previous/SKILL.md +0 -305
  102. package/skills/adcp-governance.previous/SKILL.md +0 -566
  103. package/skills/adcp-measurement.previous/SKILL.md +0 -136
  104. package/skills/adcp-media-buy.previous/SKILL.md +0 -556
  105. package/skills/adcp-si.previous/SKILL.md +0 -206
  106. package/skills/adcp-signals.previous/SKILL.md +0 -204
@@ -1,12 +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";
5
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
+ }
6
16
  const DEFAULT_PROBE_INTERVAL_MS = 6e5;
7
17
  const DEFAULT_CAPABILITY_REFRESH_INTERVAL_MS = 864e5;
8
18
  const DEFAULT_PERSISTENCE_TIMEOUT_MS = 3e4;
9
- const DEFAULT_BOOTSTRAP_PAGE_LIMIT = 100;
10
19
  const VERSION_MISMATCH_RECOVERY_ATTEMPTS = 3;
11
20
  const VERSION_MISMATCH_RECOVERY_BACKOFF_MS = 5;
12
21
  class WholesaleFeedSync extends EventEmitter {
@@ -47,11 +56,10 @@ class WholesaleFeedSync extends EventEmitter {
47
56
  probeTimer = null;
48
57
  capabilityTimer = null;
49
58
  lifecycleEpoch = 0;
59
+ mirrorRevision = 0;
50
60
  /**
51
- * Read-only view of the in-memory product index. The `mode` reflects the
52
- * sync strategy for product events specifically — in mixed-capability
53
- * agents (products-feed + signals-wholesale-only, or vice versa) this
54
- * 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.
55
63
  */
56
64
  products = {
57
65
  list: () => [...this.productIndex.values()],
@@ -86,19 +94,12 @@ class WholesaleFeedSync extends EventEmitter {
86
94
  },
87
95
  _mode: "manual",
88
96
  /**
89
- * `true` when the agent supports `discovery_mode: 'wholesale'` on
90
- * `get_signals` (i.e., signals are browsable). When `false`, the
91
- * agent only supports brief-mode discovery — `signals.list()` will
92
- * 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.
93
99
  */
94
100
  queryable: true
95
101
  };
96
- /**
97
- * One-shot console warning when adopters call `signals.list()` or
98
- * `signals.search()` against an agent that doesn't support wholesale
99
- * signal enumeration. Without this, empty results read as "no signals
100
- * match" rather than "the agent doesn't browse, only briefs."
101
- */
102
+ // Warn once when empty signal results reflect missing wholesale support.
102
103
  warnIfSignalsNotQueryable() {
103
104
  if (this.signals.queryable || this.signalsQueryableWarned) return;
104
105
  if (this._state === "idle" || this._state === "bootstrapping") return;
@@ -124,14 +125,9 @@ class WholesaleFeedSync extends EventEmitter {
124
125
  }
125
126
  // ====== Lifecycle ======
126
127
  /**
127
- * Probe the agent's capabilities, pick a sync mode, and bootstrap the
128
- * in-memory replica via wholesale enumeration. In `'auto-poll'` mode
129
- * starts the conditional wholesale-feed version probe loop.
130
- *
131
- * Safe to call repeatedly — concurrent calls await the in-flight
132
- * bootstrap and return when it completes (no duplicate bootstrap, no
133
- * silent drop). Sequential calls re-probe capabilities and re-bootstrap,
134
- * 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.
135
131
  */
136
132
  async start() {
137
133
  if (this.startPromise && this.startPromiseEpoch === this.lifecycleEpoch) return this.startPromise;
@@ -151,7 +147,7 @@ class WholesaleFeedSync extends EventEmitter {
151
147
  if (!await this.restorePersistedState(epoch)) return;
152
148
  if (!await this.resolveMode(epoch)) return;
153
149
  try {
154
- await this.bootstrap({ epoch });
150
+ await this.bootstrapWithRetries({ epoch });
155
151
  } finally {
156
152
  if (this.isLifecycleCurrent(epoch)) {
157
153
  if (this._mode === "auto-poll") this.scheduleProbe(epoch);
@@ -175,6 +171,7 @@ class WholesaleFeedSync extends EventEmitter {
175
171
  */
176
172
  async reset() {
177
173
  this.stop();
174
+ ++this.mirrorRevision;
178
175
  this.productIndex.clear();
179
176
  this.signalIndex.clear();
180
177
  this.productWholesaleFeedVersion = void 0;
@@ -200,7 +197,7 @@ class WholesaleFeedSync extends EventEmitter {
200
197
  async refresh() {
201
198
  const epoch = this.lifecycleEpoch;
202
199
  this.emit("resyncing", { reason: "manual" });
203
- await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
200
+ await this.bootstrapWithRetries({ emitDiffs: true, epoch, propagateFailure: true });
204
201
  }
205
202
  /**
206
203
  * Apply one legacy-view account-level wholesale feed webhook to the local mirror.
@@ -249,6 +246,11 @@ class WholesaleFeedSync extends EventEmitter {
249
246
  this.rememberLastWebhookEventId(event.event_id);
250
247
  throw err;
251
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
+ }
252
254
  if (webhook.previous_wholesale_feed_version && currentVersion && webhook.previous_wholesale_feed_version !== currentVersion) {
253
255
  if (!await this.recoverFromVersionMismatch(event, epoch)) return;
254
256
  await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
@@ -329,12 +331,24 @@ class WholesaleFeedSync extends EventEmitter {
329
331
  return true;
330
332
  }
331
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
+ }
332
344
  async bootstrap(options = {}) {
333
345
  const epoch = options.epoch ?? this.lifecycleEpoch;
334
346
  if (!this.isLifecycleCurrent(epoch)) return false;
335
347
  this.setState("bootstrapping");
336
348
  const previousLastSyncedAt = this._lastSyncedAt;
349
+ const expectedRevision = this.mirrorRevision;
337
350
  let reportedFailure;
351
+ let committed = false;
338
352
  try {
339
353
  const previousProducts = new Map(this.productIndex);
340
354
  const previousSignals = new Map(this.signalIndex);
@@ -346,8 +360,12 @@ class WholesaleFeedSync extends EventEmitter {
346
360
  const refreshProducts = entities !== "signals";
347
361
  const refreshSignals = entities !== "products" && this.signals.queryable;
348
362
  if (refreshProducts) {
349
- productResult = await this.bootstrapProducts(epoch);
363
+ productResult = await this.bootstrapProducts(epoch, options.conditional);
350
364
  if (productResult.cancelled) return false;
365
+ if (this.mirrorRevision !== expectedRevision) {
366
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
367
+ return false;
368
+ }
351
369
  if (productResult.failure) {
352
370
  const failure = productResult.failure;
353
371
  reportedFailure = failure.error;
@@ -361,8 +379,12 @@ class WholesaleFeedSync extends EventEmitter {
361
379
  }
362
380
  }
363
381
  if (refreshSignals) {
364
- signalResult = await this.bootstrapSignals(epoch);
382
+ signalResult = await this.bootstrapSignals(epoch, options.conditional);
365
383
  if (signalResult.cancelled) return false;
384
+ if (this.mirrorRevision !== expectedRevision) {
385
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
386
+ return false;
387
+ }
366
388
  if (signalResult.failure) {
367
389
  const failure = signalResult.failure;
368
390
  reportedFailure = failure.error;
@@ -376,6 +398,12 @@ class WholesaleFeedSync extends EventEmitter {
376
398
  }
377
399
  }
378
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;
379
407
  if (refreshProducts && productResult) {
380
408
  this.commitProductMetadata(productResult.metadata);
381
409
  if (!productResult.unchanged) {
@@ -407,6 +435,7 @@ class WholesaleFeedSync extends EventEmitter {
407
435
  return true;
408
436
  } catch (err) {
409
437
  if (!this.isLifecycleCurrent(epoch)) return false;
438
+ if (!committed && this.mirrorRevision !== expectedRevision && err !== reportedFailure) return false;
410
439
  if (err === reportedFailure) throw err;
411
440
  this._lastSyncedAt = previousLastSyncedAt;
412
441
  this.setState(this._lastSyncedAt ? "degraded" : "error");
@@ -423,102 +452,74 @@ class WholesaleFeedSync extends EventEmitter {
423
452
  if (this.listenerCount("error") > 0) this.emit("error", failure);
424
453
  return false;
425
454
  }
426
- /** Returns `true` when the seller short-circuited with `unchanged: true`. */
427
- async bootstrapProducts(epoch) {
428
- let cursor;
429
- const into = /* @__PURE__ */ new Map();
430
- let metadata = this.currentProductMetadata();
431
- do {
432
- const params = {
433
- buying_mode: "wholesale",
434
- pagination: { max_results: DEFAULT_BOOTSTRAP_PAGE_LIMIT, ...cursor && { cursor } },
435
- ...this.account && { account: this.account }
436
- };
437
- if (!cursor && metadata.wholesaleFeedVersion && this._capabilities.wholesaleFeedVersioning) {
438
- params.if_wholesale_feed_version = metadata.wholesaleFeedVersion;
439
- if (metadata.pricingVersion) params.if_pricing_version = metadata.pricingVersion;
440
- }
441
- const result = await this.client.getProducts(params);
442
- if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
443
- if (result.success === false || result.status !== void 0 && result.status !== "completed" && result.status !== "success") {
444
- return {
445
- cancelled: false,
446
- unchanged: false,
447
- items: into,
448
- metadata,
449
- failure: { error: new Error(result.error ?? "Product bootstrap failed"), adcpError: result.adcpError }
450
- };
451
- }
452
- const body = result.data;
453
- if (!body)
454
- return {
455
- cancelled: false,
456
- unchanged: false,
457
- items: into,
458
- metadata,
459
- failure: { error: new Error("Product bootstrap did not return a completed catalog") }
460
- };
461
- if (body.unchanged) {
462
- metadata = mergeFeedMetadata(metadata, body);
463
- return { cancelled: false, unchanged: true, items: into, metadata };
464
- }
465
- const products = Array.isArray(body.products) ? body.products : [];
466
- for (const product of products) {
467
- const id = product.product_id;
468
- if (typeof id === "string") into.set(id, product);
469
- }
470
- metadata = mergeFeedMetadata(metadata, body);
471
- cursor = body.pagination?.has_more ? body.pagination?.cursor : void 0;
472
- } while (cursor);
473
- return { cancelled: false, unchanged: false, items: into, metadata };
455
+ bootstrapProducts(epoch, conditional) {
456
+ return this.bootstrapFeed("product", epoch, conditional);
474
457
  }
475
- async bootstrapSignals(epoch) {
476
- let cursor;
477
- const into = /* @__PURE__ */ new Map();
478
- let metadata = this.currentSignalMetadata();
479
- do {
480
- const params = {
481
- discovery_mode: "wholesale",
482
- pagination: { max_results: DEFAULT_BOOTSTRAP_PAGE_LIMIT, ...cursor && { cursor } },
483
- ...this.account && { account: this.account }
484
- };
485
- if (!cursor && metadata.wholesaleFeedVersion && this._capabilities.wholesaleFeedVersioning) {
486
- params.if_wholesale_feed_version = metadata.wholesaleFeedVersion;
487
- if (metadata.pricingVersion) params.if_pricing_version = metadata.pricingVersion;
488
- }
489
- const result = await this.client.getSignals(params);
490
- if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
491
- if (result.success === false || result.status !== void 0 && result.status !== "completed" && result.status !== "success") {
492
- return {
493
- cancelled: false,
494
- unchanged: false,
495
- items: into,
496
- metadata,
497
- failure: { error: new Error(result.error ?? "Signal bootstrap failed"), adcpError: result.adcpError }
498
- };
499
- }
500
- const body = result.data;
501
- if (!body)
502
- return {
503
- cancelled: false,
504
- unchanged: false,
505
- items: into,
506
- metadata,
507
- failure: { error: new Error("Signal bootstrap did not return a completed catalog") }
508
- };
509
- if (body.unchanged) {
510
- metadata = mergeFeedMetadata(metadata, body);
511
- return { cancelled: false, unchanged: true, items: into, metadata };
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;
512
474
  }
513
- const signals = Array.isArray(body.signals) ? body.signals : [];
514
- for (const signal of signals) {
515
- const id = signal.signal_agent_segment_id;
516
- 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
517
521
  }
518
- metadata = mergeFeedMetadata(metadata, body);
519
- cursor = body.pagination?.has_more ? body.pagination?.cursor : void 0;
520
- } while (cursor);
521
- return { cancelled: false, unchanged: false, items: into, metadata };
522
+ };
522
523
  }
523
524
  async recoverFromBulkChange(event, epoch = this.lifecycleEpoch) {
524
525
  this.emit("resyncing", { reason: "bulk_change" });
@@ -529,14 +530,22 @@ class WholesaleFeedSync extends EventEmitter {
529
530
  );
530
531
  }
531
532
  const entities = affected === "product" ? "products" : "signals";
532
- return this.bootstrap({ emitDiffs: true, entities, epoch, propagateFailure: true });
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();
533
538
  }
534
539
  async recoverFromVersionMismatch(event, epoch = this.lifecycleEpoch) {
535
540
  this.emit("resyncing", { reason: "version_mismatch" });
536
541
  const beforeVersion = this.currentWholesaleFeedVersionForEvent(event);
537
542
  for (let attempt = 1; attempt <= VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
538
543
  const recovered = await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
539
- if (!recovered) return false;
544
+ if (!recovered) {
545
+ if (!this.isLifecycleCurrent(epoch)) return false;
546
+ if (attempt === VERSION_MISMATCH_RECOVERY_ATTEMPTS) throw supersededRepairError();
547
+ continue;
548
+ }
540
549
  const afterVersion = this.currentWholesaleFeedVersionForEvent(event);
541
550
  if (afterVersion !== beforeVersion) return true;
542
551
  if (attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS) {
@@ -690,63 +699,16 @@ class WholesaleFeedSync extends EventEmitter {
690
699
  }
691
700
  }
692
701
  applyEvent(event) {
693
- switch (event.event_type) {
694
- case "product.created":
695
- case "product.updated": {
696
- const payload = event.payload;
697
- if (typeof payload.product_id !== "string") return;
698
- if (payload.product) {
699
- this.productIndex.set(payload.product_id, payload.product);
700
- return;
701
- }
702
- return;
703
- }
704
- case "product.priced": {
705
- const payload = event.payload;
706
- const existing = this.productIndex.get(payload.product_id);
707
- if (existing && Array.isArray(payload.pricing_options)) {
708
- this.productIndex.set(payload.product_id, {
709
- ...existing,
710
- pricing_options: payload.pricing_options
711
- });
712
- }
713
- return;
714
- }
715
- case "product.removed": {
716
- const payload = event.payload;
717
- if (typeof payload.product_id === "string") this.productIndex.delete(payload.product_id);
718
- return;
719
- }
720
- case "signal.created":
721
- case "signal.updated": {
722
- const payload = event.payload;
723
- if (typeof payload.signal_agent_segment_id !== "string") return;
724
- if (payload.signal) {
725
- this.signalIndex.set(payload.signal_agent_segment_id, payload.signal);
726
- }
727
- return;
728
- }
729
- case "signal.priced": {
730
- const payload = event.payload;
731
- const existing = this.signalIndex.get(payload.signal_agent_segment_id);
732
- if (existing && Array.isArray(payload.pricing_options)) {
733
- this.signalIndex.set(payload.signal_agent_segment_id, {
734
- ...existing,
735
- pricing_options: payload.pricing_options
736
- });
737
- }
738
- return;
739
- }
740
- case "signal.removed": {
741
- const payload = event.payload;
742
- if (typeof payload.signal_agent_segment_id === "string") {
743
- this.signalIndex.delete(payload.signal_agent_segment_id);
744
- }
745
- return;
746
- }
747
- case "wholesale_feed.bulk_change":
748
- return;
749
- }
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]));
750
712
  }
751
713
  emitTypedEvent(event) {
752
714
  switch (event.event_type) {
@@ -780,107 +742,23 @@ class WholesaleFeedSync extends EventEmitter {
780
742
  }
781
743
  // ====== Private: diff emission (auto-poll / manual refresh) ======
782
744
  emitDiffs(previousProducts, previousSignals) {
783
- const now = (/* @__PURE__ */ new Date()).toISOString();
784
- const makeEvent = (event_type, entity_type, entity_id, payload) => ({
785
- // crypto.randomUUID() emits a v4 UUID, NOT v7. Synthetic events
786
- // are flagged via `synthetic: true` on the emit envelope so
787
- // adopters writing event_ids to a dedupe table know not to
788
- // treat the ID as seller-authored. Real webhook events carry the
789
- // agent's authoritative event_id.
790
- event_id: randomUUID(),
791
- event_type,
792
- entity_type,
793
- entity_id,
794
- created_at: now,
795
- payload
796
- });
797
- 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) {
798
760
  this.emit("event", { event, synthetic: true });
799
- this.emit(channel, { event, synthetic: true });
800
- };
801
- for (const [id, product] of this.productIndex) {
802
- const prev = previousProducts.get(id);
803
- if (!prev) {
804
- emit(
805
- "product.created",
806
- makeEvent("product.created", "product", id, {
807
- product_id: id,
808
- product,
809
- applies_to: { scope: this.productCacheScope }
810
- })
811
- );
812
- } else if (priceChanged(prev, product)) {
813
- emit(
814
- "product.priced",
815
- makeEvent("product.priced", "product", id, {
816
- product_id: id,
817
- pricing_options: product.pricing_options ?? [],
818
- applies_to: { scope: this.productCacheScope }
819
- })
820
- );
821
- } else if (!isDeepStrictEqual(prev, product)) {
822
- emit(
823
- "product.updated",
824
- makeEvent("product.updated", "product", id, {
825
- product_id: id,
826
- product,
827
- applies_to: { scope: this.productCacheScope }
828
- })
829
- );
830
- }
831
- }
832
- for (const [id] of previousProducts) {
833
- if (!this.productIndex.has(id)) {
834
- emit(
835
- "product.removed",
836
- makeEvent("product.removed", "product", id, {
837
- product_id: id,
838
- applies_to: { scope: this.productCacheScope }
839
- })
840
- );
841
- }
842
- }
843
- for (const [id, signal] of this.signalIndex) {
844
- const prev = previousSignals.get(id);
845
- if (!prev) {
846
- emit(
847
- "signal.created",
848
- makeEvent("signal.created", "signal", id, {
849
- signal_agent_segment_id: id,
850
- signal,
851
- applies_to: { scope: this.signalCacheScope }
852
- })
853
- );
854
- } else if (signalPriceChanged(prev, signal)) {
855
- emit(
856
- "signal.priced",
857
- makeEvent("signal.priced", "signal", id, {
858
- signal_agent_segment_id: id,
859
- pricing_options: signal.pricing_options ?? [],
860
- applies_to: { scope: this.signalCacheScope }
861
- })
862
- );
863
- } else if (!isDeepStrictEqual(prev, signal)) {
864
- emit(
865
- "signal.updated",
866
- makeEvent("signal.updated", "signal", id, {
867
- signal_agent_segment_id: id,
868
- signal,
869
- applies_to: { scope: this.signalCacheScope }
870
- })
871
- );
872
- }
873
- }
874
- for (const [id] of previousSignals) {
875
- if (!this.signalIndex.has(id)) {
876
- emit(
877
- "signal.removed",
878
- makeEvent("signal.removed", "signal", id, {
879
- signal_agent_segment_id: id,
880
- applies_to: { scope: this.signalCacheScope }
881
- })
882
- );
883
- }
761
+ this.emit(event.event_type, { event, synthetic: true });
884
762
  }
885
763
  }
886
764
  // ====== Private: search ======
@@ -1057,16 +935,6 @@ function assertOptionalDate(value, field, invalid) {
1057
935
  function parsePersistedDate(value) {
1058
936
  return value === void 0 ? void 0 : new Date(value);
1059
937
  }
1060
- function priceChanged(prev, next) {
1061
- const a = prev.pricing_options;
1062
- const b = next.pricing_options;
1063
- return !isDeepStrictEqual(a, b);
1064
- }
1065
- function signalPriceChanged(prev, next) {
1066
- const a = prev.pricing_options;
1067
- const b = next.pricing_options;
1068
- return !isDeepStrictEqual(a, b);
1069
- }
1070
938
  function isUuidV7(value) {
1071
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);
1072
940
  }
@@ -1076,13 +944,6 @@ function compareUuidV7(a, b) {
1076
944
  const normalizedB = b.toLowerCase();
1077
945
  return normalizedA === normalizedB ? 0 : normalizedA > normalizedB ? 1 : -1;
1078
946
  }
1079
- function mergeFeedMetadata(current, body) {
1080
- return {
1081
- wholesaleFeedVersion: typeof body.wholesale_feed_version === "string" ? body.wholesale_feed_version : current.wholesaleFeedVersion,
1082
- pricingVersion: typeof body.pricing_version === "string" ? body.pricing_version : current.pricingVersion,
1083
- cacheScope: body.cache_scope === "public" || body.cache_scope === "account" ? body.cache_scope : current.cacheScope
1084
- };
1085
- }
1086
947
  async function sleep(ms) {
1087
948
  await new Promise((resolve) => setTimeout(resolve, ms));
1088
949
  }
package/docs/README.md CHANGED
@@ -25,6 +25,12 @@ Use the [buyer quick start](./guides/BUYER-QUICKSTART-3.2.md). For the complete
25
25
  tool and type inventory, use [llms.txt](./llms.txt) and the
26
26
  [type summary](./TYPE-SUMMARY.md).
27
27
 
28
+ ## Upgrade from SDK 14.0
29
+
30
+ Use the [14.0-to-14.1 upgrade checklist](./migration-14.0-to-14.1.md) for
31
+ required changes, opt-in features, and stricter signature verification. SDK
32
+ 14.1.0 continues to use AdCP 3.2.1 on the wire.
33
+
28
34
  ## Upgrade from SDK 13
29
35
 
30
36
  Start with the [release-bound upgrade worksheet](./migration-14.x-rc-worksheet.md),
@@ -1,7 +1,7 @@
1
1
  # AdCP Type Summary
2
2
 
3
- > Generated at: 2026-10-04
4
- > @adcp/sdk v14.1.0
3
+ > Generated at: 2026-10-05
4
+ > @adcp/sdk v14.2.0
5
5
 
6
6
  Curated reference of the types that matter for using the AdCP client. For full generated types see `src/lib/types/tools.generated.ts` and `src/lib/types/core.generated.ts`.
7
7
 
@@ -0,0 +1,3 @@
1
+ # Buyer-owned storage and scheduling
2
+
3
+ See the [adapter contracts and recovery guide](https://github.com/adcontextprotocol/adcp-client/blob/main/docs/development/BUYER-STORAGE.md) for timer-free catalog mirrors, CAS provisioning, dispatch hooks, and caller-owned replay keys.
@@ -71,7 +71,9 @@ from authoritative `list_accounts`. The notification's status is not trusted as
71
71
  a snapshot, which avoids reordered deliveries overwriting current state.
72
72
 
73
73
  The registry is in memory by default. Set `accountStorage` to an adapter with
74
- `get(key)` and `set(key, entry)` for persistence. Keys partition by seller URI,
74
+ `get(key)` and `set(key, entry)` for persistence. For multiple processes, add
75
+ revision-checked `compareAndSet` writes and dispatch ledger hooks; see
76
+ [buyer storage and scheduling](BUYER-STORAGE.md). Keys partition by seller URI,
75
77
  protocol, and caller scope. Client-credentials OAuth uses the stable client ID, token endpoint, scopes, and resource. Authorization-code OAuth pins this client's initial grant fingerprint, keeping distinct user grants apart while automatic refreshes preserve its partition. Create a new client when switching users. Other credential modes fingerprint credentials;
76
78
  for continuity across token rotations, supply `accountRegistryScope` from a
77
79
  trusted stable principal identifier. Never share that scope between tenants.