@adcp/sdk 6.10.0 → 6.12.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 (116) hide show
  1. package/bin/adcp-config.js +7 -1
  2. package/bin/adcp.js +191 -4
  3. package/dist/lib/adapters/index.d.ts +0 -2
  4. package/dist/lib/adapters/index.d.ts.map +1 -1
  5. package/dist/lib/adapters/index.js +1 -8
  6. package/dist/lib/adapters/index.js.map +1 -1
  7. package/dist/lib/index.d.ts +1 -1
  8. package/dist/lib/index.d.ts.map +1 -1
  9. package/dist/lib/index.js +2 -7
  10. package/dist/lib/index.js.map +1 -1
  11. package/dist/lib/mock-server/index.d.ts +2 -0
  12. package/dist/lib/mock-server/index.d.ts.map +1 -1
  13. package/dist/lib/mock-server/index.js +17 -0
  14. package/dist/lib/mock-server/index.js.map +1 -1
  15. package/dist/lib/mock-server/sales-guaranteed/recipe.d.ts +155 -0
  16. package/dist/lib/mock-server/sales-guaranteed/recipe.d.ts.map +1 -0
  17. package/dist/lib/mock-server/sales-guaranteed/recipe.js +107 -0
  18. package/dist/lib/mock-server/sales-guaranteed/recipe.js.map +1 -0
  19. package/dist/lib/mock-server/sales-guaranteed/server.d.ts.map +1 -1
  20. package/dist/lib/mock-server/sales-guaranteed/server.js +212 -0
  21. package/dist/lib/mock-server/sales-guaranteed/server.js.map +1 -1
  22. package/dist/lib/mock-server/sales-non-guaranteed/recipe.d.ts +123 -0
  23. package/dist/lib/mock-server/sales-non-guaranteed/recipe.d.ts.map +1 -0
  24. package/dist/lib/mock-server/sales-non-guaranteed/recipe.js +81 -0
  25. package/dist/lib/mock-server/sales-non-guaranteed/recipe.js.map +1 -0
  26. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  27. package/dist/lib/server/ctx-metadata/index.d.ts +1 -1
  28. package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -1
  29. package/dist/lib/server/ctx-metadata/index.js +3 -1
  30. package/dist/lib/server/ctx-metadata/index.js.map +1 -1
  31. package/dist/lib/server/ctx-metadata/wire-shape.d.ts +21 -0
  32. package/dist/lib/server/ctx-metadata/wire-shape.d.ts.map +1 -1
  33. package/dist/lib/server/ctx-metadata/wire-shape.js +111 -0
  34. package/dist/lib/server/ctx-metadata/wire-shape.js.map +1 -1
  35. package/dist/lib/server/decisioning/async-outcome.d.ts +17 -0
  36. package/dist/lib/server/decisioning/async-outcome.d.ts.map +1 -1
  37. package/dist/lib/server/decisioning/async-outcome.js +23 -18
  38. package/dist/lib/server/decisioning/async-outcome.js.map +1 -1
  39. package/dist/lib/server/decisioning/context.d.ts +27 -2
  40. package/dist/lib/server/decisioning/context.d.ts.map +1 -1
  41. package/dist/lib/server/decisioning/index.d.ts +4 -0
  42. package/dist/lib/server/decisioning/index.d.ts.map +1 -1
  43. package/dist/lib/server/decisioning/index.js +16 -1
  44. package/dist/lib/server/decisioning/index.js.map +1 -1
  45. package/dist/lib/server/decisioning/platform.d.ts +17 -0
  46. package/dist/lib/server/decisioning/platform.d.ts.map +1 -1
  47. package/dist/lib/server/decisioning/platform.js.map +1 -1
  48. package/dist/lib/server/decisioning/proposal/dispatch.d.ts +203 -0
  49. package/dist/lib/server/decisioning/proposal/dispatch.d.ts.map +1 -0
  50. package/dist/lib/server/decisioning/proposal/dispatch.js +395 -0
  51. package/dist/lib/server/decisioning/proposal/dispatch.js.map +1 -0
  52. package/dist/lib/server/decisioning/proposal/index.d.ts +21 -0
  53. package/dist/lib/server/decisioning/proposal/index.d.ts.map +1 -0
  54. package/dist/lib/server/decisioning/proposal/index.js +37 -0
  55. package/dist/lib/server/decisioning/proposal/index.js.map +1 -0
  56. package/dist/lib/server/decisioning/proposal/lifecycle.d.ts +195 -0
  57. package/dist/lib/server/decisioning/proposal/lifecycle.d.ts.map +1 -0
  58. package/dist/lib/server/decisioning/proposal/lifecycle.js +366 -0
  59. package/dist/lib/server/decisioning/proposal/lifecycle.js.map +1 -0
  60. package/dist/lib/server/decisioning/proposal/mock-manager.d.ts +93 -0
  61. package/dist/lib/server/decisioning/proposal/mock-manager.d.ts.map +1 -0
  62. package/dist/lib/server/decisioning/proposal/mock-manager.js +109 -0
  63. package/dist/lib/server/decisioning/proposal/mock-manager.js.map +1 -0
  64. package/dist/lib/server/decisioning/proposal/store.d.ts +279 -0
  65. package/dist/lib/server/decisioning/proposal/store.d.ts.map +1 -0
  66. package/dist/lib/server/decisioning/proposal/store.js +291 -0
  67. package/dist/lib/server/decisioning/proposal/store.js.map +1 -0
  68. package/dist/lib/server/decisioning/proposal/types.d.ts +394 -0
  69. package/dist/lib/server/decisioning/proposal/types.d.ts.map +1 -0
  70. package/dist/lib/server/decisioning/proposal/types.js +58 -0
  71. package/dist/lib/server/decisioning/proposal/types.js.map +1 -0
  72. package/dist/lib/server/decisioning/runtime/from-platform.d.ts +25 -0
  73. package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
  74. package/dist/lib/server/decisioning/runtime/from-platform.js +204 -19
  75. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  76. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -1
  77. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +5 -2
  78. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -1
  79. package/dist/lib/server/decisioning/runtime/task-registry.d.ts +5 -0
  80. package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -1
  81. package/dist/lib/server/decisioning/runtime/task-registry.js +4 -1
  82. package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -1
  83. package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -1
  84. package/dist/lib/server/decisioning/runtime/to-context.js +10 -2
  85. package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -1
  86. package/dist/lib/server/index.d.ts +1 -1
  87. package/dist/lib/server/index.d.ts.map +1 -1
  88. package/dist/lib/server/index.js +3 -1
  89. package/dist/lib/server/index.js.map +1 -1
  90. package/dist/lib/server/test-controller.d.ts +2 -0
  91. package/dist/lib/server/test-controller.d.ts.map +1 -1
  92. package/dist/lib/server/test-controller.js +6 -11
  93. package/dist/lib/server/test-controller.js.map +1 -1
  94. package/dist/lib/testing/client.d.ts.map +1 -1
  95. package/dist/lib/testing/client.js +7 -1
  96. package/dist/lib/testing/client.js.map +1 -1
  97. package/dist/lib/testing/comply-controller.d.ts +2 -0
  98. package/dist/lib/testing/comply-controller.d.ts.map +1 -1
  99. package/dist/lib/testing/comply-controller.js.map +1 -1
  100. package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
  101. package/dist/lib/testing/storyboard/task-map.js +1 -0
  102. package/dist/lib/testing/storyboard/task-map.js.map +1 -1
  103. package/dist/lib/testing/storyboard/test-kit.d.ts.map +1 -1
  104. package/dist/lib/testing/storyboard/test-kit.js +4 -0
  105. package/dist/lib/testing/storyboard/test-kit.js.map +1 -1
  106. package/dist/lib/testing/types.d.ts +10 -0
  107. package/dist/lib/testing/types.d.ts.map +1 -1
  108. package/dist/lib/version.d.ts +3 -3
  109. package/dist/lib/version.js +3 -3
  110. package/examples/hello_seller_adapter_guaranteed.ts +29 -2
  111. package/examples/hello_seller_adapter_proposal_mode.ts +575 -0
  112. package/package.json +1 -1
  113. package/dist/lib/adapters/proposal-manager.d.ts +0 -142
  114. package/dist/lib/adapters/proposal-manager.d.ts.map +0 -1
  115. package/dist/lib/adapters/proposal-manager.js +0 -184
  116. package/dist/lib/adapters/proposal-manager.js.map +0 -1
