@adcp/sdk 14.0.0 → 14.1.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 (185) 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 +31 -1
  10. package/dist/lib/core/SingleAgentClient.d.ts +31 -1
  11. package/dist/lib/core/SingleAgentClient.js +355 -35
  12. package/dist/lib/core/SingleAgentClient.mjs +365 -37
  13. package/dist/lib/core/TaskExecutor.js +2 -1
  14. package/dist/lib/core/TaskExecutor.mjs +2 -1
  15. package/dist/lib/core/account-key.d.mts +3 -0
  16. package/dist/lib/core/account-key.d.ts +3 -0
  17. package/dist/lib/core/account-key.js +41 -0
  18. package/dist/lib/core/account-key.mjs +17 -0
  19. package/dist/lib/core/account-resolution.d.mts +2 -0
  20. package/dist/lib/core/account-resolution.d.ts +2 -0
  21. package/dist/lib/core/buyer-account-registry.d.mts +65 -0
  22. package/dist/lib/core/buyer-account-registry.d.ts +65 -0
  23. package/dist/lib/core/buyer-account-registry.js +518 -0
  24. package/dist/lib/core/buyer-account-registry.mjs +494 -0
  25. package/dist/lib/core/product-cache.d.mts +18 -0
  26. package/dist/lib/core/product-cache.d.ts +18 -0
  27. package/dist/lib/core/product-cache.js +137 -0
  28. package/dist/lib/core/product-cache.mjs +112 -0
  29. package/dist/lib/errors/index.d.mts +40 -1
  30. package/dist/lib/errors/index.d.ts +40 -1
  31. package/dist/lib/errors/index.js +69 -3
  32. package/dist/lib/errors/index.mjs +64 -3
  33. package/dist/lib/governance/authorization.d.mts +17 -1
  34. package/dist/lib/governance/authorization.d.ts +17 -1
  35. package/dist/lib/governance/authorization.js +55 -7
  36. package/dist/lib/governance/authorization.mjs +59 -7
  37. package/dist/lib/governance/index.d.mts +2 -2
  38. package/dist/lib/governance/index.d.ts +2 -2
  39. package/dist/lib/governance/index.js +2 -0
  40. package/dist/lib/governance/index.mjs +3 -1
  41. package/dist/lib/index.d.mts +5 -3
  42. package/dist/lib/index.d.ts +5 -3
  43. package/dist/lib/index.js +20 -0
  44. package/dist/lib/index.mjs +19 -0
  45. package/dist/lib/protocols/a2a.js +9 -1
  46. package/dist/lib/protocols/a2a.mjs +9 -1
  47. package/dist/lib/protocols/index.js +9 -2
  48. package/dist/lib/protocols/index.mjs +9 -2
  49. package/dist/lib/protocols/mcp-modern.js +2 -1
  50. package/dist/lib/protocols/mcp-modern.mjs +2 -1
  51. package/dist/lib/protocols/mcp.js +5 -2
  52. package/dist/lib/protocols/mcp.mjs +5 -2
  53. package/dist/lib/protocols/rawResponseCapture.d.mts +6 -0
  54. package/dist/lib/protocols/rawResponseCapture.d.ts +6 -0
  55. package/dist/lib/protocols/rawResponseCapture.js +41 -29
  56. package/dist/lib/protocols/rawResponseCapture.mjs +40 -29
  57. package/dist/lib/protocols/signedRequestRejection.d.mts +9 -0
  58. package/dist/lib/protocols/signedRequestRejection.d.ts +9 -0
  59. package/dist/lib/protocols/signedRequestRejection.js +209 -0
  60. package/dist/lib/protocols/signedRequestRejection.mjs +189 -0
  61. package/dist/lib/protocols/transportDiagnostics.d.mts +1 -0
  62. package/dist/lib/protocols/transportDiagnostics.d.ts +1 -0
  63. package/dist/lib/protocols/transportDiagnostics.js +2 -0
  64. package/dist/lib/protocols/transportDiagnostics.mjs +1 -0
  65. package/dist/lib/registry/types.generated.d.mts +112 -45
  66. package/dist/lib/registry/types.generated.d.ts +112 -45
  67. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  68. package/dist/lib/server/account-provisioning.d.mts +2 -0
  69. package/dist/lib/server/account-provisioning.d.ts +2 -0
  70. package/dist/lib/server/account-provisioning.js +30 -0
  71. package/dist/lib/server/account-provisioning.mjs +6 -0
  72. package/dist/lib/server/account-reference-warnings.d.mts +12 -0
  73. package/dist/lib/server/account-reference-warnings.d.ts +12 -0
  74. package/dist/lib/server/account-reference-warnings.js +48 -0
  75. package/dist/lib/server/account-reference-warnings.mjs +23 -0
  76. package/dist/lib/server/auth-signature.js +1 -0
  77. package/dist/lib/server/auth-signature.mjs +1 -0
  78. package/dist/lib/server/create-adcp-server.d.mts +34 -0
  79. package/dist/lib/server/create-adcp-server.d.ts +34 -0
  80. package/dist/lib/server/create-adcp-server.js +225 -14
  81. package/dist/lib/server/create-adcp-server.mjs +225 -14
  82. package/dist/lib/server/decisioning/account.d.mts +2 -0
  83. package/dist/lib/server/decisioning/account.d.ts +2 -0
  84. package/dist/lib/server/decisioning/runtime/from-platform.js +49 -10
  85. package/dist/lib/server/decisioning/runtime/from-platform.mjs +49 -10
  86. package/dist/lib/server/index.d.mts +2 -2
  87. package/dist/lib/server/index.d.ts +2 -2
  88. package/dist/lib/server/index.js +2 -0
  89. package/dist/lib/server/index.mjs +3 -1
  90. package/dist/lib/signing/agent-resolver/consistency.d.mts +6 -13
  91. package/dist/lib/signing/agent-resolver/consistency.d.ts +6 -13
  92. package/dist/lib/signing/agent-resolver/consistency.js +0 -1
  93. package/dist/lib/signing/agent-resolver/consistency.mjs +0 -1
  94. package/dist/lib/signing/agent-resolver/errors.d.mts +1 -1
  95. package/dist/lib/signing/agent-resolver/errors.d.ts +1 -1
  96. package/dist/lib/signing/agent-resolver/fetch-helpers.d.mts +2 -0
  97. package/dist/lib/signing/agent-resolver/fetch-helpers.d.ts +2 -0
  98. package/dist/lib/signing/agent-resolver/fetch-helpers.js +2 -1
  99. package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +2 -1
  100. package/dist/lib/signing/agent-resolver/jwks-set.js +24 -3
  101. package/dist/lib/signing/agent-resolver/jwks-set.mjs +24 -3
  102. package/dist/lib/signing/agent-resolver/legacy-brand.d.mts +15 -0
  103. package/dist/lib/signing/agent-resolver/legacy-brand.d.ts +15 -0
  104. package/dist/lib/signing/agent-resolver/legacy-brand.js +60 -0
  105. package/dist/lib/signing/agent-resolver/legacy-brand.mjs +36 -0
  106. package/dist/lib/signing/agent-resolver/operator-authorization.d.mts +13 -0
  107. package/dist/lib/signing/agent-resolver/operator-authorization.d.ts +13 -0
  108. package/dist/lib/signing/agent-resolver/operator-authorization.js +108 -0
  109. package/dist/lib/signing/agent-resolver/operator-authorization.mjs +84 -0
  110. package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +14 -4
  111. package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +14 -4
  112. package/dist/lib/signing/agent-resolver/resolve-agent.js +101 -132
  113. package/dist/lib/signing/agent-resolver/resolve-agent.mjs +102 -133
  114. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.mts +7 -1
  115. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.ts +7 -1
  116. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.js +50 -17
  117. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.mjs +50 -17
  118. package/dist/lib/signing/agent-resolver/select-agent.d.mts +14 -15
  119. package/dist/lib/signing/agent-resolver/select-agent.d.ts +14 -15
  120. package/dist/lib/signing/agent-resolver/select-agent.js +98 -13
  121. package/dist/lib/signing/agent-resolver/select-agent.mjs +95 -13
  122. package/dist/lib/signing/brand-jwks.d.mts +27 -75
  123. package/dist/lib/signing/brand-jwks.d.ts +27 -75
  124. package/dist/lib/signing/brand-jwks.js +112 -182
  125. package/dist/lib/signing/brand-jwks.mjs +112 -182
  126. package/dist/lib/signing/errors.d.mts +3 -1
  127. package/dist/lib/signing/errors.d.ts +3 -1
  128. package/dist/lib/signing/errors.js +4 -1
  129. package/dist/lib/signing/errors.mjs +4 -1
  130. package/dist/lib/signing/jwks-https.d.mts +7 -0
  131. package/dist/lib/signing/jwks-https.d.ts +7 -0
  132. package/dist/lib/signing/jwks-https.js +31 -8
  133. package/dist/lib/signing/jwks-https.mjs +31 -8
  134. package/dist/lib/signing/jwks.d.mts +8 -0
  135. package/dist/lib/signing/jwks.d.ts +8 -0
  136. package/dist/lib/signing/middleware.js +2 -1
  137. package/dist/lib/signing/middleware.mjs +2 -1
  138. package/dist/lib/signing/publisher-pins.d.mts +11 -0
  139. package/dist/lib/signing/publisher-pins.d.ts +11 -0
  140. package/dist/lib/signing/publisher-pins.js +125 -0
  141. package/dist/lib/signing/publisher-pins.mjs +101 -0
  142. package/dist/lib/signing/server.d.mts +1 -0
  143. package/dist/lib/signing/server.d.ts +1 -0
  144. package/dist/lib/signing/types.d.mts +5 -0
  145. package/dist/lib/signing/types.d.ts +5 -0
  146. package/dist/lib/signing/verifier.js +49 -4
  147. package/dist/lib/signing/verifier.mjs +49 -4
  148. package/dist/lib/signing/webhook-verifier.d.mts +7 -2
  149. package/dist/lib/signing/webhook-verifier.d.ts +7 -2
  150. package/dist/lib/signing/webhook-verifier.js +42 -2
  151. package/dist/lib/signing/webhook-verifier.mjs +43 -3
  152. package/dist/lib/testing/storyboard/account-policy.d.mts +2 -0
  153. package/dist/lib/testing/storyboard/account-policy.d.ts +2 -0
  154. package/dist/lib/testing/storyboard/account-policy.js +35 -0
  155. package/dist/lib/testing/storyboard/account-policy.mjs +11 -0
  156. package/dist/lib/testing/storyboard/context.js +6 -0
  157. package/dist/lib/testing/storyboard/context.mjs +6 -0
  158. package/dist/lib/testing/storyboard/request-builder.js +12 -2
  159. package/dist/lib/testing/storyboard/request-builder.mjs +12 -2
  160. package/dist/lib/testing/storyboard/runner.js +3 -2
  161. package/dist/lib/testing/storyboard/runner.mjs +3 -2
  162. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  163. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  164. package/dist/lib/version.d.mts +3 -3
  165. package/dist/lib/version.d.ts +3 -3
  166. package/dist/lib/version.js +3 -3
  167. package/dist/lib/version.mjs +3 -3
  168. package/dist/lib/wholesale-feed-sync/sync.d.mts +1 -0
  169. package/dist/lib/wholesale-feed-sync/sync.d.ts +1 -0
  170. package/dist/lib/wholesale-feed-sync/sync.js +92 -21
  171. package/dist/lib/wholesale-feed-sync/sync.mjs +92 -21
  172. package/dist/lib/wholesale-feed-sync/types.d.mts +4 -3
  173. package/dist/lib/wholesale-feed-sync/types.d.ts +4 -3
  174. package/docs/TYPE-SUMMARY.md +2 -2
  175. package/docs/guides/BUILD-AN-AGENT.md +2 -2
  176. package/docs/guides/BUYER-QUICKSTART-3.2.md +2 -0
  177. package/docs/guides/FIRST-CALL-TO-A-SELLER.md +104 -0
  178. package/docs/guides/SIGNING-GUIDE.md +16 -7
  179. package/docs/guides/account-resolution.md +86 -0
  180. package/docs/llms.txt +3 -2
  181. package/docs/migration-14.x-rc-worksheet.md +4 -4
  182. package/docs/migration-4.x-to-5.x.md +1 -0
  183. package/docs/migration-agent-resolution-3.3.md +123 -0
  184. package/docs/recipes/verifying-inbound-webhooks.md +56 -15
  185. package/package.json +2 -2
