@adcp/sdk 6.11.0 → 6.13.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 (103) 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/auth/oauth/index.d.ts +1 -0
  8. package/dist/lib/auth/oauth/index.d.ts.map +1 -1
  9. package/dist/lib/auth/oauth/index.js +17 -1
  10. package/dist/lib/auth/oauth/index.js.map +1 -1
  11. package/dist/lib/auth/oauth/web-flow.d.ts +270 -0
  12. package/dist/lib/auth/oauth/web-flow.d.ts.map +1 -0
  13. package/dist/lib/auth/oauth/web-flow.js +413 -0
  14. package/dist/lib/auth/oauth/web-flow.js.map +1 -0
  15. package/dist/lib/index.d.ts +1 -1
  16. package/dist/lib/index.d.ts.map +1 -1
  17. package/dist/lib/index.js +2 -7
  18. package/dist/lib/index.js.map +1 -1
  19. package/dist/lib/mock-server/index.d.ts +2 -0
  20. package/dist/lib/mock-server/index.d.ts.map +1 -1
  21. package/dist/lib/mock-server/index.js +17 -0
  22. package/dist/lib/mock-server/index.js.map +1 -1
  23. package/dist/lib/mock-server/sales-guaranteed/recipe.d.ts +155 -0
  24. package/dist/lib/mock-server/sales-guaranteed/recipe.d.ts.map +1 -0
  25. package/dist/lib/mock-server/sales-guaranteed/recipe.js +107 -0
  26. package/dist/lib/mock-server/sales-guaranteed/recipe.js.map +1 -0
  27. package/dist/lib/mock-server/sales-guaranteed/server.d.ts.map +1 -1
  28. package/dist/lib/mock-server/sales-guaranteed/server.js +212 -0
  29. package/dist/lib/mock-server/sales-guaranteed/server.js.map +1 -1
  30. package/dist/lib/mock-server/sales-non-guaranteed/recipe.d.ts +123 -0
  31. package/dist/lib/mock-server/sales-non-guaranteed/recipe.d.ts.map +1 -0
  32. package/dist/lib/mock-server/sales-non-guaranteed/recipe.js +81 -0
  33. package/dist/lib/mock-server/sales-non-guaranteed/recipe.js.map +1 -0
  34. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  35. package/dist/lib/server/ctx-metadata/index.d.ts +1 -1
  36. package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -1
  37. package/dist/lib/server/ctx-metadata/index.js +3 -1
  38. package/dist/lib/server/ctx-metadata/index.js.map +1 -1
  39. package/dist/lib/server/ctx-metadata/wire-shape.d.ts +21 -0
  40. package/dist/lib/server/ctx-metadata/wire-shape.d.ts.map +1 -1
  41. package/dist/lib/server/ctx-metadata/wire-shape.js +111 -0
  42. package/dist/lib/server/ctx-metadata/wire-shape.js.map +1 -1
  43. package/dist/lib/server/decisioning/context.d.ts +19 -0
  44. package/dist/lib/server/decisioning/context.d.ts.map +1 -1
  45. package/dist/lib/server/decisioning/index.d.ts +3 -0
  46. package/dist/lib/server/decisioning/index.d.ts.map +1 -1
  47. package/dist/lib/server/decisioning/index.js +16 -1
  48. package/dist/lib/server/decisioning/index.js.map +1 -1
  49. package/dist/lib/server/decisioning/platform.d.ts +17 -0
  50. package/dist/lib/server/decisioning/platform.d.ts.map +1 -1
  51. package/dist/lib/server/decisioning/platform.js.map +1 -1
  52. package/dist/lib/server/decisioning/proposal/dispatch.d.ts +203 -0
  53. package/dist/lib/server/decisioning/proposal/dispatch.d.ts.map +1 -0
  54. package/dist/lib/server/decisioning/proposal/dispatch.js +395 -0
  55. package/dist/lib/server/decisioning/proposal/dispatch.js.map +1 -0
  56. package/dist/lib/server/decisioning/proposal/index.d.ts +21 -0
  57. package/dist/lib/server/decisioning/proposal/index.d.ts.map +1 -0
  58. package/dist/lib/server/decisioning/proposal/index.js +37 -0
  59. package/dist/lib/server/decisioning/proposal/index.js.map +1 -0
  60. package/dist/lib/server/decisioning/proposal/lifecycle.d.ts +195 -0
  61. package/dist/lib/server/decisioning/proposal/lifecycle.d.ts.map +1 -0
  62. package/dist/lib/server/decisioning/proposal/lifecycle.js +366 -0
  63. package/dist/lib/server/decisioning/proposal/lifecycle.js.map +1 -0
  64. package/dist/lib/server/decisioning/proposal/mock-manager.d.ts +93 -0
  65. package/dist/lib/server/decisioning/proposal/mock-manager.d.ts.map +1 -0
  66. package/dist/lib/server/decisioning/proposal/mock-manager.js +109 -0
  67. package/dist/lib/server/decisioning/proposal/mock-manager.js.map +1 -0
  68. package/dist/lib/server/decisioning/proposal/store.d.ts +279 -0
  69. package/dist/lib/server/decisioning/proposal/store.d.ts.map +1 -0
  70. package/dist/lib/server/decisioning/proposal/store.js +291 -0
  71. package/dist/lib/server/decisioning/proposal/store.js.map +1 -0
  72. package/dist/lib/server/decisioning/proposal/types.d.ts +394 -0
  73. package/dist/lib/server/decisioning/proposal/types.d.ts.map +1 -0
  74. package/dist/lib/server/decisioning/proposal/types.js +58 -0
  75. package/dist/lib/server/decisioning/proposal/types.js.map +1 -0
  76. package/dist/lib/server/decisioning/runtime/from-platform.d.ts +25 -0
  77. package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
  78. package/dist/lib/server/decisioning/runtime/from-platform.js +198 -15
  79. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  80. package/dist/lib/server/index.d.ts +1 -1
  81. package/dist/lib/server/index.d.ts.map +1 -1
  82. package/dist/lib/server/index.js +3 -1
  83. package/dist/lib/server/index.js.map +1 -1
  84. package/dist/lib/testing/client.d.ts.map +1 -1
  85. package/dist/lib/testing/client.js +7 -1
  86. package/dist/lib/testing/client.js.map +1 -1
  87. package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
  88. package/dist/lib/testing/storyboard/task-map.js +1 -0
  89. package/dist/lib/testing/storyboard/task-map.js.map +1 -1
  90. package/dist/lib/testing/storyboard/test-kit.d.ts.map +1 -1
  91. package/dist/lib/testing/storyboard/test-kit.js +4 -0
  92. package/dist/lib/testing/storyboard/test-kit.js.map +1 -1
  93. package/dist/lib/testing/types.d.ts +10 -0
  94. package/dist/lib/testing/types.d.ts.map +1 -1
  95. package/dist/lib/version.d.ts +3 -3
  96. package/dist/lib/version.js +3 -3
  97. package/examples/hello_seller_adapter_guaranteed.ts +29 -2
  98. package/examples/hello_seller_adapter_proposal_mode.ts +575 -0
  99. package/package.json +1 -1
  100. package/dist/lib/adapters/proposal-manager.d.ts +0 -142
  101. package/dist/lib/adapters/proposal-manager.d.ts.map +0 -1
  102. package/dist/lib/adapters/proposal-manager.js +0 -184
  103. 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
  }