@@ -58,6 +58,7 @@ const node_crypto_1 = require("node:crypto");
58
58
  const create_adcp_server_1 = require("../../create-adcp-server");
59
59
  const account_1 = require("../account");
60
60
  const async_outcome_1 = require("../async-outcome");
61
+ const proposal_1 = require("../proposal");
61
62
  const errors_1 = require("../../errors");
62
63
  const validate_platform_1 = require("./validate-platform");
63
64
  const validate_specialisms_1 = require("../validate-specialisms");
@@ -617,7 +618,7 @@ function createAdcpServerFromPlatform(platform, opts) {
617
618
  mediaBuy: mergeHandlers(opts.mediaBuy, buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observability, fwLogger, {
618
619
  allowPrivateWebhookUrls: opts.allowPrivateWebhookUrls === true,
619
620
  autoEmitCompletionWebhooks: opts.autoEmitCompletionWebhooks !== false,
620
- }, ctxFor, effectiveCtxMetadata, opts.mediaBuyStore), 'mediaBuy', mergeOpts),
621
+ }, ctxFor, effectiveCtxMetadata, opts.mediaBuyStore, opts.proposalStore), 'mediaBuy', mergeOpts),
621
622
  creative: mergeHandlers(opts.creative, buildCreativeHandlers(platform, taskRegistry, taskWebhookEmit, observability, fwLogger, {
622
623
  allowPrivateWebhookUrls: opts.allowPrivateWebhookUrls === true,
623
624
  autoEmitCompletionWebhooks: opts.autoEmitCompletionWebhooks !== false,
@@ -1367,8 +1368,18 @@ async function projectSync(fn, mapResult, refresh) {
1367
1368
  // Mutates `wire` in place — every handler builds a fresh response per
1368
1369
  // call so mutation is safe. Runs BEFORE the framework wraps in envelope
1369
1370
  // / caches in idempotency, so cached replays stay clean too.
1371
+ //
1372
+ // implementation_config strip: same pattern. Recipes ride on
1373
+ // Product.implementation_config server-side (typed contract between
1374
+ // ProposalManager and DecisioningPlatform via ctx.recipes) but are
1375
+ // opaque-to-buyer. The wire schema is `additionalProperties: true`
1376
+ // so the field is technically legal — but recipes carry upstream
1377
+ // identifiers (network codes, line-item template ids, ad-unit ids,
1378
+ // GAM line-item priority) that leak topology to buyers. Strip
1379
+ // before the response leaves the dispatcher.
1370
1380
  if (wire != null && typeof wire === 'object') {
1371
1381
  (0, ctx_metadata_1.stripCtxMetadata)(wire);
1382
+ (0, ctx_metadata_1.stripImplementationConfig)(wire);
1372
1383
  }
1373
1384
  return wire;
1374
1385
  }
@@ -1415,18 +1426,19 @@ async function projectSync(fn, mapResult, refresh) {
1415
1426
  */
1416
1427
  async function routeIfHandoff(taskRegistry, opts, result, project) {
1417
1428
  if ((0, async_outcome_2.isTaskHandoff)(result)) {
1418
- const taskFn = (0, async_outcome_2._extractTaskFn)(result);
1419
- if (!taskFn) {
1429
+ const entry = (0, async_outcome_2._extractHandoffEntry)(result);
1430
+ if (!entry) {
1420
1431
  // Forgery — adopter constructed something with the brand symbol
1421
1432
  // but didn't go through ctx.handoffToTask. Treat as a sync
1422
1433
  // success arm with an empty body (caller-supplied projection
1423
1434
  // shapes the result; this branch is defensive).
1424
1435
  return await project(result);
1425
1436
  }
1437
+ const { fn: taskFn, options } = entry;
1426
1438
  return dispatchHitl(taskRegistry, opts, async (taskId) => {
1427
1439
  const inner = await taskFn((0, to_context_1.buildHandoffContext)(taskRegistry, taskId));
1428
1440
  return await project(inner);
1429
- });
1441
+ }, options?.task_id);
1430
1442
  }
1431
1443
  // Catch the most common LLM-scaffolded mistake: hand-rolling a
1432
1444
  // `{status: 'submitted', task_id: '...'}` envelope instead of returning
@@ -1524,12 +1536,13 @@ async function emitSyncCompletionWebhook(opts, result) {
1524
1536
  }, 'onWebhookEmit', opts.logger);
1525
1537
  }
1526
1538
  }
1527
- async function dispatchHitl(taskRegistry, opts, taskFn) {
1539
+ async function dispatchHitl(taskRegistry, opts, taskFn, overrideTaskId) {
1528
1540
  const createStart = Date.now();
1529
1541
  const { taskId } = await taskRegistry.create({
1530
1542
  tool: opts.tool,
1531
1543
  accountId: opts.accountId,
1532
1544
  hasWebhook: opts.pushNotificationUrl !== undefined,
1545
+ ...(overrideTaskId !== undefined && { overrideTaskId }),
1533
1546
  });
1534
1547
  safeFire(opts.observability?.onTaskCreate, {
1535
1548
  tool: opts.tool,
@@ -1570,7 +1583,18 @@ async function dispatchHitl(taskRegistry, opts, taskFn) {
1570
1583
  taskFnError = err;
1571
1584
  }
1572
1585
  if (taskFnError === undefined) {
1573
- // Success path
1586
+ // Success path. Strip wire-only-server fields BEFORE the registry
1587
+ // write so every downstream consumer (tasks/get polling at
1588
+ // line 1826, emitTaskWebhook below at line 2493, idempotency
1589
+ // replays of the Submitted envelope) inherits a clean payload.
1590
+ // Mirrors the same chokepoint pattern projectSync uses for the
1591
+ // sync arm. Without this strip, ctx_metadata + implementation_config
1592
+ // ride to the buyer via the HITL completion path even though the
1593
+ // sync path is clean — surfaced by security review on PR #1562.
1594
+ if (result != null && typeof result === 'object') {
1595
+ (0, ctx_metadata_1.stripCtxMetadata)(result);
1596
+ (0, ctx_metadata_1.stripImplementationConfig)(result);
1597
+ }
1574
1598
  try {
1575
1599
  await taskRegistry.complete(taskId, result);
1576
1600
  }
@@ -2391,9 +2415,11 @@ function validatePushNotificationToken(token) {
2391
2415
  }
2392
2416
  return { ok: true };
2393
2417
  }
2394
- function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observability, logger, pushOpts, ctxFor, ctxMetadataStore, mediaBuyStore) {
2418
+ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observability, logger, pushOpts, ctxFor, ctxMetadataStore, mediaBuyStore, proposalStore) {
2395
2419
  const sales = platform.sales;
2396
- if (!sales)
2420
+ const proposalManager = platform.proposalManager;
2421
+ // Without sales AND without a proposal manager, there's nothing to dispatch.
2422
+ if (!sales && !proposalManager)
2397
2423
  return undefined;
2398
2424
  // Core lifecycle methods are optional on the SalesPlatform interface
2399
2425
  // (#1341) — the per-specialism mapping in `RequiredPlatformsFor<S>`
@@ -2405,21 +2431,97 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2405
2431
  // (`opts.mediaBuy.X`) can supply it OR the framework returns
2406
2432
  // `METHOD_NOT_FOUND` from `tools/list` for the unsupported tool.
2407
2433
  return {
2408
- ...(sales.getProducts && {
2434
+ ...((sales?.getProducts || proposalManager) && {
2409
2435
  getProducts: async (params, ctx) => {
2410
2436
  const reqCtx = ctxFor(ctx);
2437
+ // v1.5 seam: intercept refine[i].action='finalize' before
2438
+ // dispatching to the manager / sales. When the framework
2439
+ // commits the proposal inline, project the response directly.
2440
+ if (proposalManager && proposalStore) {
2441
+ const intercept = await (0, proposal_1.maybeInterceptFinalize)({
2442
+ request: params,
2443
+ manager: proposalManager,
2444
+ store: proposalStore,
2445
+ ctx: reqCtx,
2446
+ });
2447
+ if (intercept.kind === 'intercepted') {
2448
+ // Unified sync-or-HITL dispatch. routeIfHandoff runs
2449
+ // `intercept.project` inline for sync `FinalizeProposalSuccess`
2450
+ // returns and inside the background task for HITL handoffs.
2451
+ // Same machinery createMediaBuy / syncCreatives use — finalize
2452
+ // inherits the framework's task-lifecycle guarantees.
2453
+ //
2454
+ // **Known gap (HITL only)**: if the buyer calls tasks/cancel
2455
+ // while the adopter's handoff fn is still mid-run, the
2456
+ // framework marks the task cancelled but `intercept.project`
2457
+ // (which fires when the handoff fn resolves) still runs
2458
+ // store.commit. The committed proposal then sits in the
2459
+ // store with no buyer-facing task to retrieve it. Same gap
2460
+ // exists for createMediaBuy HITL today (see comment in
2461
+ // `sales.createMediaBuy` body re: CONSUMING reservation
2462
+ // cleanup). Mitigations: the proposal expires via the
2463
+ // 7-day eviction window in `InMemoryProposalStore`, and
2464
+ // production durable stores can implement a sweep against
2465
+ // the task registry to release stale committed-but-
2466
+ // unretrievable proposals. Closing the gap end-to-end
2467
+ // requires propagating an AbortSignal into the projection
2468
+ // — framework-level work tracked separately, not finalize-
2469
+ // specific.
2470
+ const push = extractPushConfig(params, logger, {
2471
+ allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls,
2472
+ });
2473
+ const out = await routeIfHandoff(taskRegistry, {
2474
+ tool: 'get_products',
2475
+ accountId: reqCtx.account.id,
2476
+ pushNotificationUrl: push.url,
2477
+ pushNotificationToken: push.token,
2478
+ emitWebhook: taskWebhookEmit ?? ctx.emitWebhook,
2479
+ autoEmitCompletion: pushOpts.autoEmitCompletionWebhooks,
2480
+ observability,
2481
+ logger,
2482
+ }, intercept.result, intercept.project);
2483
+ return out;
2484
+ }
2485
+ }
2411
2486
  return projectSync(async () => {
2412
- const result = await sales.getProducts(params, reqCtx);
2487
+ // Pick dispatch target: ProposalManager (when wired) takes
2488
+ // ownership of get_products; sales is the v1 fallback.
2489
+ // Refine routing per Python's _select_proposal_method:
2490
+ // refine_products iff buying_mode='refine' AND
2491
+ // capabilities.refine AND the manager implements it.
2492
+ let result;
2493
+ if (proposalManager) {
2494
+ const buyingMode = params.buying_mode;
2495
+ const useRefine = buyingMode === 'refine' &&
2496
+ proposalManager.capabilities.refine === true &&
2497
+ typeof proposalManager.refineProducts === 'function';
2498
+ result = useRefine
2499
+ ? await proposalManager.refineProducts(params, reqCtx)
2500
+ : await proposalManager.getProducts(params, reqCtx);
2501
+ }
2502
+ else {
2503
+ result = await sales.getProducts(params, reqCtx);
2504
+ }
2413
2505
  // Auto-store products: persist each Product's wire shape +
2414
2506
  // ctx_metadata so subsequent createMediaBuy / updateMediaBuy
2415
2507
  // calls referencing product_id can hydrate the full Product
2416
2508
  // automatically (publisher sees `req.packages[i].product`).
2417
2509
  await autoStoreResources(ctxMetadataStore, reqCtx.account?.id, 'product', result?.products, 'product_id', logger);
2510
+ // v1.5 seam: persist proposals[] as DRAFT records (with
2511
+ // typed recipes pulled from Product.implementation_config)
2512
+ // so subsequent finalize / create_media_buy can hydrate.
2513
+ if (proposalStore) {
2514
+ await (0, proposal_1.maybePersistDraftAfterGetProducts)({
2515
+ response: result,
2516
+ store: proposalStore,
2517
+ ctx: reqCtx,
2518
+ });
2519
+ }
2418
2520
  return result;
2419
2521
  }, r => r);
2420
2522
  },
2421
2523
  }),
2422
- ...(sales.createMediaBuy && {
2524
+ ...(sales?.createMediaBuy && {
2423
2525
  createMediaBuy: async (params, ctx) => {
2424
2526
  const reqCtx = ctxFor(ctx);
2425
2527
  // Auto-hydrate: walk `params.packages`, attach the full Product object
@@ -2427,11 +2529,59 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2427
2529
  // `pkg.product.format_ids`, `pkg.product.ctx_metadata?.gam?.ad_unit_ids`
2428
2530
  // directly — no separate lookup, no boilerplate.
2429
2531
  await hydratePackagesWithProducts(ctxMetadataStore, reqCtx.account?.id, params?.packages, logger);
2532
+ // v1.5 seam: when the request carries a proposal_id, reserve
2533
+ // the proposal (atomic CAS COMMITTED → CONSUMING), validate
2534
+ // expiry + capability overlap, hydrate ctx.recipes. The
2535
+ // adapter runs against the reservation; finalize_consumption
2536
+ // (success) or release_consumption (failure) closes it out.
2537
+ let reservation = null;
2538
+ if (proposalStore) {
2539
+ reservation = await (0, proposal_1.maybeReserveProposalForCreateMediaBuy)({
2540
+ request: params,
2541
+ manager: proposalManager,
2542
+ store: proposalStore,
2543
+ ctx: reqCtx,
2544
+ });
2545
+ if (reservation) {
2546
+ reqCtx.recipes = reservation.recipes;
2547
+ }
2548
+ }
2430
2549
  return projectSync(async () => {
2431
2550
  const push = extractPushConfig(params, logger, {
2432
2551
  allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls,
2433
2552
  });
2434
- const result = await sales.createMediaBuy(params, reqCtx);
2553
+ let result;
2554
+ try {
2555
+ result = await sales.createMediaBuy(params, reqCtx);
2556
+ }
2557
+ catch (err) {
2558
+ // Adapter rejected — roll back the reservation so the buyer
2559
+ // can retry without PROPOSAL_NOT_COMMITTED blocking them.
2560
+ if (reservation && proposalStore) {
2561
+ await (0, proposal_1.releaseProposalReservation)({
2562
+ store: proposalStore,
2563
+ record: reservation,
2564
+ logger,
2565
+ });
2566
+ }
2567
+ throw err;
2568
+ }
2569
+ // Inline-success path: promote CONSUMING → CONSUMED with the
2570
+ // adapter's media_buy_id. HITL handoff: the proposal stays
2571
+ // CONSUMING until the handoff completes — wiring the
2572
+ // post-completion commit hook is a v1.6 follow-up; for now
2573
+ // adopters using HITL accept the reservation lingers until
2574
+ // eviction. Most adopters use inline create_media_buy.
2575
+ if (reservation && proposalStore && !(0, async_outcome_2.isTaskHandoff)(result)) {
2576
+ const mediaBuyId = result.media_buy_id;
2577
+ if (mediaBuyId) {
2578
+ await (0, proposal_1.finalizeProposalConsumption)({
2579
+ store: proposalStore,
2580
+ record: reservation,
2581
+ mediaBuyId,
2582
+ });
2583
+ }
2584
+ }
2435
2585
  return routeIfHandoff(taskRegistry, {
2436
2586
  tool: 'create_media_buy',
2437
2587
  accountId: reqCtx.account.id,
@@ -2448,7 +2598,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2448
2598
  }, r => r);
2449
2599
  },
2450
2600
  }),
2451
- ...(sales.updateMediaBuy && {
2601
+ ...(sales?.updateMediaBuy && {
2452
2602
  updateMediaBuy: async (params, ctx) => {
2453
2603
  const reqCtx = ctxFor(ctx);
2454
2604
  // `media_buy_id` is required on the wire schema, but `validation: 'off'`
@@ -2467,6 +2617,21 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2467
2617
  // directly — no separate lookup. Misses are silent; publisher falls
2468
2618
  // back to its own DB. Schema-driven via `x-entity` (#1109).
2469
2619
  await hydrateForTool(ctxMetadataStore, reqCtx.account?.id, 'update_media_buy', params, logger);
2620
+ // v1.5 seam: hydrate ctx.recipes via reverse-index so the adapter's
2621
+ // updateMediaBuy can apply the same recipe that drove createMediaBuy.
2622
+ // Re-validates capability overlap on any packages-shaped patch
2623
+ // (Resolutions §5).
2624
+ if (proposalStore) {
2625
+ const record = await (0, proposal_1.maybeHydrateRecipesForMediaBuyId)({
2626
+ mediaBuyId: media_buy_id,
2627
+ store: proposalStore,
2628
+ ctx: reqCtx,
2629
+ packages: params.packages,
2630
+ });
2631
+ if (record) {
2632
+ reqCtx.recipes = record.recipes;
2633
+ }
2634
+ }
2470
2635
  return projectSync(async () => {
2471
2636
  const push = extractPushConfig(params, logger, {
2472
2637
  allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls,
@@ -2509,7 +2674,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2509
2674
  syncCreatives: async (params, ctx) => {
2510
2675
  const reqCtx = ctxFor(ctx);
2511
2676
  const creatives = params.creatives ?? [];
2512
- if (!sales.syncCreatives) {
2677
+ if (!sales?.syncCreatives) {
2513
2678
  return (0, errors_1.adcpError)('UNSUPPORTED_FEATURE', {
2514
2679
  message: 'sync_creatives not supported by this sales platform',
2515
2680
  });
@@ -2529,9 +2694,29 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2529
2694
  }, result, rows => ({ creatives: rows.map(normalizeRowErrors) }));
2530
2695
  }, r => r);
2531
2696
  },
2532
- ...(sales.getMediaBuyDelivery && {
2697
+ ...(sales?.getMediaBuyDelivery && {
2533
2698
  getMediaBuyDelivery: async (params, ctx) => {
2534
2699
  const reqCtx = ctxFor(ctx);
2700
+ // v1.5 seam: hydrate ctx.recipes for delivery reads. Per
2701
+ // Resolutions §5, recipe-driven delivery aggregation needs the
2702
+ // same recipe view the originating createMediaBuy used.
2703
+ if (proposalStore) {
2704
+ const ids = params.media_buy_ids;
2705
+ // The wire shape uses `media_buy_ids[]`; hydrate from the first id
2706
+ // when present (per-buy recipe lookup; if buyer queries multiple
2707
+ // ids, the adapter is responsible for per-id recipe routing).
2708
+ const firstId = Array.isArray(ids) && ids.length > 0 ? ids[0] : undefined;
2709
+ if (firstId) {
2710
+ const record = await (0, proposal_1.maybeHydrateRecipesForMediaBuyId)({
2711
+ mediaBuyId: firstId,
2712
+ store: proposalStore,
2713
+ ctx: reqCtx,
2714
+ });
2715
+ if (record) {
2716
+ reqCtx.recipes = record.recipes;
2717
+ }
2718
+ }
2719
+ }
2535
2720
  return projectSync(async () => {
2536
2721
  const result = await sales.getMediaBuyDelivery(params, reqCtx);
2537
2722
  warnIfTruncatedMultiIdResponse('getMediaBuyDelivery', 'media_buy_ids', params.media_buy_ids, result?.media_buy_deliveries, logger);
@@ -2552,7 +2737,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2552
2737
  // platform-derived handler when absent lets `mergeHandlers` pick up the
2553
2738
  // adopter's custom handler from `opts.mediaBuy` instead of throwing
2554
2739
  // `sales.getMediaBuys is not a function`.
2555
- ...(sales.getMediaBuys && {
2740
+ ...(sales?.getMediaBuys && {
2556
2741
  getMediaBuys: async (params, ctx) => {
2557
2742
  const reqCtx = ctxFor(ctx);
2558
2743
  return projectSync(async () => {
@@ -2564,7 +2749,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2564
2749
  }, r => r);
2565
2750
  },
2566
2751
  }),
2567
- ...(sales.providePerformanceFeedback && {
2752
+ ...(sales?.providePerformanceFeedback && {
2568
2753
  providePerformanceFeedback: async (params, ctx) => {
2569
2754
  const reqCtx = ctxFor(ctx);
2570
2755
  // Auto-hydrate `req.media_buy` from the prior createMediaBuy /
@@ -2579,13 +2764,13 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2579
2764
  return projectSync(() => sales.providePerformanceFeedback(params, reqCtx), r => r);
2580
2765
  },
2581
2766
  }),
2582
- ...(sales.listCreativeFormats && {
2767
+ ...(sales?.listCreativeFormats && {
2583
2768
  listCreativeFormats: async (params, ctx) => {
2584
2769
  const reqCtx = ctxFor(ctx);
2585
2770
  return projectSync(() => sales.listCreativeFormats(params, reqCtx), r => r);
2586
2771
  },
2587
2772
  }),
2588
- ...(sales.listCreatives && {
2773
+ ...(sales?.listCreatives && {
2589
2774
  listCreatives: async (params, ctx) => {
2590
2775
  const reqCtx = ctxFor(ctx);
2591
2776
  return projectSync(async () => {