@@ -25,6 +25,7 @@ var import_node_events = require("node:events");
25
25
  var import_node_util = require("node:util");
26
26
  var import_node_crypto = require("node:crypto");
27
27
  var import_webhook_notification = require('./webhook-notification.js');
28
+ const bootstrapReadFailures = /* @__PURE__ */ new WeakSet();
28
29
  const DEFAULT_PROBE_INTERVAL_MS = 6e5;
29
30
  const DEFAULT_CAPABILITY_REFRESH_INTERVAL_MS = 864e5;
30
31
  const DEFAULT_PERSISTENCE_TIMEOUT_MS = 3e4;
@@ -172,12 +173,13 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
172
173
  const epoch = this.lifecycleEpoch;
173
174
  if (!await this.restorePersistedState(epoch)) return;
174
175
  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);
176
+ try {
177
+ await this.bootstrap({ epoch });
178
+ } finally {
179
+ if (this.isLifecycleCurrent(epoch)) {
180
+ if (this._mode === "auto-poll") this.scheduleProbe(epoch);
181
+ if (this.capabilityRefreshIntervalMs > 0) this.scheduleCapabilityRefresh(epoch);
182
+ }
181
183
  }
182
184
  }
183
185
  /** Stop all background activity. Preserves in-memory state and version tokens. */