@@ -1572,7 +1583,18 @@ async function dispatchHitl(taskRegistry, opts, taskFn, overrideTaskId) {
1572
1583
  taskFnError = err;
1573
1584
  }
1574
1585
  if (taskFnError === undefined) {
1575
- // 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
+ }
1576
1598
  try {
1577
1599
  await taskRegistry.complete(taskId, result);
1578
1600
  }
@@ -2393,9 +2415,11 @@ function validatePushNotificationToken(token) {
2393
2415
  }
2394
2416
  return { ok: true };
2395
2417
  }
2396
- function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observability, logger, pushOpts, ctxFor, ctxMetadataStore, mediaBuyStore) {
2418
+ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observability, logger, pushOpts, ctxFor, ctxMetadataStore, mediaBuyStore, proposalStore) {
2397
2419
  const sales = platform.sales;
2398
- if (!sales)
2420
+ const proposalManager = platform.proposalManager;
2421
+ // Without sales AND without a proposal manager, there's nothing to dispatch.
2422
+ if (!sales && !proposalManager)
2399
2423
  return undefined;
2400
2424
  // Core lifecycle methods are optional on the SalesPlatform interface
2401
2425
  // (#1341) — the per-specialism mapping in `RequiredPlatformsFor<S>`
@@ -2407,21 +2431,97 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2407
2431
  // (`opts.mediaBuy.X`) can supply it OR the framework returns
2408
2432
  // `METHOD_NOT_FOUND` from `tools/list` for the unsupported tool.
2409
2433
  return {
2410
- ...(sales.getProducts && {
2434
+ ...((sales?.getProducts || proposalManager) && {
2411
2435
  getProducts: async (params, ctx) => {
2412
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
+ }
2413
2486
  return projectSync(async () => {
2414
- 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
+ }
2415
2505
  // Auto-store products: persist each Product's wire shape +
2416
2506
  // ctx_metadata so subsequent createMediaBuy / updateMediaBuy
2417
2507
  // calls referencing product_id can hydrate the full Product
2418
2508
  // automatically (publisher sees `req.packages[i].product`).
2419
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
+ }
2420
2520
  return result;
2421
2521
  }, r => r);
2422
2522
  },
2423
2523
  }),
2424
- ...(sales.createMediaBuy && {
2524
+ ...(sales?.createMediaBuy && {
2425
2525
  createMediaBuy: async (params, ctx) => {
2426
2526
  const reqCtx = ctxFor(ctx);
2427
2527
  // Auto-hydrate: walk `params.packages`, attach the full Product object
@@ -2429,11 +2529,59 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2429
2529
  // `pkg.product.format_ids`, `pkg.product.ctx_metadata?.gam?.ad_unit_ids`
2430
2530
  // directly — no separate lookup, no boilerplate.
2431
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
+ }
2432
2549
  return projectSync(async () => {
2433
2550
  const push = extractPushConfig(params, logger, {
2434
2551
  allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls,
2435
2552
  });
2436
- 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
+ }
2437
2585
  return routeIfHandoff(taskRegistry, {
2438
2586
  tool: 'create_media_buy',
2439
2587
  accountId: reqCtx.account.id,
@@ -2450,7 +2598,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2450
2598
  }, r => r);
2451
2599
  },
2452
2600
  }),
2453
- ...(sales.updateMediaBuy && {
2601
+ ...(sales?.updateMediaBuy && {
2454
2602
  updateMediaBuy: async (params, ctx) => {
2455
2603
  const reqCtx = ctxFor(ctx);
2456
2604
  // `media_buy_id` is required on the wire schema, but `validation: 'off'`
@@ -2469,6 +2617,21 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2469
2617
  // directly — no separate lookup. Misses are silent; publisher falls
2470
2618
  // back to its own DB. Schema-driven via `x-entity` (#1109).
2471
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
+ }
2472
2635
  return projectSync(async () => {
2473
2636
  const push = extractPushConfig(params, logger, {
2474
2637
  allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls,
@@ -2511,7 +2674,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2511
2674
  syncCreatives: async (params, ctx) => {
2512
2675
  const reqCtx = ctxFor(ctx);
2513
2676
  const creatives = params.creatives ?? [];
2514
- if (!sales.syncCreatives) {
2677
+ if (!sales?.syncCreatives) {
2515
2678
  return (0, errors_1.adcpError)('UNSUPPORTED_FEATURE', {
2516
2679
  message: 'sync_creatives not supported by this sales platform',
2517
2680
  });
@@ -2531,9 +2694,29 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2531
2694
  }, result, rows => ({ creatives: rows.map(normalizeRowErrors) }));
2532
2695
  }, r => r);
2533
2696
  },
2534
- ...(sales.getMediaBuyDelivery && {
2697
+ ...(sales?.getMediaBuyDelivery && {
2535
2698
  getMediaBuyDelivery: async (params, ctx) => {
2536
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
+ }
2537
2720
  return projectSync(async () => {
2538
2721
  const result = await sales.getMediaBuyDelivery(params, reqCtx);
2539
2722
  warnIfTruncatedMultiIdResponse('getMediaBuyDelivery', 'media_buy_ids', params.media_buy_ids, result?.media_buy_deliveries, logger);
@@ -2554,7 +2737,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2554
2737
  // platform-derived handler when absent lets `mergeHandlers` pick up the
2555
2738
  // adopter's custom handler from `opts.mediaBuy` instead of throwing
2556
2739
  // `sales.getMediaBuys is not a function`.
2557
- ...(sales.getMediaBuys && {
2740
+ ...(sales?.getMediaBuys && {
2558
2741
  getMediaBuys: async (params, ctx) => {
2559
2742
  const reqCtx = ctxFor(ctx);
2560
2743
  return projectSync(async () => {
@@ -2566,7 +2749,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2566
2749
  }, r => r);
2567
2750
  },
2568
2751
  }),
2569
- ...(sales.providePerformanceFeedback && {
2752
+ ...(sales?.providePerformanceFeedback && {
2570
2753
  providePerformanceFeedback: async (params, ctx) => {
2571
2754
  const reqCtx = ctxFor(ctx);
2572
2755
  // Auto-hydrate `req.media_buy` from the prior createMediaBuy /
@@ -2581,13 +2764,13 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2581
2764
  return projectSync(() => sales.providePerformanceFeedback(params, reqCtx), r => r);
2582
2765
  },
2583
2766
  }),
2584
- ...(sales.listCreativeFormats && {
2767
+ ...(sales?.listCreativeFormats && {
2585
2768
  listCreativeFormats: async (params, ctx) => {
2586
2769
  const reqCtx = ctxFor(ctx);
2587
2770
  return projectSync(() => sales.listCreativeFormats(params, reqCtx), r => r);
2588
2771
  },
2589
2772
  }),
2590
- ...(sales.listCreatives && {
2773
+ ...(sales?.listCreatives && {
2591
2774
  listCreatives: async (params, ctx) => {
2592
2775
  const reqCtx = ctxFor(ctx);
2593
2776
  return projectSync(async () => {