@adcp/sdk 15.9.5 → 15.9.6

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 (44) hide show
  1. package/dist/lib/core/AgentClient.d.mts +4 -2
  2. package/dist/lib/core/AgentClient.d.ts +4 -2
  3. package/dist/lib/core/AgentClient.js +4 -2
  4. package/dist/lib/core/AgentClient.mjs +4 -2
  5. package/dist/lib/core/SingleAgentClient.d.mts +7 -3
  6. package/dist/lib/core/SingleAgentClient.d.ts +7 -3
  7. package/dist/lib/core/SingleAgentClient.js +204 -13
  8. package/dist/lib/core/SingleAgentClient.mjs +209 -13
  9. package/dist/lib/core/product-cache.d.mts +3 -1
  10. package/dist/lib/core/product-cache.d.ts +3 -1
  11. package/dist/lib/core/product-cache.js +2 -1
  12. package/dist/lib/core/product-cache.mjs +2 -1
  13. package/dist/lib/registry/types.d.mts +3 -2
  14. package/dist/lib/registry/types.d.ts +3 -2
  15. package/dist/lib/registry/types.generated.d.mts +21 -9
  16. package/dist/lib/registry/types.generated.d.ts +21 -9
  17. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  18. package/dist/lib/server/decisioning/runtime/from-platform.js +6 -2
  19. package/dist/lib/server/decisioning/runtime/from-platform.mjs +6 -2
  20. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  21. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  22. package/dist/lib/v2/projection/augment-response.js +6 -3
  23. package/dist/lib/v2/projection/augment-response.mjs +8 -5
  24. package/dist/lib/v2/projection/creative-delivery.d.mts +2 -0
  25. package/dist/lib/v2/projection/creative-delivery.d.ts +2 -0
  26. package/dist/lib/v2/projection/creative-delivery.js +36 -17
  27. package/dist/lib/v2/projection/creative-delivery.mjs +37 -18
  28. package/dist/lib/v2/projection/legacy-metadata.d.mts +12 -0
  29. package/dist/lib/v2/projection/legacy-metadata.d.ts +12 -0
  30. package/dist/lib/v2/projection/legacy-metadata.js +41 -1
  31. package/dist/lib/v2/projection/legacy-metadata.mjs +37 -1
  32. package/dist/lib/v2/projection/v1-to-v2.d.mts +7 -0
  33. package/dist/lib/v2/projection/v1-to-v2.d.ts +7 -0
  34. package/dist/lib/v2/projection/v1-to-v2.js +83 -2
  35. package/dist/lib/v2/projection/v1-to-v2.mjs +86 -1
  36. package/dist/lib/version.d.mts +3 -3
  37. package/dist/lib/version.d.ts +3 -3
  38. package/dist/lib/version.js +3 -3
  39. package/dist/lib/version.mjs +3 -3
  40. package/docs/TYPE-SUMMARY.md +2 -2
  41. package/docs/guides/CREATIVE-DELIVERY.md +45 -6
  42. package/docs/llms.txt +2 -2
  43. package/docs/migration-14.x-rc-worksheet.md +4 -4
  44. package/package.json +1 -1