@@ -187,7 +189,8 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
187
189
  if (this.capabilityTimer) clearTimeout(this.capabilityTimer);
188
190
  this.probeTimer = null;
189
191
  this.capabilityTimer = null;
190
- if (this._state === "syncing" || this._state === "bootstrapping") this.setState("idle");
192
+ if (this._state === "syncing" || this._state === "bootstrapping" || this._state === "degraded")
193
+ this.setState("idle");
191
194
  }
192
195
  /**
193
196
  * Stop, clear all indexes and version tokens. Call `start()` again to
@@ -220,7 +223,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
220
223
  async refresh() {
221
224
  const epoch = this.lifecycleEpoch;
222
225
  this.emit("resyncing", { reason: "manual" });
223
- await this.bootstrap({ emitDiffs: true, epoch });
226
+ await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
224
227
  }
225
228
  /**
226
229
  * Apply one legacy-view account-level wholesale feed webhook to the local mirror.
@@ -279,8 +282,10 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
279
282
  try {
280
283
  if (!await this.recoverFromBulkChange(event, epoch)) return;
281
284
  } catch (err) {
282
- await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
283
- this.rememberLastWebhookEventId(event.event_id);
285
+ if (!(err instanceof Error && bootstrapReadFailures.has(err))) {
286
+ await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
287
+ this.rememberLastWebhookEventId(event.event_id);
288
+ }
284
289
  throw err;
285
290
  }
286
291
  await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
@@ -351,6 +356,8 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
351
356
  const epoch = options.epoch ?? this.lifecycleEpoch;
352
357
  if (!this.isLifecycleCurrent(epoch)) return false;
353
358
  this.setState("bootstrapping");
359
+ const previousLastSyncedAt = this._lastSyncedAt;
360
+ let reportedFailure;
354
361
  try {
355
362
  const previousProducts = new Map(this.productIndex);
356
363
  const previousSignals = new Map(this.signalIndex);
@@ -364,10 +371,32 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
364
371
  if (refreshProducts) {
365
372
  productResult = await this.bootstrapProducts(epoch);
366
373
  if (productResult.cancelled) return false;
374
+ if (productResult.failure) {
375
+ const failure = productResult.failure;
376
+ reportedFailure = failure.error;
377
+ this.handleBootstrapFailure(failure);
378
+ if (options.propagateFailure) {
379
+ bootstrapReadFailures.add(failure.error);
380
+ if (failure.adcpError) Object.assign(failure.error, { adcpError: failure.adcpError });
381
+ throw failure.error;
382
+ }
383
+ return false;
384
+ }
367
385
  }
368
386
  if (refreshSignals) {
369
387
  signalResult = await this.bootstrapSignals(epoch);
370
388
  if (signalResult.cancelled) return false;
389
+ if (signalResult.failure) {
390
+ const failure = signalResult.failure;
391
+ reportedFailure = failure.error;
392
+ this.handleBootstrapFailure(failure);
393
+ if (options.propagateFailure) {
394
+ bootstrapReadFailures.add(failure.error);
395
+ if (failure.adcpError) Object.assign(failure.error, { adcpError: failure.adcpError });
396
+ throw failure.error;
397
+ }
398
+ return false;
399
+ }
371
400
  }
372
401
  if (!this.isLifecycleCurrent(epoch)) return false;
373
402
  if (refreshProducts && productResult) {
@@ -401,13 +430,22 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
401
430
  return true;
402
431
  } catch (err) {
403
432
  if (!this.isLifecycleCurrent(epoch)) return false;
404
- this.setState("error");
433
+ if (err === reportedFailure) throw err;
434
+ this._lastSyncedAt = previousLastSyncedAt;
435
+ this.setState(this._lastSyncedAt ? "degraded" : "error");
405
436
  const error = err instanceof Error ? err : new Error(String(err));
406
437
  this.errorHandler?.(error);
407
- this.emit("error", { error });
438
+ if (this.listenerCount("error") > 0) this.emit("error", { error });
439
+ if (options.propagateFailure) bootstrapReadFailures.add(error);
408
440
  throw error;
409
441
  }
410
442
  }
443
+ handleBootstrapFailure(failure) {
444
+ this.setState(this._lastSyncedAt ? "degraded" : "error");
445
+ this.errorHandler?.(failure.error);
446
+ if (this.listenerCount("error") > 0) this.emit("error", failure);
447
+ return false;
448
+ }
411
449
  /** Returns `true` when the seller short-circuited with `unchanged: true`. */
