@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.
- package/bin/adcp-config.js +7 -1
- package/bin/adcp.js +191 -4
- package/dist/lib/adapters/index.d.ts +0 -2
- package/dist/lib/adapters/index.d.ts.map +1 -1
- package/dist/lib/adapters/index.js +1 -8
- package/dist/lib/adapters/index.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +2 -7
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/mock-server/index.d.ts +2 -0
- package/dist/lib/mock-server/index.d.ts.map +1 -1
- package/dist/lib/mock-server/index.js +17 -0
- package/dist/lib/mock-server/index.js.map +1 -1
- package/dist/lib/mock-server/sales-guaranteed/recipe.d.ts +155 -0
- package/dist/lib/mock-server/sales-guaranteed/recipe.d.ts.map +1 -0
- package/dist/lib/mock-server/sales-guaranteed/recipe.js +107 -0
- package/dist/lib/mock-server/sales-guaranteed/recipe.js.map +1 -0
- package/dist/lib/mock-server/sales-guaranteed/server.d.ts.map +1 -1
- package/dist/lib/mock-server/sales-guaranteed/server.js +212 -0
- package/dist/lib/mock-server/sales-guaranteed/server.js.map +1 -1
- package/dist/lib/mock-server/sales-non-guaranteed/recipe.d.ts +123 -0
- package/dist/lib/mock-server/sales-non-guaranteed/recipe.d.ts.map +1 -0
- package/dist/lib/mock-server/sales-non-guaranteed/recipe.js +81 -0
- package/dist/lib/mock-server/sales-non-guaranteed/recipe.js.map +1 -0
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/ctx-metadata/index.d.ts +1 -1
- package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -1
- package/dist/lib/server/ctx-metadata/index.js +3 -1
- package/dist/lib/server/ctx-metadata/index.js.map +1 -1
- package/dist/lib/server/ctx-metadata/wire-shape.d.ts +21 -0
- package/dist/lib/server/ctx-metadata/wire-shape.d.ts.map +1 -1
- package/dist/lib/server/ctx-metadata/wire-shape.js +111 -0
- package/dist/lib/server/ctx-metadata/wire-shape.js.map +1 -1
- package/dist/lib/server/decisioning/async-outcome.d.ts +17 -0
- package/dist/lib/server/decisioning/async-outcome.d.ts.map +1 -1
- package/dist/lib/server/decisioning/async-outcome.js +23 -18
- package/dist/lib/server/decisioning/async-outcome.js.map +1 -1
- package/dist/lib/server/decisioning/context.d.ts +27 -2
- package/dist/lib/server/decisioning/context.d.ts.map +1 -1
- package/dist/lib/server/decisioning/index.d.ts +4 -0
- package/dist/lib/server/decisioning/index.d.ts.map +1 -1
- package/dist/lib/server/decisioning/index.js +16 -1
- package/dist/lib/server/decisioning/index.js.map +1 -1
- package/dist/lib/server/decisioning/platform.d.ts +17 -0
- package/dist/lib/server/decisioning/platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/platform.js.map +1 -1
- package/dist/lib/server/decisioning/proposal/dispatch.d.ts +203 -0
- package/dist/lib/server/decisioning/proposal/dispatch.d.ts.map +1 -0
- package/dist/lib/server/decisioning/proposal/dispatch.js +395 -0
- package/dist/lib/server/decisioning/proposal/dispatch.js.map +1 -0
- package/dist/lib/server/decisioning/proposal/index.d.ts +21 -0
- package/dist/lib/server/decisioning/proposal/index.d.ts.map +1 -0
- package/dist/lib/server/decisioning/proposal/index.js +37 -0
- package/dist/lib/server/decisioning/proposal/index.js.map +1 -0
- package/dist/lib/server/decisioning/proposal/lifecycle.d.ts +195 -0
- package/dist/lib/server/decisioning/proposal/lifecycle.d.ts.map +1 -0
- package/dist/lib/server/decisioning/proposal/lifecycle.js +366 -0
- package/dist/lib/server/decisioning/proposal/lifecycle.js.map +1 -0
- package/dist/lib/server/decisioning/proposal/mock-manager.d.ts +93 -0
- package/dist/lib/server/decisioning/proposal/mock-manager.d.ts.map +1 -0
- package/dist/lib/server/decisioning/proposal/mock-manager.js +109 -0
- package/dist/lib/server/decisioning/proposal/mock-manager.js.map +1 -0
- package/dist/lib/server/decisioning/proposal/store.d.ts +279 -0
- package/dist/lib/server/decisioning/proposal/store.d.ts.map +1 -0
- package/dist/lib/server/decisioning/proposal/store.js +291 -0
- package/dist/lib/server/decisioning/proposal/store.js.map +1 -0
- package/dist/lib/server/decisioning/proposal/types.d.ts +394 -0
- package/dist/lib/server/decisioning/proposal/types.d.ts.map +1 -0
- package/dist/lib/server/decisioning/proposal/types.js +58 -0
- package/dist/lib/server/decisioning/proposal/types.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts +25 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +204 -19
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +5 -2
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts +5 -0
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/task-registry.js +4 -1
- package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/to-context.js +10 -2
- package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -1
- package/dist/lib/server/index.d.ts +1 -1
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +3 -1
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/test-controller.d.ts +2 -0
- package/dist/lib/server/test-controller.d.ts.map +1 -1
- package/dist/lib/server/test-controller.js +6 -11
- package/dist/lib/server/test-controller.js.map +1 -1
- package/dist/lib/testing/client.d.ts.map +1 -1
- package/dist/lib/testing/client.js +7 -1
- package/dist/lib/testing/client.js.map +1 -1
- package/dist/lib/testing/comply-controller.d.ts +2 -0
- package/dist/lib/testing/comply-controller.d.ts.map +1 -1
- package/dist/lib/testing/comply-controller.js.map +1 -1
- package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/task-map.js +1 -0
- package/dist/lib/testing/storyboard/task-map.js.map +1 -1
- package/dist/lib/testing/storyboard/test-kit.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/test-kit.js +4 -0
- package/dist/lib/testing/storyboard/test-kit.js.map +1 -1
- package/dist/lib/testing/types.d.ts +10 -0
- package/dist/lib/testing/types.d.ts.map +1 -1
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/examples/hello_seller_adapter_guaranteed.ts +29 -2
- package/examples/hello_seller_adapter_proposal_mode.ts +575 -0
- package/package.json +1 -1
- package/dist/lib/adapters/proposal-manager.d.ts +0 -142
- package/dist/lib/adapters/proposal-manager.d.ts.map +0 -1
- package/dist/lib/adapters/proposal-manager.js +0 -184
- 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
|
|
1419
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2773
|
+
...(sales?.listCreatives && {
|
|
2589
2774
|
listCreatives: async (params, ctx) => {
|
|
2590
2775
|
const reqCtx = ctxFor(ctx);
|
|
2591
2776
|
return projectSync(async () => {
|