@@ -159,6 +159,7 @@ import { resolvePropertyList } from "../server/targeting-helpers.mjs";
159
159
  import {
160
160
  CreativeFormatCapabilityError,
161
161
  CreativeFormatProjectionError,
162
+ canUseProductOptionsForCreativeWire,
162
163
  projectMediaBuyCreativesForDelivery,
163
164
  projectCreativeForDelivery,
164
165
  projectSyncCreativesForDelivery,
@@ -176,6 +177,10 @@ import {
176
177
  import { toCanonicalOnlyResponse } from "../v2/projection/augment-response.mjs";
177
178
  import {
178
179
  legacyFormatRefsForDeclaration,
180
+ isSynthesizedFormatOption,
181
+ selectedFormatOptions,
182
+ rememberSellerFormatEvidence,
183
+ sellerFormatEvidence,
179
184
  rememberSellerDeclaredFormatOption,
180
185
  sellerDeclaredFormatOption,
181
186
  structuredCloneWithLegacyCreativeMetadata
@@ -299,6 +304,56 @@ const RECORDLESS_HMAC_READ_ONLY_TASKS = /* @__PURE__ */ new Set([
299
304
  ]);
300
305
  const TASK_SCOPED_STATE_LIMIT = 1e4;
301
306
  const WEBHOOK_JWKS_RESOLVER_LIMIT = 1024;
307
+ function creativeMatchesProductOption(creative, declaration) {
308
+ const params = isPlainRecord(declaration.params) ? declaration.params : {};
309
+ const assets = isPlainRecord(creative.assets) ? creative.assets : {};
310
+ const slots = Array.isArray(params.slots) ? params.slots : [];
311
+ if (!slots.every((slot) => {
312
+ if (!isPlainRecord(slot) || typeof slot.asset_group_id !== "string") return false;
313
+ const value = assets[slot.asset_group_id];
314
+ const entries = value === void 0 ? [] : Array.isArray(value) ? value : [value];
315
+ const minimum = Math.max(typeof slot.min === "number" ? slot.min : 0, slot.required === true ? 1 : 0);
316
+ if (entries.length < minimum || typeof slot.max === "number" && entries.length > slot.max) return false;
317
+ return entries.every(
318
+ (asset) => isPlainRecord(asset) && (typeof slot.asset_type !== "string" || asset.asset_type === slot.asset_type)
319
+ );
320
+ }))
321
+ return false;
322
+ const formatParams = isPlainRecord(creative.format_parameters) ? creative.format_parameters : {};
323
+ const kind = String(creative.format_kind);
324
+ const mediaType = kind.startsWith("image") ? "image" : kind.startsWith("video") ? "video" : kind.startsWith("audio") ? "audio" : kind === "html5" ? "html" : void 0;
325
+ const assetTypes = mediaType === "video" ? ["video", "vast"] : mediaType === "audio" ? ["audio", "vast", "daast"] : mediaType === "html" ? ["html", "zip"] : mediaType ? [mediaType] : ["image", "video", "audio", "html", "zip", "vast", "daast"];
326
+ const requiredMediaSlots = slots.filter(
327
+ (slot) => isPlainRecord(slot) && (slot.required === true || typeof slot.min === "number" && slot.min > 0) && assetTypes.includes(String(slot.asset_type))
328
+ );
329
+ const preferredGroups = mediaType ? [`${mediaType}_main`, mediaType, kind, ...assetTypes] : [];
330
+ const primaryGroup = (requiredMediaSlots.length === 1 ? String(requiredMediaSlots[0].asset_group_id) : void 0) ?? preferredGroups.find((group) => assets[group] !== void 0);
331
+ const sourceAssets = primaryGroup ? [assets[primaryGroup]] : Object.values(assets);
332
+ const dimensionalAssets = sourceAssets.flatMap((value) => Array.isArray(value) ? value : [value]).filter(
333
+ (value) => isPlainRecord(value) && assetTypes.includes(String(value.asset_type))
334
+ );
335
+ return (dimensionalAssets.length > 0 ? dimensionalAssets : [{}]).every((asset) => {
336
+ const actual = { ...asset, ...formatParams };
337
+ for (const key of ["width", "height"]) {
338
+ if (typeof params[key] === "number" && actual[key] !== params[key]) return false;
339
+ const min = params[`min_${key}`];
340
+ const max = params[`max_${key}`];
341
+ if (typeof min === "number" && (typeof actual[key] !== "number" || actual[key] < min)) return false;
342
+ if (typeof max === "number" && (typeof actual[key] !== "number" || actual[key] > max)) return false;
343
+ }
344
+ if (Array.isArray(params.sizes) && !params.sizes.some((size) => isPlainRecord(size) && size.width === actual.width && size.height === actual.height))
345
+ return false;
346
+ const duration = actual.duration_ms ?? actual.duration_ms_exact;
347
+ const exact = params.duration_ms_exact ?? params.duration_ms;
348
+ if (typeof exact === "number" && duration !== exact) return false;
349
+ if (typeof exact !== "number" && Array.isArray(params.duration_ms_range)) {
350
+ const [min, max] = params.duration_ms_range;
351
+ if (typeof duration !== "number" || typeof min === "number" && duration < min || typeof max === "number" && duration > max)
352
+ return false;
353
+ }
354
+ return true;
355
+ });
356
+ }
302
357
  function isDeferredClientFinalizationContext(value) {
303
358
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
304
359
  const context = value;
@@ -1431,7 +1486,7 @@ class SingleAgentClient {
1431
1486
  }
1432
1487
  }
1433
1488
  }
1434
- rememberCanonicalProductRoutes(products, account, authoritativeProducts) {
1489
+ rememberCanonicalProductRoutes(products, account, authoritativeProducts, evidence) {
1435
1490
  this.invalidateCanonicalProductRoutes(authoritativeProducts ?? products, account);
1436
1491
  const accountScope = this.canonicalAccountScope(account);
1437
1492
  const declaredOptions = authoritativeProducts ? this.sellerDeclaredFormatOptions(authoritativeProducts) : void 0;
@@ -1451,12 +1506,15 @@ class SingleAgentClient {
1451
1506
  const refs = legacyFormatRefsForDeclaration(option);
1452
1507
  const declared = declaredOptions ? declaredOptions.get(product.product_id)?.some((declaredRef) => this.sameCanonicalOptionRef(declaredRef, ref)) : sellerDeclaredFormatOption(option);
1453
1508
  if (declared !== void 0) rememberSellerDeclaredFormatOption(option, declared);
1509
+ if (declared === true && evidence)
1510
+ rememberSellerFormatEvidence(option, { ...evidence, productId: product.product_id });
1454
1511
  this.rememberCanonicalLegacyRoute(this.canonicalLegacyRouteKey(account, product.product_id, ref), {
1455
1512
  kind: "product",
1456
1513
  accountScope,
1457
1514
  productId: product.product_id,
1458
1515
  optionRef: ref,
1459
1516
  refs,
1517
+ ...declared === true && sellerFormatEvidence(option) ? { declaration: structuredCloneWithLegacyCreativeMetadata(option) } : {},
1460
1518
  ...declared === false ? { undeclaredSelector: undeclaredFormatOptionSelector(option) } : {}
1461
1519
  });
1462
1520
  }
@@ -4666,6 +4724,13 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4666
4724
  }
4667
4725
  }
4668
4726
  const conditionalSnapshot = skipCacheRead ? void 0 : cache?.read(cacheScope, cacheParams, true);
4727
+ const productEvidence = this.usesScopedDiscoveryTransport(
4728
+ effectiveOptions
4729
+ ) ? void 0 : {
4730
+ scopeKey: this.getCapabilityEvidenceScope().scopeKey,
4731
+ accountScope: this.canonicalAccountScope(account),
4732
+ expiresAt: Date.now() + resolveAgentDiscoveryTtlMs(this.config.discoveryCache?.ttlMs)
4733
+ };
4669
4734
  const result = await this.executeAndHandle(
4670
4735
  "get_products",
4671
4736
  "onGetProductsStatusChange",
@@ -4678,7 +4743,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4678
4743
  projectionCatalogs: projectionCatalogs ?? this.config.projectionCatalogs
4679
4744
  });
4680
4745
  const authoritativeProducts = Array.isArray(data.products) ? data.products : response.products;
4681
- this.rememberCanonicalProductRoutes(response.products, account, authoritativeProducts);
4746
+ this.rememberCanonicalProductRoutes(response.products, account, authoritativeProducts, productEvidence);
4682
4747
  const { _message: _dropLegacyMessage, ...canonical } = response;
4683
4748
  void _dropLegacyMessage;
4684
4749
  return { ...canonical, projection: { diagnostics } };
@@ -4689,7 +4754,9 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4689
4754
  );