412
450
  async bootstrapProducts(epoch) {
413
451
  let cursor;
@@ -425,8 +463,24 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
425
463
  }
426
464
  const result = await this.client.getProducts(params);
427
465
  if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
466
+ if (result.success === false || result.status !== void 0 && result.status !== "completed" && result.status !== "success") {
467
+ return {
468
+ cancelled: false,
469
+ unchanged: false,
470
+ items: into,
471
+ metadata,
472
+ failure: { error: new Error(result.error ?? "Product bootstrap failed"), adcpError: result.adcpError }
473
+ };
474
+ }
428
475
  const body = result.data;
429
- if (!body) return { cancelled: false, unchanged: false, items: into, metadata };
476
+ if (!body)
477
+ return {
478
+ cancelled: false,
479
+ unchanged: false,
480
+ items: into,
481
+ metadata,
482
+ failure: { error: new Error("Product bootstrap did not return a completed catalog") }
483
+ };
430
484
  if (body.unchanged) {
431
485
  metadata = mergeFeedMetadata(metadata, body);
432
486
  return { cancelled: false, unchanged: true, items: into, metadata };
@@ -457,8 +511,24 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
457
511
  }
458
512
  const result = await this.client.getSignals(params);
459
513
  if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
514
+ if (result.success === false || result.status !== void 0 && result.status !== "completed" && result.status !== "success") {
515
+ return {
516
+ cancelled: false,
517
+ unchanged: false,
518
+ items: into,
519
+ metadata,
520
+ failure: { error: new Error(result.error ?? "Signal bootstrap failed"), adcpError: result.adcpError }
521
+ };
522
+ }
460
523
  const body = result.data;
461
- if (!body) return { cancelled: false, unchanged: false, items: into, metadata };
524
+ if (!body)
525
+ return {
526
+ cancelled: false,
527
+ unchanged: false,
528
+ items: into,
529
+ metadata,
530
+ failure: { error: new Error("Signal bootstrap did not return a completed catalog") }
531
+ };
462
532
  if (body.unchanged) {
463
533
  metadata = mergeFeedMetadata(metadata, body);
464
534
  return { cancelled: false, unchanged: true, items: into, metadata };
@@ -482,13 +552,13 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
482
552
  );
483
553
  }
484
554
  const entities = affected === "product" ? "products" : "signals";
485
- return this.bootstrap({ emitDiffs: true, entities, epoch });
555
+ return this.bootstrap({ emitDiffs: true, entities, epoch, propagateFailure: true });
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 });
561
+ const recovered = await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
492
562
  if (!recovered) return false;
493
563
  const afterVersion = this.currentWholesaleFeedVersionForEvent(event);
494
564
  if (afterVersion !== beforeVersion) return true;
@@ -510,11 +580,12 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
510
580
  await this.probeVersion(epoch);
511
581
  } catch (err) {
512
582
  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);
583
+ if (this._state !== "degraded" && this._state !== "error") {
584
+ const error = err instanceof Error ? err : new Error(String(err));
585
+ this.handleBootstrapFailure({ error });
586
+ }
516
587
  }
517
- if (this.isLifecycleCurrent(epoch) && this._state === "syncing" && this._mode === "auto-poll") {
588
+ if (this.isLifecycleCurrent(epoch) && (this._state === "syncing" || this._state === "degraded" || this._state === "error") && this._mode === "auto-poll") {
518
589
  this.scheduleProbe(epoch);
519
590
  }
520
591
  }
@@ -541,7 +612,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
541
612
  this.emit("error", { error });
542
613
  this.errorHandler?.(error);
543
614
  }