4690
4755
  const finishCache = async (completed) => {
4691
4756
  if (!cache) return completed;
4692
- const cachedResult = cache.write(cacheScope, cacheParams, completed, conditionalSnapshot);
4757
+ const cachedResult = cache.write(cacheScope, cacheParams, completed, conditionalSnapshot, (materialized) => {
4758
+ this.rememberCanonicalProductRoutes(materialized.data?.products ?? [], account, void 0, productEvidence);
4759
+ });
4693
4760
  if (cachedResult.success && cachedResult.status === "completed") {
4694
4761
  if (!skipCacheRead && cacheParams.if_wholesale_feed_version === void 0 && cacheParams.if_pricing_version === void 0 && cachedResult.data.unchanged === true) {
4695
4762
  return this.getProducts(params, inputHandler, {
@@ -4849,7 +4916,12 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4849
4916
  if (hasCreativeFormatData) {
4850
4917
  this.validateBeforeCreativeCapabilityProbe("create_media_buy", params, taskOptions);
4851
4918
  }
4852
- const wireMode = hasCreativeFormatData ? await this.resolveCreativeFormatWireMode("create_media_buy", taskOptions) : "canonical";
4919
+ const wireMode = hasCreativeFormatData ? await this.resolveCreativeWireMode(
4920
+ "create_media_buy",
4921
+ taskOptions,
4922
+ true,
4923
+ () => this.productOptionsSupportCanonicalWire("create_media_buy", params, [], options)
4924
+ ) : "canonical";
4853
4925
  const wireParams = params;
4854
4926
  const result = await this.executeAndHandle(
4855
4927
  "create_media_buy",
@@ -4937,7 +5009,12 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4937
5009
  if (hasCreativeFormatData) {
4938
5010
  this.validateBeforeCreativeCapabilityProbe("update_media_buy", params, taskOptions);
4939
5011
  }
4940
- const wireMode = hasCreativeFormatData ? await this.resolveCreativeFormatWireMode("update_media_buy", taskOptions) : "canonical";
5012
+ const wireMode = hasCreativeFormatData ? await this.resolveCreativeWireMode(
5013
+ "update_media_buy",
5014
+ taskOptions,
5015
+ true,
5016
+ () => this.productOptionsSupportCanonicalWire("update_media_buy", params, [], options)
5017
+ ) : "canonical";
4941
5018
  const result = await this.executeAndHandle(
4942
5019
  "update_media_buy",
4943
5020
  "onUpdateMediaBuyStatusChange",
@@ -5026,7 +5103,6 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
5026
5103
  projectionCatalogs ?? this.config.projectionCatalogs
5027
5104
  );
5028
5105
  this.validateBeforeCreativeCapabilityProbe("sync_creatives", params, taskOptions);
5029
- const wireMode = await this.resolveCreativeFormatWireMode("sync_creatives", taskOptions);
5030
5106
  const configuredSelectorContainers = [
5031
5107
  ...creativeFormatProjection?.selectorContainers ?? []
5032
5108
  ];
@@ -5038,10 +5114,17 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
5038
5114
  const assignmentPackageContainers = Array.isArray(params.assignments) ? params.assignments.flatMap(
5039
5115
  (assignment) => typeof assignment.package_id === "string" && !configuredPackageIds.has(assignment.package_id) ? [{ package_id: assignment.package_id }] : []
5040
5116
  ) : [];
5117
+ const selectorContainers = [...configuredSelectorContainers, ...assignmentPackageContainers];
5118
+ const wireMode = await this.resolveCreativeWireMode(
5119
+ "sync_creatives",
5120
+ taskOptions,
5121
+ true,
5122
+ () => this.productOptionsSupportCanonicalWire("sync_creatives", params, selectorContainers, options)
5123
+ );
5041
5124
  const wireParams = this.projectUndeclaredFormatOptions(
5042
5125
  projectSyncCreativesForDelivery(
5043
5126
  params,
5044
- [...configuredSelectorContainers, ...assignmentPackageContainers],
5127
+ selectorContainers,
5045
5128
  wireMode,
5046
5129
  effectiveLegacyFormatConverter,
5047
5130
  effectiveCanonicalFormatLegacyResolver
@@ -5081,16 +5164,17 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
5081
5164
  * sending a mutation. Discovers capabilities and the current tool's input
5082
5165
  * schema, or reuses fresh cached/primed evidence, with this client's wire pin.
5083
5166
  *
5084
- * `unknown` preserves the conservative legacy projection for older peers
5085
- * whose capabilities and schema do not establish support. Projection still
5086
- * requires an unambiguous legacy reference for each creative in that case.
5167
+ * This context-free check returns `unknown` when capabilities and schema do
5168
+ * not establish support. A write to a proven 3.1 seller can still select
5169
+ * canonical delivery from matching seller-advertised product options;
5170
+ * otherwise projection requires an unambiguous legacy reference.
5087
5171
  * Conflicting evidence and unproven support with a 3.2+ buyer pin throw
5088
5172
  * `CreativeFormatCapabilityError`, just as they do during a write.
5089
5173
  */
5090
5174
  async resolveCreativeFormatWireMode(taskType, options) {
5091
5175
  return this.resolveCreativeWireMode(taskType, options, true);
5092
5176
  }
5093
- async resolveCreativeWireMode(taskType, options, canRediscover) {
5177
+ async resolveCreativeWireMode(taskType, options, canRediscover, matchingProductOptions) {
5094
5178
  const discoveryContext = {};
5095
5179
  const capabilities = await this.getCapabilities({
5096
5180
  ...options,
@@ -5109,13 +5193,16 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
5109
5193
  `Seller capability and ${taskType} input schema disagree about canonical creative support`
5110
5194
  );
5111
5195
  }
5112
- const resolved = declared !== "unknown" ? declared : schema;
5196
+ let resolved = declared !== "unknown" ? declared : schema;
5197
+ if (resolved === "unknown" && canUseProductOptionsForCreativeWire(capabilities, wireRelease) && matchingProductOptions?.()) {
5198
+ resolved = "canonical";
5199
+ }
5113
5200
  const release = /^v?(\d+)\.(\d+)(?:\.|-|$)/.exec(wireRelease.trim());
5114
5201
  const canonicalRequiredByBuyerPin = release !== null && (Number(release[1]) > 3 || Number(release[1]) === 3 && Number(release[2]) >= 2);
5115
5202
  if (resolved === "unknown" && canonicalRequiredByBuyerPin) {
5116
5203
  if (canRediscover && !usesScopedFetch && this.normalizedAgent.protocol === "mcp" && this.capabilitiesFromSharedCache && toolSchemas?.has(taskType) && !toolInputSchemasByProperties.has(toolSchemas)) {
5117
5204
  await this.invalidateDiscoveryCache(options);
5118
- return this.resolveCreativeWireMode(taskType, options, false);
5205
+ return this.resolveCreativeWireMode(taskType, options, false, matchingProductOptions);
5119
5206
  }
5120
5207
  throw new CreativeFormatCapabilityError(
5121
5208
  `Cannot prove which AdCP release the seller serves for ${taskType}; a 3.2+ client will not guess a legacy creative wire shape`
@@ -5123,6 +5210,115 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
5123
5210
  }
5124
5211
  return resolved;
5125
5212
  }
5213
+ usesScopedDiscoveryTransport(options) {
5214
+ return normalizeTransportOptions(options?.transport ?? this.config.transport)?.trustedFetchFn !== void 0;
5215
+ }
5216
+ /** Product evidence applies to this request only; it must never prime a tool-wide capability. */
5217
+ productOptionsSupportCanonicalWire(taskType, request, selectorContainers = [], options) {
5218
+ if (!isPlainRecord(request) || this.usesScopedDiscoveryTransport(options)) return false;
5219
+ const scopeKey = this.getCapabilityEvidenceScope().scopeKey;
5220
+ const accountScope = this.canonicalAccountScope(request.account);
5221
+ const optionRef = (value) => {
5222
+ if (!isPlainRecord(value) || typeof value.format_option_id !== "string" || !value.format_option_id) {
5223
+ return void 0;
5224
+ }
5225
+ if (typeof value.publisher_domain === "string") {
5226
+ return {
5227
+ scope: "publisher",
5228
+ publisher_domain: value.publisher_domain,
5229
+ format_option_id: value.format_option_id
5230
+ };
5231
+ }
5232
+ if (value.scope !== void 0 && value.scope !== "product") return void 0;
5233
+ return { scope: "product", format_option_id: value.format_option_id };
5234
+ };
5235
+ const accepts = (container, creative, depth = 0) => {
5236
+ if (depth > 32) return false;
5237
+ if (Array.isArray(container.selector_containers)) {
5238
+ return container.selector_containers.length > 0 && container.selector_containers.every((nested) => accepts(nested, creative, depth + 1));
5239
+ }
5240
+ const refs = Array.isArray(container.format_option_refs) ? container.format_option_refs.map(optionRef) : [];
5241
+ if (refs.some((ref) => ref === void 0)) return false;
5242
+ const creativeRef = isPlainRecord(creative) ? optionRef(creative.format_option_ref) : void 0;
5243
+ if (isPlainRecord(creative) && creative.format_option_ref !== void 0 && !creativeRef) return false;
5244
+ const isSellerDeclaration = (value) => {
5245
+ if (!isPlainRecord(value) || sellerDeclaredFormatOption(value) !== true || isSynthesizedFormatOption(value)) {
5246
+ return false;
5247
+ }
5248
+ const evidence2 = sellerFormatEvidence(value);
5249
+ if (!evidence2 || evidence2.scopeKey !== scopeKey || typeof container.product_id === "string" && evidence2.productId !== container.product_id || evidence2.expiresAt <= Date.now() || evidence2.accountScope !== accountScope && evidence2.accountScope !== "none")
5250
+ return false;
5251
+ const ref = optionRef(value);
5252
+ if (ref && formatOptionOriginFromRoutes(
5253
+ options?.canonicalFormatLegacyResolver,
5254
+ ref,
5255
+ typeof container.product_id === "string" ? container.product_id : void 0
5256
+ )?.seller_declared === false)
5257
+ return false;
5258
+ return !ref || typeof container.product_id !== "string" || this.cachedFormatOptionOrigin(request.account, ref, container.product_id)?.seller_declared !== false;
5259
+ };
5260
+ const declarations = [
5261
+ ...(Array.isArray(container.format_options) ? container.format_options : []).filter(isSellerDeclaration),
5262
+ ...selectedFormatOptions(container).filter(isSellerDeclaration)
5263
+ ];
5264
+ if (typeof container.product_id === "string") {
5265
+ for (const ref of [...refs, ...creativeRef ? [creativeRef] : []]) {
5266
+ if (!ref) continue;
5267
+ const route = this.routeForOption(request.account, container.product_id, ref);
5268
+ if (route?.kind === "product" && route.declaration && isSellerDeclaration(route.declaration))
5269
+ declarations.push(route.declaration);
5270
+ }
5271
+ }
5272
+ const available = declarations.filter((declaration) => declaration.experimental !== true);
5273
+ const matchesRef = (declaration, ref) => {
5274
+ const declaredRef = optionRef(declaration);
5275
+ return declaredRef !== void 0 && this.sameCanonicalOptionRef(declaredRef, ref);
5276
+ };
5277
+ if (!refs.every((ref) => available.some((declaration) => matchesRef(declaration, ref)))) return false;
5278
+ const selected = available.filter(
5279
+ (declaration) => refs.length === 0 || refs.some((ref) => matchesRef(declaration, ref))
5280
+ );
5281
+ if (creative === void 0) return refs.length > 0 && selected.length > 0;
5282
+ if (!isPlainRecord(creative) || typeof creative.format_kind !== "string" || creative.format_id !== void 0)
5283
+ return false;
5284
+ return selected.some(
5285
+ (declaration) => declaration.format_kind === creative.format_kind && (!creativeRef || matchesRef(declaration, creativeRef)) && creativeMatchesProductOption(creative, declaration)
5286
+ );
5287
+ };
5288
+ if (taskType === "sync_creatives") {
5289
+ if (!Array.isArray(request.creatives) || request.creatives.length === 0) return false;
5290
+ const creatives = request.creatives;
5291
+ const assignments = Array.isArray(request.assignments) ? request.assignments : [];
5292
+ if (!assignments.every(
5293
+ (value) => isPlainRecord(value) && typeof value.package_id === "string" && typeof value.creative_id === "string" && creatives.some((creative) => isPlainRecord(creative) && creative.creative_id === value.creative_id)
5294
+ ))
5295
+ return false;
5296
+ return creatives.every((creative) => {
5297
+ if (!isPlainRecord(creative)) return false;
5298
+ const assigned = new Set(
5299
+ assignments.flatMap(
5300
+ (value) => isPlainRecord(value) && value.creative_id === creative.creative_id && typeof value.package_id === "string" ? [value.package_id] : []
5301
+ )
5302
+ );
5303
+ const relevant = assigned.size === 0 ? selectorContainers : selectorContainers.filter(
5304
+ (container) => typeof container.package_id === "string" && assigned.has(container.package_id)
5305
+ );
5306
+ return relevant.length > 0 && [...assigned].every((id) => relevant.some((container) => container.package_id === id)) && relevant.every((container) => accepts(container, creative));
5307
+ });
5308
+ }
5309
+ const packages = ["packages", "new_packages"].flatMap((key) => Array.isArray(request[key]) ? request[key] : []);
5310
+ let evidence = false;
5311
+ return packages.length > 0 && packages.every((pkg) => {
5312
+ if (!isPlainRecord(pkg)) return false;
5313
+ const creatives = Array.isArray(pkg.creatives) ? pkg.creatives : [];
5314
+ const hasSelectors = Array.isArray(pkg.format_option_refs) && pkg.format_option_refs.length > 0;
5315
+ if (!hasSelectors && creatives.length === 0) {
5316
+ return pkg.format_kind === void 0 && pkg.format_ids === void 0;
5317
+ }
5318
+ evidence = true;
5319
+ return (!hasSelectors || accepts(pkg)) && creatives.every((creative) => accepts(pkg, creative));
5320
+ }) && evidence;
5321
+ }
5126
5322
  /** Validate malformed creative payloads before capability discovery can perform I/O. */
5127
5323
  validateBeforeCreativeCapabilityProbe(taskType, params, options) {
5128
5324
  if (options.skipIdempotencyAutoInject || options.skipAccountValidation || options.skipRequestValidation) return;
@@ -12,7 +12,9 @@ export declare class ProductCache {
12
12
  private key;
13
13
  read<T>(scope: string, params: Record<string, unknown>, allowExpired?: boolean): TaskResult<T> | undefined;
14
14
  conditionalParams(scope: string, params: Record<string, unknown>): Record<string, unknown>;
15
- write<T>(scope: string, params: Record<string, unknown>, result: TaskResult<T>, conditionalSnapshot?: TaskResult<T>): TaskResult<T>;
15
+ write<T>(scope: string, params: Record<string, unknown>, result: TaskResult<T>, conditionalSnapshot?: TaskResult<T>,
16
+ /** @internal Refresh private discovery evidence before a confirmed snapshot is cloned into storage. */
17
+ onRevalidated?: (materialized: TaskResult<T>) => void): TaskResult<T>;
16
18
  clear(): void;
17
19
  }
18
20
  export declare function createProductCache(options?: ProductCacheOptions): ProductCache;
@@ -12,7 +12,9 @@ export declare class ProductCache {
12
12
  private key;
13
13
  read<T>(scope: string, params: Record<string, unknown>, allowExpired?: boolean): TaskResult<T> | undefined;
14
14
  conditionalParams(scope: string, params: Record<string, unknown>): Record<string, unknown>;
15
- write<T>(scope: string, params: Record<string, unknown>, result: TaskResult<T>, conditionalSnapshot?: TaskResult<T>): TaskResult<T>;
15
+ write<T>(scope: string, params: Record<string, unknown>, result: TaskResult<T>, conditionalSnapshot?: TaskResult<T>,
16
+ /** @internal Refresh private discovery evidence before a confirmed snapshot is cloned into storage. */
17
+ onRevalidated?: (materialized: TaskResult<T>) => void): TaskResult<T>;
16
18
  clear(): void;
17
19
  }
18
20
  export declare function createProductCache(options?: ProductCacheOptions): ProductCache;
@@ -86,7 +86,7 @@ class ProductCache {
86
86
  ...typeof data.pricing_version === "string" && { if_pricing_version: data.pricing_version }
87
87
  };
88
88
  }
89
- write(scope, params, result, conditionalSnapshot) {
89
+ write(scope, params, result, conditionalSnapshot, onRevalidated) {
90
90
  const key = this.key(scope, params);
91
91
  if (!key) return result;
92
92
  if (!result.success || result.status !== "completed" || !result.data || typeof result.data !== "object")
@@ -109,6 +109,7 @@ class ProductCache {
109
109
  return result;
110
110
  if (params.if_wholesale_feed_version !== void 0 || params.if_pricing_version !== void 0) return result;
111
111
  completed = { ...result, data: (0, import_legacy_metadata.structuredCloneWithLegacyCreativeMetadata)(oldData) };
112
+ onRevalidated?.(completed);
112
113
  } else if (!Array.isArray(data.products)) return result;
113
114
  if (!this.entries.has(key) && this.entries.size >= (this.options.maxEntries ?? 1e3))
114
115
  this.entries.delete(this.entries.keys().next().value);
@@ -62,7 +62,7 @@ class ProductCache {
62
62
  ...typeof data.pricing_version === "string" && { if_pricing_version: data.pricing_version }
63
63
  };
64
64
  }
65
- write(scope, params, result, conditionalSnapshot) {
65
+ write(scope, params, result, conditionalSnapshot, onRevalidated) {
66
66
  const key = this.key(scope, params);
67
67
  if (!key) return result;
68
68
  if (!result.success || result.status !== "completed" || !result.data || typeof result.data !== "object")
@@ -85,6 +85,7 @@ class ProductCache {
85
85
  return result;
86
86
  if (params.if_wholesale_feed_version !== void 0 || params.if_pricing_version !== void 0) return result;
87
87
  completed = { ...result, data: structuredCloneWithLegacyCreativeMetadata(oldData) };
88
+ onRevalidated?.(completed);
88
89
  } else if (!Array.isArray(data.products)) return result;
89
90
  if (!this.entries.has(key) && this.entries.size >= (this.options.maxEntries ?? 1e3))
90
91
  this.entries.delete(this.entries.keys().next().value);
@@ -144,9 +144,10 @@ export type SavePropertyRequest = Omit<RegistrySavePropertyRequest, 'authorized_
144
144
  /** Response from POST /api/properties/save (200) */
145
145
  export type SavePropertyResponse = operations['saveProperty']['responses']['200']['content']['application/json'];
146
146
  type RegistryResolveIdentifiersRequest = NonNullable<operations['resolveIdentifiers']['requestBody']>['content']['application/json'];
147
- /** Request body for POST /api/registry/resolve. `mode` defaults to `resolve` server-side. */
148
- export type ResolveIdentifiersRequest = Omit<RegistryResolveIdentifiersRequest, 'mode'> & {
147
+ /** Request body for POST /api/registry/resolve. Mode and sandbox use the registry's defaults when omitted. */
148
+ export type ResolveIdentifiersRequest = Omit<RegistryResolveIdentifiersRequest, 'mode' | 'sandbox'> & {
149
149
  mode?: RegistryResolveIdentifiersRequest['mode'];
150
+ sandbox?: RegistryResolveIdentifiersRequest['sandbox'];
150
151
  };
151
152
  /** Response from POST /api/registry/resolve (200) */
152
153
  export type ResolveIdentifiersResponse = operations['resolveIdentifiers']['responses']['200']['content']['application/json'];
@@ -144,9 +144,10 @@ export type SavePropertyRequest = Omit<RegistrySavePropertyRequest, 'authorized_
144
144
  /** Response from POST /api/properties/save (200) */
145
145
  export type SavePropertyResponse = operations['saveProperty']['responses']['200']['content']['application/json'];
146
146
  type RegistryResolveIdentifiersRequest = NonNullable<operations['resolveIdentifiers']['requestBody']>['content']['application/json'];
147
- /** Request body for POST /api/registry/resolve. `mode` defaults to `resolve` server-side. */
148
- export type ResolveIdentifiersRequest = Omit<RegistryResolveIdentifiersRequest, 'mode'> & {
147
+ /** Request body for POST /api/registry/resolve. Mode and sandbox use the registry's defaults when omitted. */
148
+ export type ResolveIdentifiersRequest = Omit<RegistryResolveIdentifiersRequest, 'mode' | 'sandbox'> & {
149
149
  mode?: RegistryResolveIdentifiersRequest['mode'];
150
+ sandbox?: RegistryResolveIdentifiersRequest['sandbox'];
150
151
  };
151
152
  /** Response from POST /api/registry/resolve (200) */
152
153
  export type ResolveIdentifiersResponse = operations['resolveIdentifiers']['responses']['200']['content']['application/json'];
@@ -283,7 +283,7 @@ export interface paths {
283
283
  put?: never;
284
284
  /**
285
285
  * Save property
286
- * @description Save or update a hosted property in the registry. Requires authentication. For existing properties, creates a revision-tracked edit. For new properties, creates the property directly. Cannot edit authoritative properties managed via adagents.json.
286
+ * @description Save or update a hosted property in the registry. Requires authentication. For bulk identifier contributions use resolveIdentifiers instead (up to 10,000 identifiers per request). This endpoint is limited to 60 requests/hour per user/IP. publisher_domain must be a lowercase hostname without a URL, port, whitespace, wildcard, or trailing dot. Reserved test domains are rejected unless sandbox is true. With sandbox: true the request is validated and a simulated result is returned without reading or writing a hosted property; the ID is ephemeral. For existing properties, creates a revision-tracked edit. For new properties, creates the property directly. Cannot edit authoritative properties managed via adagents.json.
287
287
  *
288
288
  * This is an identity-only write surface: the stored document always carries `authorized_agents: []`. Sales authorization lives solely in the publisher's own origin `adagents.json`; the community registry cannot mint or carry it. Any `authorized_agents` sent in the request body is ignored.
289
289
  */
@@ -4892,6 +4892,11 @@ export interface components {
4892
4892
  */
4893
4893
  OrganizationRevenueTier: "under_1m" | "1m_5m" | "5m_50m" | "50m_250m" | "250m_1b" | "1b_plus";
4894
4894
  ResolveResponse: {
4895
+ /**
4896
+ * @description Present for a non-persistent sandbox simulation. New property handles are ephemeral.
4897
+ * @enum {boolean}
4898
+ */
4899
+ sandbox?: true;
4895
4900
  resolved: components["schemas"]["ResolvedEntry"][];
4896
4901
  summary: {
4897
4902
  total: number;
@@ -4915,30 +4920,30 @@ export interface components {
4915
4920
  CatalogIdentifier: {
4916
4921
  /** @example domain */
4917
4922
  type: string;
4918
- /** @example nytimes.com */
4923
+ /** @example streamhaus.com */
4919
4924
  value: string;
4920
4925
  };
4926
+ /** @description Up to 10,000 identifiers plus provenance. resolve requires authentication and contributes unknown identifiers; lookup is read-only. Domain/subdomain values must follow core/identifier.json; production rejects reserved test domains. sandbox: true simulates contributions without writes. */
4921
4927
  ResolveRequest: {
4922
4928
  /** @description Identifiers to resolve (and, in resolve mode, contribute). Max 10,000 per call for all callers. */
4923
4929
  identifiers: components["schemas"]["CatalogIdentifier"][];
4924
4930
  provenance: components["schemas"]["FactProvenance"];
4925
4931
  /**
4926
- * @description `resolve` (default) contributes the identifiers, auto-creates missing catalog entries, logs demand activity, and returns rids — requires authentication. `lookup` is a pure read: no write, no activity log, no auth.
4932
+ * @description resolve (default) contributes identifiers, creates missing catalog entries, and logs activity; authentication is required. lookup is a public read with no writes or activity logging.
4927
4933
  * @default resolve
4928
4934
  * @enum {string}
4929
4935
  */
4930
4936
  mode: "resolve" | "lookup";
4937
+ /** @description Simulate contributions without storing properties, identifiers, facts, or activity. Reads existing catalog data; new handles are ephemeral. Reserved test domains are allowed only in sandbox mode. Defaults to false when omitted. */
4938
+ sandbox?: boolean;
4931
4939
  };
4932
4940
  FactProvenance: {
4933
4941
  /**
4934
- * @description How the caller knows these identifiers — the trust/audit envelope on the fact. Determines the confidence the catalog assigns. `crawl` is reserved for server-side pipelines; callers use the others.
4942
+ * @description How the caller knows these identifiers — the trust/audit envelope on the fact. Determines the confidence the catalog assigns. crawl is reserved for server-side pipelines; callers use the others.
4935
4943
  * @enum {string}
4936
4944
  */
4937
4945
  type: "agency_allowlist" | "publisher_declaration" | "impression_log" | "ssp_inventory" | "deal_history" | "crawl" | "data_partner" | "member_assertion";
4938
- /**
4939
- * @description Optional free-text annotation (campaign, dataset).
4940
- * @example unilever_q3
4941
- */
4946
+ /** @description Optional free-text annotation (campaign, dataset). */
4942
4947
  context?: string;
4943
4948
  };
4944
4949
  CatalogDisputeTriageResult: {
@@ -5731,6 +5736,11 @@ export interface operations {
5731
5736
  "application/json": {
5732
5737
  /** @example examplepub.com */
5733
5738
  publisher_domain: string;
5739
+ /**
5740
+ * @description Validate and simulate a save without persisting it. Allows reserved test hostnames.
5741
+ * @default false
5742
+ */
5743
+ sandbox?: boolean;
5734
5744
  /**
5735
5745
  * @description Ignored. Community-registry rows never assert sales authorization — the owner's origin adagents.json is the sole authorization source — so any value here is dropped and the stored document carries authorized_agents:[].
5736
5746
  * @example []
@@ -5770,6 +5780,8 @@ export interface operations {
5770
5780
  success: true;
5771
5781
  message: string;
5772
5782
  id: string;
5783
+ /** @enum {boolean} */
5784
+ sandbox?: true;
5773
5785
  revision_number?: number;
5774
5786
  };
5775
5787
  };
@@ -8475,7 +8487,7 @@ export interface operations {
8475
8487
  getAgentAuthorizations: {
8476
8488
  parameters: {
8477
8489
  query: {
8478
- /** @description Agent URL to look up. Canonicalized server-side (lowercased, trailing slashes trimmed). */
8490
+ /** @description Agent URL to look up. Canonicalized server-side per AdCP URL canonicalization: the scheme and host fold, the path does not. */
8479
8491
  agent_url: string;
8480
8492
  /** @description `effective` (default) applies override layer; `raw` reads base table. */
8481
8493
  include?: "raw" | "effective";
@@ -283,7 +283,7 @@ export interface paths {
283
283
  put?: never;
284
284
  /**
285
285
  * Save property
286
- * @description Save or update a hosted property in the registry. Requires authentication. For existing properties, creates a revision-tracked edit. For new properties, creates the property directly. Cannot edit authoritative properties managed via adagents.json.
286
+ * @description Save or update a hosted property in the registry. Requires authentication. For bulk identifier contributions use resolveIdentifiers instead (up to 10,000 identifiers per request). This endpoint is limited to 60 requests/hour per user/IP. publisher_domain must be a lowercase hostname without a URL, port, whitespace, wildcard, or trailing dot. Reserved test domains are rejected unless sandbox is true. With sandbox: true the request is validated and a simulated result is returned without reading or writing a hosted property; the ID is ephemeral. For existing properties, creates a revision-tracked edit. For new properties, creates the property directly. Cannot edit authoritative properties managed via adagents.json.
287
287
  *
288
288
  * This is an identity-only write surface: the stored document always carries `authorized_agents: []`. Sales authorization lives solely in the publisher's own origin `adagents.json`; the community registry cannot mint or carry it. Any `authorized_agents` sent in the request body is ignored.
289
289
  */
@@ -4892,6 +4892,11 @@ export interface components {
4892
4892
  */
4893
4893
  OrganizationRevenueTier: "under_1m" | "1m_5m" | "5m_50m" | "50m_250m" | "250m_1b" | "1b_plus";
4894
4894
  ResolveResponse: {
4895
+ /**
4896
+ * @description Present for a non-persistent sandbox simulation. New property handles are ephemeral.
4897
+ * @enum {boolean}
4898
+ */
4899
+ sandbox?: true;
4895
4900
  resolved: components["schemas"]["ResolvedEntry"][];
4896
4901
  summary: {
4897
4902
  total: number;
@@ -4915,30 +4920,30 @@ export interface components {
4915
4920
  CatalogIdentifier: {
4916
4921
  /** @example domain */
4917
4922
  type: string;
4918
- /** @example nytimes.com */
4923
+ /** @example streamhaus.com */
4919
4924
  value: string;
4920
4925
  };
4926
+ /** @description Up to 10,000 identifiers plus provenance. resolve requires authentication and contributes unknown identifiers; lookup is read-only. Domain/subdomain values must follow core/identifier.json; production rejects reserved test domains. sandbox: true simulates contributions without writes. */
4921
4927
  ResolveRequest: {
4922
4928
  /** @description Identifiers to resolve (and, in resolve mode, contribute). Max 10,000 per call for all callers. */
4923
4929
  identifiers: components["schemas"]["CatalogIdentifier"][];
4924
4930
  provenance: components["schemas"]["FactProvenance"];
4925
4931
  /**
4926
- * @description `resolve` (default) contributes the identifiers, auto-creates missing catalog entries, logs demand activity, and returns rids — requires authentication. `lookup` is a pure read: no write, no activity log, no auth.
4932
+ * @description resolve (default) contributes identifiers, creates missing catalog entries, and logs activity; authentication is required. lookup is a public read with no writes or activity logging.
4927
4933
  * @default resolve
4928
4934
  * @enum {string}
4929
4935
  */
4930
4936
  mode: "resolve" | "lookup";
4937
+ /** @description Simulate contributions without storing properties, identifiers, facts, or activity. Reads existing catalog data; new handles are ephemeral. Reserved test domains are allowed only in sandbox mode. Defaults to false when omitted. */
4938
+ sandbox?: boolean;
4931
4939
  };
4932
4940
  FactProvenance: {
4933
4941
  /**
4934
- * @description How the caller knows these identifiers — the trust/audit envelope on the fact. Determines the confidence the catalog assigns. `crawl` is reserved for server-side pipelines; callers use the others.
4942
+ * @description How the caller knows these identifiers — the trust/audit envelope on the fact. Determines the confidence the catalog assigns. crawl is reserved for server-side pipelines; callers use the others.
4935
4943
  * @enum {string}
4936
4944
  */
4937
4945
  type: "agency_allowlist" | "publisher_declaration" | "impression_log" | "ssp_inventory" | "deal_history" | "crawl" | "data_partner" | "member_assertion";
4938
- /**
4939
- * @description Optional free-text annotation (campaign, dataset).
4940
- * @example unilever_q3
4941
- */
4946
+ /** @description Optional free-text annotation (campaign, dataset). */
4942
4947
  context?: string;
4943
4948
  };
4944
4949
  CatalogDisputeTriageResult: {
@@ -5731,6 +5736,11 @@ export interface operations {
5731
5736
  "application/json": {
5732
5737
  /** @example examplepub.com */
5733
5738
  publisher_domain: string;
5739
+ /**
5740
+ * @description Validate and simulate a save without persisting it. Allows reserved test hostnames.
5741
+ * @default false
5742
+ */
5743
+ sandbox?: boolean;
5734
5744
  /**
5735
5745
  * @description Ignored. Community-registry rows never assert sales authorization — the owner's origin adagents.json is the sole authorization source — so any value here is dropped and the stored document carries authorized_agents:[].
5736
5746
  * @example []
@@ -5770,6 +5780,8 @@ export interface operations {
5770
5780
  success: true;
5771
5781
  message: string;
5772
5782
  id: string;
5783
+ /** @enum {boolean} */
5784
+ sandbox?: true;
5773
5785
  revision_number?: number;
5774
5786
  };
5775
5787
  };
@@ -8475,7 +8487,7 @@ export interface operations {
8475
8487
  getAgentAuthorizations: {
8476
8488
  parameters: {
8477
8489
  query: {
8478
- /** @description Agent URL to look up. Canonicalized server-side (lowercased, trailing slashes trimmed). */
8490
+ /** @description Agent URL to look up. Canonicalized server-side per AdCP URL canonicalization: the scheme and host fold, the path does not. */
8479
8491
  agent_url: string;
8480
8492
  /** @description `effective` (default) applies override layer; `raw` reads base table. */
8481
8493
  include?: "raw" | "effective";
@@ -4,5 +4,5 @@
4
4
  "source_sha": "4e553ad955f83b49c7d221ab5c3ff78237ad02e3",
5
5
  "source_tarball_sha256": "580656d6466ef9f0d1119985e6726c2efea718dc671e2ad30957fcb2fd54af0f",
6
6
  "upstream_adcp_version": "2.5.3",
7
- "synced_at": "2026-10-17T14:53:03.023Z"
7
+ "synced_at": "2026-10-19T00:55:30.097Z"
8
8
  }