544
- if (this.isLifecycleCurrent(epoch) && this._state === "syncing" && this.capabilityRefreshIntervalMs > 0) {
615
+ if (this.isLifecycleCurrent(epoch) && (this._state === "syncing" || this._state === "degraded" || this._state === "error") && this.capabilityRefreshIntervalMs > 0) {
545
616
  this.scheduleCapabilityRefresh(epoch);
546
617
  }
547
618
  }
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { assertLegacyWholesaleFeedRepresentation } from "./webhook-notification.mjs";
5
+ const bootstrapReadFailures = /* @__PURE__ */ new WeakSet();
5
6
  const DEFAULT_PROBE_INTERVAL_MS = 6e5;
6
7
  const DEFAULT_CAPABILITY_REFRESH_INTERVAL_MS = 864e5;
7
8
  const DEFAULT_PERSISTENCE_TIMEOUT_MS = 3e4;
@@ -149,12 +150,13 @@ class WholesaleFeedSync extends EventEmitter {
149
150
  const epoch = this.lifecycleEpoch;
150
151
  if (!await this.restorePersistedState(epoch)) return;
151
152
  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);
153
+ try {
154
+ await this.bootstrap({ epoch });
155
+ } finally {
156
+ if (this.isLifecycleCurrent(epoch)) {
157
+ if (this._mode === "auto-poll") this.scheduleProbe(epoch);
158
+ if (this.capabilityRefreshIntervalMs > 0) this.scheduleCapabilityRefresh(epoch);
159
+ }
158
160
  }
159
161
  }
160
162
  /** Stop all background activity. Preserves in-memory state and version tokens. */
@@ -164,7 +166,8 @@ class WholesaleFeedSync extends EventEmitter {
164
166
  if (this.capabilityTimer) clearTimeout(this.capabilityTimer);
165
167
  this.probeTimer = null;
166
168
  this.capabilityTimer = null;
167
- if (this._state === "syncing" || this._state === "bootstrapping") this.setState("idle");
169
+ if (this._state === "syncing" || this._state === "bootstrapping" || this._state === "degraded")
170
+ this.setState("idle");
168
171
  }
169
172
  /**
170
173
  * Stop, clear all indexes and version tokens. Call `start()` again to
@@ -197,7 +200,7 @@ class WholesaleFeedSync extends EventEmitter {
197
200
  async refresh() {
198
201
  const epoch = this.lifecycleEpoch;
199
202
  this.emit("resyncing", { reason: "manual" });
200
- await this.bootstrap({ emitDiffs: true, epoch });
203
+ await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
201
204
  }
202
205
  /**
203
206
  * Apply one legacy-view account-level wholesale feed webhook to the local mirror.
@@ -256,8 +259,10 @@ class WholesaleFeedSync extends EventEmitter {
256
259
  try {
257
260
  if (!await this.recoverFromBulkChange(event, epoch)) return;
258
261
  } catch (err) {
259
- await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
260
- this.rememberLastWebhookEventId(event.event_id);
262
+ if (!(err instanceof Error && bootstrapReadFailures.has(err))) {
263
+ await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
264
+ this.rememberLastWebhookEventId(event.event_id);
265
+ }
261
266
  throw err;
262
267
  }
263
268
  await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
@@ -328,6 +333,8 @@ class WholesaleFeedSync extends EventEmitter {
328
333
  const epoch = options.epoch ?? this.lifecycleEpoch;
329
334
  if (!this.isLifecycleCurrent(epoch)) return false;
330
335
  this.setState("bootstrapping");
336
+ const previousLastSyncedAt = this._lastSyncedAt;
337
+ let reportedFailure;
331
338
  try {
332
339
  const previousProducts = new Map(this.productIndex);
333
340
  const previousSignals = new Map(this.signalIndex);
@@ -341,10 +348,32 @@ class WholesaleFeedSync extends EventEmitter {
341
348
  if (refreshProducts) {
342
349
  productResult = await this.bootstrapProducts(epoch);
343
350
  if (productResult.cancelled) return false;
351
+ if (productResult.failure) {
352
+ const failure = productResult.failure;
353
+ reportedFailure = failure.error;
354
+ this.handleBootstrapFailure(failure);
355
+ if (options.propagateFailure) {
356
+ bootstrapReadFailures.add(failure.error);
357
+ if (failure.adcpError) Object.assign(failure.error, { adcpError: failure.adcpError });
358
+ throw failure.error;
359
+ }
360
+ return false;
361
+ }
344
362
  }
345
363
  if (refreshSignals) {
346
364
  signalResult = await this.bootstrapSignals(epoch);
347
365
  if (signalResult.cancelled) return false;
366
+ if (signalResult.failure) {
367
+ const failure = signalResult.failure;
368
+ reportedFailure = failure.error;
369
+ this.handleBootstrapFailure(failure);
370
+ if (options.propagateFailure) {
371
+ bootstrapReadFailures.add(failure.error);
372
+ if (failure.adcpError) Object.assign(failure.error, { adcpError: failure.adcpError });
373
+ throw failure.error;
374
+ }
375
+ return false;
376
+ }
348
377
  }
349
378
  if (!this.isLifecycleCurrent(epoch)) return false;
350
379
  if (refreshProducts && productResult) {
@@ -378,13 +407,22 @@ class WholesaleFeedSync extends EventEmitter {
378
407
  return true;
379
408
  } catch (err) {
380
409
  if (!this.isLifecycleCurrent(epoch)) return false;
381
- this.setState("error");
410
+ if (err === reportedFailure) throw err;
411
+ this._lastSyncedAt = previousLastSyncedAt;
412
+ this.setState(this._lastSyncedAt ? "degraded" : "error");
382
413
  const error = err instanceof Error ? err : new Error(String(err));
383
414
  this.errorHandler?.(error);
384
- this.emit("error", { error });
415
+ if (this.listenerCount("error") > 0) this.emit("error", { error });
416
+ if (options.propagateFailure) bootstrapReadFailures.add(error);
385
417
  throw error;
386
418
  }
387
419
  }
420
+ handleBootstrapFailure(failure) {
421
+ this.setState(this._lastSyncedAt ? "degraded" : "error");
422
+ this.errorHandler?.(failure.error);
423
+ if (this.listenerCount("error") > 0) this.emit("error", failure);
424
+ return false;
425
+ }
388
426
  /** Returns `true` when the seller short-circuited with `unchanged: true`. */
389
427
  async bootstrapProducts(epoch) {
390
428
  let cursor;
@@ -402,8 +440,24 @@ class WholesaleFeedSync extends EventEmitter {
402
440
  }
403
441
  const result = await this.client.getProducts(params);
404
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
+ }
405
452
  const body = result.data;
406
- if (!body) return { cancelled: false, unchanged: false, items: into, metadata };
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
+ };
407
461
  if (body.unchanged) {
408
462
  metadata = mergeFeedMetadata(metadata, body);
409
463
  return { cancelled: false, unchanged: true, items: into, metadata };
@@ -434,8 +488,24 @@ class WholesaleFeedSync extends EventEmitter {
434
488
  }
435
489
  const result = await this.client.getSignals(params);
436
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
+ }
437
500
  const body = result.data;
438
- if (!body) return { cancelled: false, unchanged: false, items: into, metadata };
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
+ };
439
509
  if (body.unchanged) {
440
510
  metadata = mergeFeedMetadata(metadata, body);
441
511
  return { cancelled: false, unchanged: true, items: into, metadata };
@@ -459,13 +529,13 @@ class WholesaleFeedSync extends EventEmitter {
459
529
  );
460
530
  }
461
531
  const entities = affected === "product" ? "products" : "signals";
462
- return this.bootstrap({ emitDiffs: true, entities, epoch });
532
+ return this.bootstrap({ emitDiffs: true, entities, epoch, propagateFailure: true });
463
533
  }
464
534
  async recoverFromVersionMismatch(event, epoch = this.lifecycleEpoch) {
465
535
  this.emit("resyncing", { reason: "version_mismatch" });
466
536
  const beforeVersion = this.currentWholesaleFeedVersionForEvent(event);
467
537
  for (let attempt = 1; attempt <= VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
468
- const recovered = await this.bootstrap({ emitDiffs: true, epoch });
538
+ const recovered = await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
469
539
  if (!recovered) return false;
470
540
  const afterVersion = this.currentWholesaleFeedVersionForEvent(event);
471
541
  if (afterVersion !== beforeVersion) return true;
@@ -487,11 +557,12 @@ class WholesaleFeedSync extends EventEmitter {
487
557
  await this.probeVersion(epoch);
488
558
  } catch (err) {
489
559
  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);
560
+ if (this._state !== "degraded" && this._state !== "error") {
561
+ const error = err instanceof Error ? err : new Error(String(err));
562
+ this.handleBootstrapFailure({ error });
563
+ }
493
564
  }
494
- if (this.isLifecycleCurrent(epoch) && this._state === "syncing" && this._mode === "auto-poll") {
565
+ if (this.isLifecycleCurrent(epoch) && (this._state === "syncing" || this._state === "degraded" || this._state === "error") && this._mode === "auto-poll") {
495
566
  this.scheduleProbe(epoch);
496
567
  }
497
568
  }
@@ -518,7 +589,7 @@ class WholesaleFeedSync extends EventEmitter {
518
589
  this.emit("error", { error });
519
590
  this.errorHandler?.(error);
520
591
  }
521
- if (this.isLifecycleCurrent(epoch) && this._state === "syncing" && this.capabilityRefreshIntervalMs > 0) {
592
+ if (this.isLifecycleCurrent(epoch) && (this._state === "syncing" || this._state === "degraded" || this._state === "error") && this.capabilityRefreshIntervalMs > 0) {
522
593
  this.scheduleCapabilityRefresh(epoch);
523
594
  }
524
595
  }
@@ -22,7 +22,7 @@ export type WholesaleFeedSyncMode = 'manual' | 'auto-poll';
22
22
  /**
23
23
  * Lifecycle state of the sync engine.
24
24
  */
25
- export type WholesaleFeedSyncState = 'idle' | 'bootstrapping' | 'syncing' | 'error';
25
+ export type WholesaleFeedSyncState = 'idle' | 'bootstrapping' | 'syncing' | 'degraded' | 'error';
26
26
  /**
27
27
  * Subset of `SingleAgentClient` that {@link WholesaleFeedSync} actually uses.
28
28
  * Lets tests inject a minimal stub without constructing a full client.
@@ -178,8 +178,8 @@ export interface ResolvedCapabilities {
178
178
  * picks a mode. Useful for UI mode badges.
179
179
  * - `resyncing` — emitted before a `wholesale_feed.bulk_change` recovery
180
180
  * re-bootstrap, webhook-version mismatch repair, or manual refresh.
181
- * - `error` — background poll/probe error. Non-fatal; sync stays in
182
- * `'syncing'` and retries on the next tick.
181
+ * - `error` — initial bootstrap, re-sync, or background probe failure.
182
+ * Failed refreshes preserve the last good mirror and retry on the next tick.
183
183
  * - `stateChange` — fires on every {@link WholesaleFeedSyncState} transition.
184
184
  */
185
185
  export interface WholesaleFeedSyncEvents {
@@ -200,6 +200,7 @@ export interface WholesaleFeedSyncEvents {
200
200
  }];
201
201
  error: [{
202
202
  error: Error;
203
+ adcpError?: import('../core/ConversationTypes.mjs').AdcpErrorInfo;
203
204
  }];
204
205
  stateChange: [{
205
206
  from: WholesaleFeedSyncState;
@@ -22,7 +22,7 @@ export type WholesaleFeedSyncMode = 'manual' | 'auto-poll';
22
22
  /**
23
23
  * Lifecycle state of the sync engine.
24
24
  */
25
- export type WholesaleFeedSyncState = 'idle' | 'bootstrapping' | 'syncing' | 'error';
25
+ export type WholesaleFeedSyncState = 'idle' | 'bootstrapping' | 'syncing' | 'degraded' | 'error';
26
26
  /**
27
27
  * Subset of `SingleAgentClient` that {@link WholesaleFeedSync} actually uses.
28
28
  * Lets tests inject a minimal stub without constructing a full client.
@@ -178,8 +178,8 @@ export interface ResolvedCapabilities {
178
178
  * picks a mode. Useful for UI mode badges.
179
179
  * - `resyncing` — emitted before a `wholesale_feed.bulk_change` recovery
180
180
  * re-bootstrap, webhook-version mismatch repair, or manual refresh.
181
- * - `error` — background poll/probe error. Non-fatal; sync stays in
182
- * `'syncing'` and retries on the next tick.
181
+ * - `error` — initial bootstrap, re-sync, or background probe failure.
182
+ * Failed refreshes preserve the last good mirror and retry on the next tick.
183
183
  * - `stateChange` — fires on every {@link WholesaleFeedSyncState} transition.
184
184
  */
185
185
  export interface WholesaleFeedSyncEvents {
@@ -200,6 +200,7 @@ export interface WholesaleFeedSyncEvents {
200
200
  }];
201
201
  error: [{
202
202
  error: Error;
203
+ adcpError?: import('../core/ConversationTypes').AdcpErrorInfo;
203
204
  }];
204
205
  stateChange: [{
205
206
  from: WholesaleFeedSyncState;
@@ -1,7 +1,7 @@
1
1
  # AdCP Type Summary
2
2
 
3
- > Generated at: 2026-10-01
4
- > @adcp/sdk v14.0.0
3
+ > Generated at: 2026-10-04
4
+ > @adcp/sdk v14.1.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
 
@@ -671,7 +671,7 @@ import {
671
671
  requireAuthenticatedOrSigned,
672
672
  mcpToolNameResolver,
673
673
  } from '@adcp/sdk/server';
674
- import { BrandJsonJwksResolver } from '@adcp/sdk/signing/server';
674
+ import { ResolvedAgentJwksResolver } from '@adcp/sdk/signing/server';
675
675
 
676
676
  serve(
677
677
  () =>
@@ -692,7 +692,7 @@ serve(
692
692
  authenticate: requireAuthenticatedOrSigned({
693
693
  signature: verifySignatureAsAuthenticator({
694
694
  capability: { supported: true, required_for: ['create_media_buy', 'update_media_buy'], covers_content_digest: 'either' },
695
- jwks: new BrandJsonJwksResolver(),
695
+ jwks: new ResolvedAgentJwksResolver('https://buyer.example/mcp', 'mcp'),
696
696
  resolveOperation: mcpToolNameResolver,
697
697
  }),
698
698
  fallback: verifyApiKey({ keys: { sk_live_abc: { principal: 'acct_42' } } }),
@@ -1,5 +1,7 @@
1
1
  # Call a seller with AdCP 3.2
2
2
 
3
+ For a new seller, start with [account setup and public discovery](./FIRST-CALL-TO-A-SELLER.md).
4
+
3
5
  For product possibility, accepted change rights, and current execution routes, use the [MediaBuy action assessment helpers](./MEDIA-BUY-ACTION-ASSESSMENT.md).
4
6
 
5
7
  Requires Node.js `^20.19.0 || >=22.12.0`. Install SDK 14
@@ -0,0 +1,104 @@
1
+ # First call to a seller
2
+
3
+ An account reference selects a provisioned account at that seller. Discovery
4
+ does not provision it. Until setup is complete, send a top-level `brand` on
5
+ tools that support public discovery, and omit `account`.
6
+
7
+ ```ts
8
+ import { AgentClient, createProductCache } from '@adcp/sdk';
9
+
10
+ const client = new AgentClient(sellerConfig, {
11
+ accountPolicy: 'auto',
12
+ productCache: createProductCache({ publicTtl: 60_000, accountTtl: 30_000 }),
13
+ });
14
+ const capabilities = await client.getCapabilities();
15
+ const brand = { domain: 'advertiser.example' };
16
+ const account = { brand, operator: 'agency.example' };
17
+
18
+ // Only use public discovery when the seller permits it.
19
+ if (!capabilities.account?.requiredForProducts) {
20
+ const publicCatalog = await client.getProducts({ brand, brief: 'Display inventory' });
21
+ if (!publicCatalog.success) console.error(publicCatalog.adcpError);
22
+ }
23
+
24
+ // Choose billing terms before accepting them. This returns seller status and handle.
25
+ const setup = await client.accounts.ensure(account, {
26
+ billing: 'operator',
27
+ paymentTerms: 'net_30',
28
+ });
29
+ if (setup.status === 'active') {
30
+ const pricedCatalog = await client.getProducts({ account, brand, brief: 'Display inventory' });
31
+ if (!pricedCatalog.success) console.error(pricedCatalog.adcpError);
32
+ }
33
+ ```
34
+
35
+ For agent-billed provisioning, pass `billingEntity` with the seller's required
36
+ business identity. `paymentTerms` and `billingEntity` are also accepted by
37
+ `resolveAccount()`. That helper keeps its existing pending-approval exception;
38
+ `accounts.ensure()` returns the status so callers can present the setup flow.
39
+ Non-active entries refresh through `list_accounts` when a seller handle is known.
40
+ An existing registry entry prevents repeated provisioning and silent acceptance
41
+ of new terms. To change terms, explicitly call `syncAccounts()`.
42
+ An explicit `ensure` with conflicting known setup terms refuses the change.
43
+ If the seller no longer lists an account, explicitly reestablish it with
44
+ `syncAccounts` after choosing terms; status repair never silently re-provisions it.
45
+ Caller cancellation stops waiting for setup; the single seller operation continues
46
+ so another caller cannot accidentally accept different terms concurrently.
47
+ Terminal setup failures permit an explicit retry. For interrupted async setup,
48
+ read `accounts.get(key).pendingTaskId`, reconcile that seller task, and pass its
49
+ completed rows to `accounts.observeSync([key], rows)` (or let the original
50
+ `syncAccounts` completion handle update the registry). Do not resubmit unresolved tasks.
51
+
52
+ For a seller that requires operator authentication, use `resolveAccount()` to
53
+ select an active account returned by `list_accounts`. An account ID echoed by
54
+ `sync_accounts` is a seller handle; implicit sellers still require the natural
55
+ key on subsequent calls. Do not replace it with `{ account_id }` automatically.
56
+
57
+ `accountPolicy` defaults to `'off'` to preserve existing requests. Opt-in
58
+ `'auto'` omits unknown accounts on public discovery tools that support `brand`;
59
+ it refuses required-account discovery, async discovery, and mutations until
60
+ the registry knows the key. `'strict'` refuses every unknown account-carrying
61
+ request. Both policies require an active account before spend commitments.
62
+ Explicit account filters on `list_accounts` remain filters.
63
+ Call `listAccounts` first when adopting these policies with existing opaque
64
+ account IDs so the registry knows those seller handles.
65
+
66
+ Provisioning is remembered per seller and caller. `syncAccounts()` and
67
+ `listAccounts()` update the registry on completed results; failed rows and dry
68
+ runs do not establish accounts. After verifying an `account.status_changed`
69
+ notification, call `client.accounts.applyStatusChange({ account_id })` to repair
70
+ from authoritative `list_accounts`. The notification's status is not trusted as
71
+ a snapshot, which avoids reordered deliveries overwriting current state.
72
+
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,
75
+ 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
+ for continuity across token rotations, supply `accountRegistryScope` from a
77
+ trusted stable principal identifier. Never share that scope between tenants.
78
+ Request-signing key identity is also part of the default fingerprint, so distinct
79
+ signers stay isolated. Key rotation requires reprovisioning or a trusted stable
80
+ `accountRegistryScope` for the same principal.
81
+ Registry memory and aliases per handle are bounded by `accountRegistryMaxEntries` (default 10,000); durable adapters allow entry eviction and reload. Without storage, reaching capacity throws a setup error; raise the limit or configure storage for large rosters. `resolveAccount()` uses the memoized registry only when you opt in with `accountPolicy: 'auto' | 'strict'`, `accountStorage`, `accountRegistryScope`, or `accountRegistryMaxEntries`; that internal use does not enable observations of unrelated rosters. Otherwise it keeps the 14.0 behavior: every call sends `sync_accounts` and reflects the seller's current status. Memoized entries require explicit `syncAccounts()` or authoritative status repair when seller linkage expires or is replaced externally. Stored entries contain account references, seller handles, status, optional pending task IDs, and hashes of explicitly chosen setup terms;
82
+ billing entities and tokens are not persisted. Manual feed mode recovers through
83
+ an explicit `refresh()`; auto-poll mode also retries initial failures.
84
+
85
+ Typed `AccountNotFoundError`, `AccountSetupRequiredError`, and
86
+ `AccountPaymentRequiredError` carry `fault: 'buyer_setup'`. Health trackers
87
+ should exclude them from seller-health failure counts. Failed task results still
88
+ expose the protocol code in `result.adcpError` and the typed exception in
89
+ `result.errorInstance`.
90
+
91
+ The optional product cache uses TTLs in milliseconds and keeps at most `maxEntries` entries (default 1,000). It keeps public and
92
+ account-scoped responses apart and stores the feed version and pricing version
93
+ with each snapshot. Missing `cache_scope` prevents caching. Stale entries are
94
+ conditionally revalidated using their own tokens; failures remain visible to
95
+ the caller and never erase the last valid entry. Caller-authored feed or pricing validators
96
+ retain their unchanged-response semantics. Caches are scoped to the client
97
+ instance so different local verification policies cannot share filtered results.
98
+
99
+ `WholesaleFeedSync` similarly keeps its last good product and signal mirrors on
100
+ a failed refresh. Its error event includes `adcpError`; a mirror becomes
101
+ `degraded` until a successful refresh restores `syncing`. Initial failure sets
102
+ `error`. Exhaustive state switches must handle the new `degraded` member.
103
+
104
+ Manual `WholesaleFeedSync.refresh()` and webhook repairs reject failed catalog reads, including failed task results. A failed webhook repair remains eligible for redelivery; acknowledge it only after repair succeeds. Bootstrap `start()` still reports failed task results through its state and error callback/event, and schedules recovery in auto-poll mode.