@adcp/sdk 7.3.0 → 7.5.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/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.ts +58 -14
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +68 -26
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/errors/index.d.ts +8 -3
- package/dist/lib/errors/index.d.ts.map +1 -1
- package/dist/lib/errors/index.js +1 -1
- package/dist/lib/errors/index.js.map +1 -1
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/protocols/index.d.ts +16 -6
- package/dist/lib/protocols/index.d.ts.map +1 -1
- package/dist/lib/protocols/index.js.map +1 -1
- package/dist/lib/protocols/responseSizeLimit.js +7 -0
- package/dist/lib/protocols/responseSizeLimit.js.map +1 -1
- package/dist/lib/registry/index.d.ts +36 -3
- package/dist/lib/registry/index.d.ts.map +1 -1
- package/dist/lib/registry/index.js +41 -5
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/create-adcp-server.d.ts +95 -0
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +543 -35
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/example-tld-guard.d.ts +9 -0
- package/dist/lib/server/example-tld-guard.d.ts.map +1 -0
- package/dist/lib/server/example-tld-guard.js +25 -0
- package/dist/lib/server/example-tld-guard.js.map +1 -0
- package/dist/lib/server/index.d.ts +5 -3
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +17 -4
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/test-controller-bridge.d.ts +885 -1
- package/dist/lib/server/test-controller-bridge.d.ts.map +1 -1
- package/dist/lib/server/test-controller-bridge.js +1502 -2
- package/dist/lib/server/test-controller-bridge.js.map +1 -1
- package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
- package/dist/lib/testing/compliance/comply.js +130 -6
- package/dist/lib/testing/compliance/comply.js.map +1 -1
- package/dist/lib/testing/compliance/index.d.ts +1 -1
- package/dist/lib/testing/compliance/index.d.ts.map +1 -1
- package/dist/lib/testing/compliance/types.d.ts +83 -0
- package/dist/lib/testing/compliance/types.d.ts.map +1 -1
- package/dist/lib/testing/index.d.ts +3 -3
- package/dist/lib/testing/index.d.ts.map +1 -1
- package/dist/lib/testing/index.js +17 -3
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/types/core.generated.d.ts +1212 -20
- package/dist/lib/types/core.generated.d.ts.map +1 -1
- package/dist/lib/types/core.generated.js +1 -1
- package/dist/lib/types/inline-enums.generated.d.ts +6 -2
- package/dist/lib/types/inline-enums.generated.d.ts.map +1 -1
- package/dist/lib/types/inline-enums.generated.js +11 -5
- package/dist/lib/types/inline-enums.generated.js.map +1 -1
- package/dist/lib/types/schemas.generated.d.ts +26202 -26253
- package/dist/lib/types/schemas.generated.d.ts.map +1 -1
- package/dist/lib/types/schemas.generated.js +1353 -1300
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/tools.generated.d.ts +1082 -21
- package/dist/lib/types/tools.generated.d.ts.map +1 -1
- package/dist/lib/types/wellknown-schemas.generated.d.ts +2 -2
- package/dist/lib/validation/sync-creatives.d.ts +6 -6
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/examples/hello_creative_adapter_ad_server.ts +5 -0
- package/examples/hello_creative_adapter_template.ts +5 -0
- package/examples/hello_seller_adapter_guaranteed.ts +5 -0
- package/examples/hello_seller_adapter_non_guaranteed.ts +5 -0
- package/examples/hello_seller_adapter_social.ts +5 -0
- package/examples/hello_si_adapter_brand.ts +5 -0
- package/package.json +2 -2
- package/skills/call-adcp-agent/SKILL.md +1 -0
|
@@ -18,11 +18,90 @@
|
|
|
18
18
|
* Sellers provide a `getSeededProducts` callback that returns the list the
|
|
19
19
|
* SDK should merge — which lets the same wiring work whether the backing
|
|
20
20
|
* store is in-memory, Postgres, Redis, or a mock.
|
|
21
|
+
*
|
|
22
|
+
* ## Scope of verification (storyboard pass through this bridge)
|
|
23
|
+
*
|
|
24
|
+
* A storyboard run that succeeds because seeded fixtures were merged into
|
|
25
|
+
* the response verifies **protocol conformance against fixture data**:
|
|
26
|
+
* wire shape, error envelopes, idempotency, signed-request handling,
|
|
27
|
+
* sandbox stamping. It does **not** verify that the seller's adapter
|
|
28
|
+
* against the real upstream (e.g., social / search / programmatic
|
|
29
|
+
* inventory APIs) is working — the upstream response is shadowed by the
|
|
30
|
+
* post-handler merge.
|
|
31
|
+
*
|
|
32
|
+
* Treat this bridge as the conformance equivalent of a recorded-fixtures
|
|
33
|
+
* unit test, not an end-to-end integration test. Sellers should still
|
|
34
|
+
* exercise their adapters against a real (or sandbox) upstream OAuth tier
|
|
35
|
+
* separately; the typical pattern is a CLI runner pointed at a deployed
|
|
36
|
+
* sandbox URL with live credentials. The two together — storyboard-via-
|
|
37
|
+
* bridge plus live-OAuth runner — give wire conformance and adapter
|
|
38
|
+
* health respectively. See adcp-client#1775 for the cross-repo
|
|
39
|
+
* coordination on making bridge participation visible in storyboard
|
|
40
|
+
* run records.
|
|
41
|
+
*
|
|
42
|
+
* ## Adopter responsibilities
|
|
43
|
+
*
|
|
44
|
+
* **`resolveAccount` is the trust boundary.** The dispatcher's sandbox
|
|
45
|
+
* gate is "request carries a sandbox marker AND (resolved account is
|
|
46
|
+
* sandbox OR no account was resolved)." If you deploy a server with this
|
|
47
|
+
* bridge registered but no `resolveAccount` configured, a buyer can stamp
|
|
48
|
+
* `context.sandbox: true` on a request and trigger the merge. That's the
|
|
49
|
+
* intended behavior for storyboard runners with no account scoping, but
|
|
50
|
+
* means **production bindings must always configure `resolveAccount`** —
|
|
51
|
+
* otherwise the buyer-supplied sandbox marker is the only gate.
|
|
52
|
+
*
|
|
53
|
+
* **Multi-tenant isolation is the adopter's job.** Callbacks receive
|
|
54
|
+
* `ctx.account` and must key their fixture store on it. The SDK does no
|
|
55
|
+
* defensive cross-check between the account on the response entries and
|
|
56
|
+
* the `ctx.account` that asked for them. A sloppy session-store keying
|
|
57
|
+
* can return tenant A's fixtures to tenant B; nothing in this module
|
|
58
|
+
* will notice. Treat fixture stores like any other multi-tenant data
|
|
59
|
+
* layer.
|
|
21
60
|
*/
|
|
22
61
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
62
|
exports.isSandboxRequest = isSandboxRequest;
|
|
24
63
|
exports.mergeSeededProductsIntoResponse = mergeSeededProductsIntoResponse;
|
|
25
64
|
exports.filterValidSeededProducts = filterValidSeededProducts;
|
|
65
|
+
exports.filterValidSeededCreatives = filterValidSeededCreatives;
|
|
66
|
+
exports.filterValidSeededMediaBuys = filterValidSeededMediaBuys;
|
|
67
|
+
exports.filterValidSeededMediaBuyDeliveries = filterValidSeededMediaBuyDeliveries;
|
|
68
|
+
exports.filterValidSeededAccounts = filterValidSeededAccounts;
|
|
69
|
+
exports.filterValidSeededAccountFinancials = filterValidSeededAccountFinancials;
|
|
70
|
+
exports.filterValidSeededCreativeFormats = filterValidSeededCreativeFormats;
|
|
71
|
+
exports.mergeSeededCreativesIntoResponse = mergeSeededCreativesIntoResponse;
|
|
72
|
+
exports.mergeSeededMediaBuysIntoResponse = mergeSeededMediaBuysIntoResponse;
|
|
73
|
+
exports.recomputeAggregatedTotals = recomputeAggregatedTotals;
|
|
74
|
+
exports.mergeSeededMediaBuyDeliveryIntoResponse = mergeSeededMediaBuyDeliveryIntoResponse;
|
|
75
|
+
exports.mergeSeededAccountsIntoResponse = mergeSeededAccountsIntoResponse;
|
|
76
|
+
exports.pickSeededAccountFinancialsForRequest = pickSeededAccountFinancialsForRequest;
|
|
77
|
+
exports.replaceAccountFinancialsIfSeeded = replaceAccountFinancialsIfSeeded;
|
|
78
|
+
exports.mergeSeededCreativeFormatsIntoResponse = mergeSeededCreativeFormatsIntoResponse;
|
|
79
|
+
exports.filterValidSeededPropertyLists = filterValidSeededPropertyLists;
|
|
80
|
+
exports.filterValidSeededCollectionLists = filterValidSeededCollectionLists;
|
|
81
|
+
exports.filterValidSeededContentStandards = filterValidSeededContentStandards;
|
|
82
|
+
exports.mergeSeededPropertyListsIntoResponse = mergeSeededPropertyListsIntoResponse;
|
|
83
|
+
exports.mergeSeededCollectionListsIntoResponse = mergeSeededCollectionListsIntoResponse;
|
|
84
|
+
exports.mergeSeededContentStandardsIntoResponse = mergeSeededContentStandardsIntoResponse;
|
|
85
|
+
exports.pickSeededPropertyListForRequest = pickSeededPropertyListForRequest;
|
|
86
|
+
exports.replacePropertyListIfSeeded = replacePropertyListIfSeeded;
|
|
87
|
+
exports.pickSeededCollectionListForRequest = pickSeededCollectionListForRequest;
|
|
88
|
+
exports.replaceCollectionListIfSeeded = replaceCollectionListIfSeeded;
|
|
89
|
+
exports.pickSeededContentStandardsForRequest = pickSeededContentStandardsForRequest;
|
|
90
|
+
exports.replaceContentStandardsIfSeeded = replaceContentStandardsIfSeeded;
|
|
91
|
+
exports.filterValidSeededSignals = filterValidSeededSignals;
|
|
92
|
+
exports.filterValidSeededCreativeDelivery = filterValidSeededCreativeDelivery;
|
|
93
|
+
exports.filterValidSeededCreativeFeatures = filterValidSeededCreativeFeatures;
|
|
94
|
+
exports.mergeSeededSignalsIntoResponse = mergeSeededSignalsIntoResponse;
|
|
95
|
+
exports.mergeSeededCreativeDeliveryIntoResponse = mergeSeededCreativeDeliveryIntoResponse;
|
|
96
|
+
exports.mergeSeededCreativeFeaturesIntoResponse = mergeSeededCreativeFeaturesIntoResponse;
|
|
97
|
+
exports.filterValidSeededBrandIdentity = filterValidSeededBrandIdentity;
|
|
98
|
+
exports.filterValidSeededRights = filterValidSeededRights;
|
|
99
|
+
exports.filterValidSeededSiOffering = filterValidSeededSiOffering;
|
|
100
|
+
exports.mergeSeededRightsIntoResponse = mergeSeededRightsIntoResponse;
|
|
101
|
+
exports.pickSeededBrandIdentityForRequest = pickSeededBrandIdentityForRequest;
|
|
102
|
+
exports.replaceBrandIdentityIfSeeded = replaceBrandIdentityIfSeeded;
|
|
103
|
+
exports.pickSeededSiOfferingForRequest = pickSeededSiOfferingForRequest;
|
|
104
|
+
exports.replaceSiOfferingIfSeeded = replaceSiOfferingIfSeeded;
|
|
26
105
|
exports.bridgeFromTestControllerStore = bridgeFromTestControllerStore;
|
|
27
106
|
exports.bridgeFromSessionStore = bridgeFromSessionStore;
|
|
28
107
|
const seed_merge_1 = require("../testing/seed-merge");
|
|
@@ -50,6 +129,25 @@ function isSandboxRequest(input) {
|
|
|
50
129
|
}
|
|
51
130
|
return false;
|
|
52
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Detect the Submitted-arm shape (`{ status: 'submitted', task_id }`) on a
|
|
134
|
+
* read-tool response payload. `get_products` formally permits this arm per
|
|
135
|
+
* `schemas/cache/3.0.11/media-buy/get-products-async-response-submitted.json`
|
|
136
|
+
* (queued custom/bespoke product curation); the dispatcher's
|
|
137
|
+
* `isSubmittedEnvelope` routes the wrap, but the bridge merge runs after
|
|
138
|
+
* wrap and would spread `products: [...]` into the Submitted envelope
|
|
139
|
+
* without this guard.
|
|
140
|
+
*
|
|
141
|
+
* Mirrors the `isSubmittedEnvelope` predicate at
|
|
142
|
+
* `src/lib/server/create-adcp-server.ts` — kept local rather than imported
|
|
143
|
+
* because that helper lives inside the dispatcher closure.
|
|
144
|
+
*/
|
|
145
|
+
function isSubmittedArm(value) {
|
|
146
|
+
if (value == null || typeof value !== 'object')
|
|
147
|
+
return false;
|
|
148
|
+
const obj = value;
|
|
149
|
+
return obj.status === 'submitted' && typeof obj.task_id === 'string';
|
|
150
|
+
}
|
|
53
151
|
/**
|
|
54
152
|
* Merge seeded products into a `get_products` response payload.
|
|
55
153
|
*
|
|
@@ -62,10 +160,29 @@ function isSandboxRequest(input) {
|
|
|
62
160
|
* handler explicitly declared `sandbox: false` (which stays authoritative
|
|
63
161
|
* — a handler that has already decided the request is non-sandbox
|
|
64
162
|
* shouldn't be overridden by the bridge).
|
|
163
|
+
*
|
|
164
|
+
* **Submitted-arm short-circuit.** `get_products` formally permits an
|
|
165
|
+
* async Submitted arm per `schemas/cache/3.0.11/media-buy/get-products-async-response-submitted.json`
|
|
166
|
+
* (queued custom/bespoke curation). The dispatcher routes
|
|
167
|
+
* `{ status: 'submitted', task_id }` handler returns through
|
|
168
|
+
* `wrapSubmittedEnvelope`, but the bridge merge then receives the
|
|
169
|
+
* unwrapped Submitted body from `formatted.structuredContent`. Without
|
|
170
|
+
* this guard, the merge would spread `products: [...]` into that body
|
|
171
|
+
* and produce a `{ status: 'submitted', task_id, products: [...], sandbox: true }`
|
|
172
|
+
* hybrid that violates the wire schema. Detect the Submitted shape and
|
|
173
|
+
* return the handler response reference-equal so the dispatcher's
|
|
174
|
+
* skip-on-reference-equality wrap-avoidance kicks in.
|
|
175
|
+
*
|
|
176
|
+
* None of the other 12 bridged read tools have a formal Submitted arm
|
|
177
|
+
* in 3.0.11 per `schemas/cache/3.0.11/core/async-response-data.json`;
|
|
178
|
+
* this guard is `get_products`-specific defense rather than a uniform
|
|
179
|
+
* pattern across helpers.
|
|
65
180
|
*/
|
|
66
181
|
function mergeSeededProductsIntoResponse(response, seeded) {
|
|
67
182
|
if (!seeded.length)
|
|
68
183
|
return response;
|
|
184
|
+
if (isSubmittedArm(response))
|
|
185
|
+
return response;
|
|
69
186
|
const seededIds = new Set();
|
|
70
187
|
for (const p of seeded)
|
|
71
188
|
seededIds.add(p.product_id);
|
|
@@ -116,6 +233,1277 @@ function filterValidSeededProducts(raw, logger) {
|
|
|
116
233
|
});
|
|
117
234
|
return valid;
|
|
118
235
|
}
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Per-tool validation + merge helpers
|
|
238
|
+
//
|
|
239
|
+
// Each helper pair (`filterValidSeededXxx` + `mergeSeededXxxIntoResponse`)
|
|
240
|
+
// mirrors the `filterValidSeededProducts` + `mergeSeededProductsIntoResponse`
|
|
241
|
+
// shape. The semantics are identical: validate-and-drop on the input side,
|
|
242
|
+
// dedupe-and-stamp-sandbox on the merge side. Symmetry is the point — adopters
|
|
243
|
+
// who understand the products seam should be able to read the others at a
|
|
244
|
+
// glance.
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
/**
|
|
247
|
+
* Validate seeded creatives. Drops entries that are not plain objects or are
|
|
248
|
+
* missing a non-empty string `creative_id` — matches the products contract
|
|
249
|
+
* (a missing identifier collides on `undefined === undefined` when deduping).
|
|
250
|
+
*/
|
|
251
|
+
function filterValidSeededCreatives(raw, logger) {
|
|
252
|
+
return filterValidById(raw, 'creative_id', 'getSeededCreatives', logger);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Validate seeded media buys. Drops entries missing a non-empty string
|
|
256
|
+
* `media_buy_id`.
|
|
257
|
+
*/
|
|
258
|
+
function filterValidSeededMediaBuys(raw, logger) {
|
|
259
|
+
return filterValidById(raw, 'media_buy_id', 'getSeededMediaBuys', logger);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Validate seeded media-buy-delivery snapshots. Drops entries missing a
|
|
263
|
+
* non-empty string `media_buy_id` — the dedup key against the handler set.
|
|
264
|
+
*/
|
|
265
|
+
function filterValidSeededMediaBuyDeliveries(raw, logger) {
|
|
266
|
+
return filterValidById(raw, 'media_buy_id', 'getSeededMediaBuyDelivery', logger);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Validate seeded accounts. Drops entries missing a non-empty string `account_id`.
|
|
270
|
+
*/
|
|
271
|
+
function filterValidSeededAccounts(raw, logger) {
|
|
272
|
+
return filterValidById(raw, 'account_id', 'getSeededAccounts', logger);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Validate seeded account-financials records. Drops entries whose `account`
|
|
276
|
+
* field is not a `{ account_id: string }`-shaped object (`AccountReference`
|
|
277
|
+
* carries `account_id` on the operator-resolved variant; the bridge keys on
|
|
278
|
+
* that field to match against the request's account).
|
|
279
|
+
*
|
|
280
|
+
* Also drops duplicate entries by `account.account_id` (first occurrence
|
|
281
|
+
* wins, matching the array-collection helpers' on-collision-seeded-wins
|
|
282
|
+
* contract — the "first" seeded entry in iteration order is authoritative).
|
|
283
|
+
* A fixture array with two entries for the same `account_id` is almost
|
|
284
|
+
* always a seed-store bug; warn-and-drop surfaces it instead of silently
|
|
285
|
+
* picking whichever happened to come first in iteration order.
|
|
286
|
+
*/
|
|
287
|
+
function filterValidSeededAccountFinancials(raw, logger) {
|
|
288
|
+
if (!Array.isArray(raw)) {
|
|
289
|
+
logger?.warn('testController.getSeededAccountFinancials did not return an array; skipping bridge', {
|
|
290
|
+
received: typeof raw,
|
|
291
|
+
});
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
const valid = [];
|
|
295
|
+
const seenAccountIds = new Set();
|
|
296
|
+
raw.forEach((entry, index) => {
|
|
297
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
298
|
+
logger?.warn('testController.getSeededAccountFinancials entry is not an object; dropping', { index });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const account = entry.account;
|
|
302
|
+
const accountId = account && typeof account === 'object' && !Array.isArray(account)
|
|
303
|
+
? account.account_id
|
|
304
|
+
: undefined;
|
|
305
|
+
if (typeof accountId !== 'string' || accountId.length === 0) {
|
|
306
|
+
logger?.warn('testController.getSeededAccountFinancials entry missing account.account_id; dropping', { index });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (seenAccountIds.has(accountId)) {
|
|
310
|
+
logger?.warn('testController.getSeededAccountFinancials duplicate account.account_id; dropping (first occurrence wins)', { index, account_id: accountId });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
seenAccountIds.add(accountId);
|
|
314
|
+
valid.push(entry);
|
|
315
|
+
});
|
|
316
|
+
return valid;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Validate seeded creative formats. Drops entries whose `format_id` is not a
|
|
320
|
+
* `{ agent_url: string, id: string }`-shaped object — both fields are
|
|
321
|
+
* required to dedupe (and to canonicalize per the URL canonicalization rules
|
|
322
|
+
* in the AdCP spec).
|
|
323
|
+
*/
|
|
324
|
+
function filterValidSeededCreativeFormats(raw, logger) {
|
|
325
|
+
if (!Array.isArray(raw)) {
|
|
326
|
+
logger?.warn('testController.getSeededCreativeFormats did not return an array; skipping bridge', {
|
|
327
|
+
received: typeof raw,
|
|
328
|
+
});
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
const valid = [];
|
|
332
|
+
raw.forEach((entry, index) => {
|
|
333
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
334
|
+
logger?.warn('testController.getSeededCreativeFormats entry is not an object; dropping', { index });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const formatId = entry.format_id;
|
|
338
|
+
if (!formatId || typeof formatId !== 'object' || Array.isArray(formatId)) {
|
|
339
|
+
logger?.warn('testController.getSeededCreativeFormats entry missing format_id; dropping', { index });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const agentUrl = formatId.agent_url;
|
|
343
|
+
const id = formatId.id;
|
|
344
|
+
if (typeof agentUrl !== 'string' || agentUrl.length === 0 || typeof id !== 'string' || id.length === 0) {
|
|
345
|
+
logger?.warn('testController.getSeededCreativeFormats entry has incomplete format_id (agent_url and id required); dropping', { index });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
valid.push(entry);
|
|
349
|
+
});
|
|
350
|
+
return valid;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Shared by-`id` validator for collections whose entries dedupe on a single
|
|
354
|
+
* top-level string field (creatives → `creative_id`, media buys →
|
|
355
|
+
* `media_buy_id`, accounts → `account_id`). Mirrors
|
|
356
|
+
* {@link filterValidSeededProducts} exactly; factored only because the three
|
|
357
|
+
* shapes share the same single-string-key contract.
|
|
358
|
+
*/
|
|
359
|
+
function filterValidById(raw, idField, callbackName, logger) {
|
|
360
|
+
if (!Array.isArray(raw)) {
|
|
361
|
+
logger?.warn(`testController.${callbackName} did not return an array; skipping bridge`, {
|
|
362
|
+
received: typeof raw,
|
|
363
|
+
});
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
const valid = [];
|
|
367
|
+
raw.forEach((entry, index) => {
|
|
368
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
369
|
+
logger?.warn(`testController.${callbackName} entry is not an object; dropping`, { index });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const id = entry[idField];
|
|
373
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
374
|
+
logger?.warn(`testController.${callbackName} entry missing ${idField}; dropping`, { index });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
valid.push(entry);
|
|
378
|
+
});
|
|
379
|
+
return valid;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Merge seeded creatives into a `list_creatives` response. Existing creatives
|
|
383
|
+
* come first; seeded entries append after deduping by `creative_id`. On
|
|
384
|
+
* collision the seeded entry wins. Stamps `sandbox: true` unless the handler
|
|
385
|
+
* explicitly declared `sandbox: false`. Returns a NEW response object.
|
|
386
|
+
*
|
|
387
|
+
* Also updates `query_summary.returned` to match the final array length and
|
|
388
|
+
* `query_summary.total_matching` to `handler.total_matching + (seeded entries
|
|
389
|
+
* that did NOT collide with the handler set)`. Storyboards that assert on
|
|
390
|
+
* counts see the merged totals, not the handler's pre-merge counts. Mirror
|
|
391
|
+
* field on `pagination.total_count` is updated by the same delta when the
|
|
392
|
+
* handler set it.
|
|
393
|
+
*/
|
|
394
|
+
function mergeSeededCreativesIntoResponse(response, seeded) {
|
|
395
|
+
if (!seeded.length)
|
|
396
|
+
return response;
|
|
397
|
+
const seededIds = new Set();
|
|
398
|
+
for (const c of seeded)
|
|
399
|
+
seededIds.add(c.creative_id);
|
|
400
|
+
const existing = Array.isArray(response.creatives) ? response.creatives : [];
|
|
401
|
+
const existingIds = new Set();
|
|
402
|
+
for (const c of existing) {
|
|
403
|
+
if (c && typeof c.creative_id === 'string')
|
|
404
|
+
existingIds.add(c.creative_id);
|
|
405
|
+
}
|
|
406
|
+
const retained = existing.filter(c => !seededIds.has(c?.creative_id));
|
|
407
|
+
const finalCreatives = [...retained, ...seeded];
|
|
408
|
+
// New entries = seeded that did NOT collide with the handler's set. Collisions
|
|
409
|
+
// replace in-place; the merged total_matching shouldn't grow on those.
|
|
410
|
+
let newCount = 0;
|
|
411
|
+
for (const c of seeded)
|
|
412
|
+
if (!existingIds.has(c.creative_id))
|
|
413
|
+
newCount += 1;
|
|
414
|
+
const merged = {
|
|
415
|
+
...response,
|
|
416
|
+
creatives: finalCreatives,
|
|
417
|
+
};
|
|
418
|
+
if (response.sandbox !== false) {
|
|
419
|
+
merged.sandbox = true;
|
|
420
|
+
}
|
|
421
|
+
// query_summary is required on list_creatives per AdCP 3.0.11. Update the
|
|
422
|
+
// counts if the handler provided them; leave the rest of the block
|
|
423
|
+
// (filters_applied, etc.) untouched.
|
|
424
|
+
const qs = response.query_summary;
|
|
425
|
+
if (qs && typeof qs === 'object') {
|
|
426
|
+
const baseTotal = typeof qs.total_matching === 'number' ? qs.total_matching : existing.length;
|
|
427
|
+
merged.query_summary = {
|
|
428
|
+
...qs,
|
|
429
|
+
total_matching: baseTotal + newCount,
|
|
430
|
+
returned: finalCreatives.length,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// pagination.total_count is optional; only update when the handler provided it.
|
|
434
|
+
const pagination = response.pagination;
|
|
435
|
+
if (pagination && typeof pagination === 'object' && typeof pagination.total_count === 'number') {
|
|
436
|
+
merged.pagination = {
|
|
437
|
+
...pagination,
|
|
438
|
+
total_count: pagination.total_count + newCount,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return merged;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Merge seeded media buys into a `get_media_buys` response. Existing media
|
|
445
|
+
* buys come first; seeded entries append after deduping by `media_buy_id`.
|
|
446
|
+
* On collision the seeded entry wins. Returns a NEW response object.
|
|
447
|
+
*
|
|
448
|
+
* `get_media_buys` does NOT carry a `query_summary` block (per AdCP 3.0.11);
|
|
449
|
+
* it exposes its count via `pagination.total_count` (optional). When the
|
|
450
|
+
* handler set `pagination.total_count`, it's incremented by the count of
|
|
451
|
+
* new (non-colliding) seeded entries so the merged response stays
|
|
452
|
+
* internally consistent. Handlers that left `total_count` off pass through
|
|
453
|
+
* unchanged.
|
|
454
|
+
*/
|
|
455
|
+
function mergeSeededMediaBuysIntoResponse(response, seeded) {
|
|
456
|
+
if (!seeded.length)
|
|
457
|
+
return response;
|
|
458
|
+
const seededIds = new Set();
|
|
459
|
+
for (const mb of seeded)
|
|
460
|
+
seededIds.add(mb.media_buy_id);
|
|
461
|
+
const existing = Array.isArray(response.media_buys) ? response.media_buys : [];
|
|
462
|
+
const existingIds = new Set();
|
|
463
|
+
for (const mb of existing) {
|
|
464
|
+
if (mb && typeof mb.media_buy_id === 'string')
|
|
465
|
+
existingIds.add(mb.media_buy_id);
|
|
466
|
+
}
|
|
467
|
+
const retained = existing.filter(mb => !seededIds.has(mb?.media_buy_id));
|
|
468
|
+
let newCount = 0;
|
|
469
|
+
for (const mb of seeded)
|
|
470
|
+
if (!existingIds.has(mb.media_buy_id))
|
|
471
|
+
newCount += 1;
|
|
472
|
+
const merged = {
|
|
473
|
+
...response,
|
|
474
|
+
media_buys: [...retained, ...seeded],
|
|
475
|
+
};
|
|
476
|
+
if (response.sandbox !== false) {
|
|
477
|
+
merged.sandbox = true;
|
|
478
|
+
}
|
|
479
|
+
const pagination = response.pagination;
|
|
480
|
+
if (pagination && typeof pagination === 'object' && typeof pagination.total_count === 'number') {
|
|
481
|
+
merged.pagination = {
|
|
482
|
+
...pagination,
|
|
483
|
+
total_count: pagination.total_count + newCount,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return merged;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Recompute `aggregated_totals` from a merged `media_buy_deliveries` array.
|
|
490
|
+
*
|
|
491
|
+
* The wire schema makes `aggregated_totals.impressions` / `spend` /
|
|
492
|
+
* `media_buy_count` REQUIRED, so once the bridge changes the delivery list it
|
|
493
|
+
* MUST rewrite the totals — otherwise `media_buy_count` is stale and the
|
|
494
|
+
* impressions/spend sums no longer reflect the merged set.
|
|
495
|
+
*
|
|
496
|
+
* Policy (see VALIDATE-YOUR-AGENT.md "Platform-proxy sellers"):
|
|
497
|
+
* - Sum-derived (required): `impressions`, `spend`, `media_buy_count`.
|
|
498
|
+
* - Sum-derived (optional): `clicks`, `completed_views`, `views`,
|
|
499
|
+
* `conversions`, `conversion_value` — recomputed ONLY when EVERY merged
|
|
500
|
+
* delivery populates the field on its `totals`. Otherwise fall back to
|
|
501
|
+
* the handler's value (or omit if the handler omitted).
|
|
502
|
+
* - Derived ratios: `roas` (`conversion_value / spend`),
|
|
503
|
+
* `completion_rate` (`completed_views / impressions`),
|
|
504
|
+
* `cost_per_acquisition` (`spend / conversions`). Recomputed ONLY when
|
|
505
|
+
* both input fields recomputed AND the divisor is non-zero. Otherwise
|
|
506
|
+
* fall back to the handler's value (or omit).
|
|
507
|
+
* - Pass-through (not derivable from per-delivery `totals`): `reach`,
|
|
508
|
+
* `reach_unit`, `frequency`, `new_to_brand_rate`. The handler's values
|
|
509
|
+
* survive verbatim.
|
|
510
|
+
*
|
|
511
|
+
* Empty merged array → `{ impressions: 0, spend: 0, media_buy_count: 0 }` +
|
|
512
|
+
* any pass-through the handler set. Divide-by-zero guards keep ratios omitted
|
|
513
|
+
* rather than producing `Infinity` / `NaN` values that would fail validation.
|
|
514
|
+
*
|
|
515
|
+
* Pure helper — testable in isolation, no I/O.
|
|
516
|
+
*/
|
|
517
|
+
function recomputeAggregatedTotals(deliveries, handlerAggregated) {
|
|
518
|
+
const totalsList = deliveries.map(d => d.totals);
|
|
519
|
+
const sumNumber = (key) => {
|
|
520
|
+
let acc = 0;
|
|
521
|
+
for (const t of totalsList) {
|
|
522
|
+
const v = t[key];
|
|
523
|
+
if (typeof v === 'number')
|
|
524
|
+
acc += v;
|
|
525
|
+
}
|
|
526
|
+
return acc;
|
|
527
|
+
};
|
|
528
|
+
const everyHasNumber = (key) => {
|
|
529
|
+
if (totalsList.length === 0)
|
|
530
|
+
return false;
|
|
531
|
+
for (const t of totalsList) {
|
|
532
|
+
const v = t[key];
|
|
533
|
+
if (typeof v !== 'number')
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
};
|
|
538
|
+
// Required sums: per spec, these fields are REQUIRED on aggregated_totals
|
|
539
|
+
// when the block exists. Empty merged set → zeros (still wire-correct).
|
|
540
|
+
const recomputed = {
|
|
541
|
+
impressions: everyHasNumber('impressions') ? sumNumber('impressions') : 0,
|
|
542
|
+
spend: everyHasNumber('spend') ? sumNumber('spend') : 0,
|
|
543
|
+
media_buy_count: deliveries.length,
|
|
544
|
+
};
|
|
545
|
+
// Optional sums — only recompute when every delivery populates the field.
|
|
546
|
+
// Otherwise fall back to the handler's value to avoid wire-incorrect
|
|
547
|
+
// partial sums.
|
|
548
|
+
const recomputedFields = new Set();
|
|
549
|
+
const optionalSumFields = ['clicks', 'completed_views', 'views', 'conversions', 'conversion_value'];
|
|
550
|
+
for (const field of optionalSumFields) {
|
|
551
|
+
if (everyHasNumber(field)) {
|
|
552
|
+
recomputed[field] = sumNumber(field);
|
|
553
|
+
recomputedFields.add(field);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
const handlerValue = handlerAggregated ? handlerAggregated[field] : undefined;
|
|
557
|
+
if (handlerValue !== undefined)
|
|
558
|
+
recomputed[field] = handlerValue;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Also track recomputed status for the required sum fields — needed for the
|
|
562
|
+
// derived-ratio guards below. `impressions` / `spend` are recomputed when
|
|
563
|
+
// every delivery populates them; empty merged set counts as "recomputed to 0".
|
|
564
|
+
if (everyHasNumber('impressions') || deliveries.length === 0)
|
|
565
|
+
recomputedFields.add('impressions');
|
|
566
|
+
if (everyHasNumber('spend') || deliveries.length === 0)
|
|
567
|
+
recomputedFields.add('spend');
|
|
568
|
+
// Derived ratios — only recompute when BOTH inputs were recomputed AND the
|
|
569
|
+
// divisor is non-zero. Otherwise fall back to the handler's value (or omit).
|
|
570
|
+
const tryRatio = (outKey, numerator, denominator) => {
|
|
571
|
+
if (recomputedFields.has(numerator) && recomputedFields.has(denominator)) {
|
|
572
|
+
const denom = recomputed[denominator];
|
|
573
|
+
const num = recomputed[numerator];
|
|
574
|
+
if (typeof denom === 'number' && denom > 0 && typeof num === 'number') {
|
|
575
|
+
recomputed[outKey] = num / denom;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Divide-by-zero — omit the ratio (don't fall back; the recomputed
|
|
579
|
+
// inputs say the ratio is undefined for this set).
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const handlerValue = handlerAggregated ? handlerAggregated[outKey] : undefined;
|
|
583
|
+
if (handlerValue !== undefined)
|
|
584
|
+
recomputed[outKey] = handlerValue;
|
|
585
|
+
};
|
|
586
|
+
tryRatio('roas', 'conversion_value', 'spend');
|
|
587
|
+
tryRatio('completion_rate', 'completed_views', 'impressions');
|
|
588
|
+
tryRatio('cost_per_acquisition', 'spend', 'conversions');
|
|
589
|
+
// Pass-through fields — not derivable from per-delivery `totals` (reach
|
|
590
|
+
// needs de-dup info we don't have; frequency depends on reach; NTB rate
|
|
591
|
+
// needs an NTB conversion count that isn't carried on per-delivery totals).
|
|
592
|
+
// Preserve handler's values verbatim.
|
|
593
|
+
const passThroughFields = ['reach', 'reach_unit', 'frequency', 'new_to_brand_rate'];
|
|
594
|
+
for (const field of passThroughFields) {
|
|
595
|
+
const handlerValue = handlerAggregated ? handlerAggregated[field] : undefined;
|
|
596
|
+
if (handlerValue !== undefined)
|
|
597
|
+
recomputed[field] = handlerValue;
|
|
598
|
+
}
|
|
599
|
+
return recomputed;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Merge seeded media-buy-delivery entries into a `get_media_buy_delivery`
|
|
603
|
+
* response. Existing handler entries come first; seeded entries append after
|
|
604
|
+
* deduping by `media_buy_id`. On collision the SEEDED entry wins, matching the
|
|
605
|
+
* precedent set by `mergeSeededMediaBuys` / `mergeSeededCreatives` /
|
|
606
|
+
* `mergeSeededAccounts` — storyboards seed deliberately, so a seeded fixture
|
|
607
|
+
* for an existing `media_buy_id` is an explicit author override.
|
|
608
|
+
*
|
|
609
|
+
* After merging, `aggregated_totals` is recomputed via
|
|
610
|
+
* {@link recomputeAggregatedTotals} so `media_buy_count` / `impressions` /
|
|
611
|
+
* `spend` reflect the merged set instead of the handler's pre-merge values.
|
|
612
|
+
*
|
|
613
|
+
* Returns a NEW response object — the handler's singleton envelope fields
|
|
614
|
+
* (`reporting_period`, `currency`, `attribution_window`, `errors`, `sandbox`,
|
|
615
|
+
* `context`, `ext`, plus webhook-only `notification_type` / `partial_data` /
|
|
616
|
+
* `sequence_number` / etc.) pass through verbatim. Stamps `sandbox: true`
|
|
617
|
+
* unless the handler explicitly declared `sandbox: false`.
|
|
618
|
+
*/
|
|
619
|
+
function mergeSeededMediaBuyDeliveryIntoResponse(response, seeded) {
|
|
620
|
+
if (!seeded.length)
|
|
621
|
+
return response;
|
|
622
|
+
const handlerDeliveries = Array.isArray(response.media_buy_deliveries) ? response.media_buy_deliveries : [];
|
|
623
|
+
const seededIds = new Set();
|
|
624
|
+
for (const d of seeded)
|
|
625
|
+
seededIds.add(d.media_buy_id);
|
|
626
|
+
// Seeded wins on collision: drop handler entries whose `media_buy_id` is in
|
|
627
|
+
// the seeded set, then append seeded entries. Order mirrors
|
|
628
|
+
// `mergeSeededMediaBuysIntoResponse` — retained handler entries first,
|
|
629
|
+
// seeded entries (including overrides) last.
|
|
630
|
+
const retained = handlerDeliveries.filter(d => !seededIds.has(d?.media_buy_id));
|
|
631
|
+
const final = [...retained, ...seeded];
|
|
632
|
+
const merged = {
|
|
633
|
+
...response,
|
|
634
|
+
media_buy_deliveries: final,
|
|
635
|
+
aggregated_totals: recomputeAggregatedTotals(final, response.aggregated_totals),
|
|
636
|
+
};
|
|
637
|
+
if (response.sandbox !== false) {
|
|
638
|
+
merged.sandbox = true;
|
|
639
|
+
}
|
|
640
|
+
return merged;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Merge seeded accounts into a `list_accounts` response. Existing accounts
|
|
644
|
+
* come first; seeded entries append after deduping by `account_id`. On
|
|
645
|
+
* collision the seeded entry wins. Returns a NEW response object.
|
|
646
|
+
*
|
|
647
|
+
* `list_accounts` exposes its count via `pagination.total_count` (optional,
|
|
648
|
+
* same as `get_media_buys`). When the handler set it, it's incremented by
|
|
649
|
+
* the count of new (non-colliding) seeded entries.
|
|
650
|
+
*/
|
|
651
|
+
function mergeSeededAccountsIntoResponse(response, seeded) {
|
|
652
|
+
if (!seeded.length)
|
|
653
|
+
return response;
|
|
654
|
+
const seededIds = new Set();
|
|
655
|
+
for (const a of seeded)
|
|
656
|
+
seededIds.add(a.account_id);
|
|
657
|
+
const existing = Array.isArray(response.accounts) ? response.accounts : [];
|
|
658
|
+
const existingIds = new Set();
|
|
659
|
+
for (const a of existing) {
|
|
660
|
+
if (a && typeof a.account_id === 'string')
|
|
661
|
+
existingIds.add(a.account_id);
|
|
662
|
+
}
|
|
663
|
+
const retained = existing.filter(a => !seededIds.has(a?.account_id));
|
|
664
|
+
let newCount = 0;
|
|
665
|
+
for (const a of seeded)
|
|
666
|
+
if (!existingIds.has(a.account_id))
|
|
667
|
+
newCount += 1;
|
|
668
|
+
const merged = {
|
|
669
|
+
...response,
|
|
670
|
+
accounts: [...retained, ...seeded],
|
|
671
|
+
};
|
|
672
|
+
if (response.sandbox !== false) {
|
|
673
|
+
merged.sandbox = true;
|
|
674
|
+
}
|
|
675
|
+
const pagination = response.pagination;
|
|
676
|
+
if (pagination && typeof pagination === 'object' && typeof pagination.total_count === 'number') {
|
|
677
|
+
merged.pagination = {
|
|
678
|
+
...pagination,
|
|
679
|
+
total_count: pagination.total_count + newCount,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return merged;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Extract `account_id` from a `get_account_financials` request's `account`
|
|
686
|
+
* reference. `AccountReference` is a discriminated union — the
|
|
687
|
+
* operator-resolved variant carries `account_id` directly, brand+operator
|
|
688
|
+
* variants do not. Returns `undefined` for variants that don't carry an
|
|
689
|
+
* `account_id` (those requests can't match a seeded fixture by id and pass
|
|
690
|
+
* through to the handler).
|
|
691
|
+
*/
|
|
692
|
+
function readRequestAccountId(req) {
|
|
693
|
+
const account = req?.account;
|
|
694
|
+
if (!account || typeof account !== 'object' || Array.isArray(account))
|
|
695
|
+
return undefined;
|
|
696
|
+
const id = account.account_id;
|
|
697
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Pick a seeded `get_account_financials` fixture matching the request's
|
|
701
|
+
* account. Unlike the array-collection helpers, this returns the SINGLE
|
|
702
|
+
* matched envelope or `undefined` — `get_account_financials` is a singleton
|
|
703
|
+
* response and "merge" reduces to "replace if the request's account matches
|
|
704
|
+
* a seeded fixture".
|
|
705
|
+
*
|
|
706
|
+
* Matching honors both the raw request and the resolved account from
|
|
707
|
+
* `resolveAccount`. `AccountReference` is a discriminated union — the
|
|
708
|
+
* operator-resolved variant carries `account_id` on the request, but the
|
|
709
|
+
* brand+operator variants do not. When the framework has already resolved
|
|
710
|
+
* the request to an account, prefer that resolved id so seeded fixtures
|
|
711
|
+
* find their match regardless of which `AccountReference` variant the
|
|
712
|
+
* buyer sent.
|
|
713
|
+
*
|
|
714
|
+
* @param resolvedAccountId - The `account_id` from `ctx.account` after
|
|
715
|
+
* `resolveAccount` ran. Pass `undefined` when no account was resolved
|
|
716
|
+
* (singleton-tenant adopters) — the function falls back to the request's
|
|
717
|
+
* `account.account_id` field, which preserves the original semantics for
|
|
718
|
+
* adopters who don't wire `resolveAccount`.
|
|
719
|
+
*/
|
|
720
|
+
function pickSeededAccountFinancialsForRequest(request, seeded, resolvedAccountId) {
|
|
721
|
+
if (!seeded.length)
|
|
722
|
+
return undefined;
|
|
723
|
+
const requestedId = resolvedAccountId ?? readRequestAccountId(request);
|
|
724
|
+
if (requestedId == null)
|
|
725
|
+
return undefined;
|
|
726
|
+
for (const entry of seeded) {
|
|
727
|
+
const id = entry.account?.account_id;
|
|
728
|
+
if (typeof id === 'string' && id === requestedId)
|
|
729
|
+
return entry;
|
|
730
|
+
}
|
|
731
|
+
return undefined;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Replace a `get_account_financials` response when a seeded fixture matches
|
|
735
|
+
* the request's account. The seeded fixture is authoritative on the
|
|
736
|
+
* financials payload (spend, period, account, currency, ...). The handler's
|
|
737
|
+
* `context` and `ext` are framework-managed (`context` echoes
|
|
738
|
+
* `adcp_version` / `request_id`; `ext` carries adopter passthrough) and
|
|
739
|
+
* MUST be preserved across the replace — wire-correct context echo is the
|
|
740
|
+
* framework's responsibility, not the seed fixture's.
|
|
741
|
+
*
|
|
742
|
+
* When no fixture matches, returns the handler response unchanged.
|
|
743
|
+
*
|
|
744
|
+
* @param resolvedAccountId - Optional resolved `account_id` from
|
|
745
|
+
* `ctx.account`; passed through to {@link pickSeededAccountFinancialsForRequest}.
|
|
746
|
+
*/
|
|
747
|
+
function replaceAccountFinancialsIfSeeded(request, response, seeded, resolvedAccountId) {
|
|
748
|
+
const picked = pickSeededAccountFinancialsForRequest(request, seeded, resolvedAccountId);
|
|
749
|
+
if (!picked)
|
|
750
|
+
return response;
|
|
751
|
+
// Preserve framework-managed envelope fields from the handler response;
|
|
752
|
+
// seeded fixture owns the financials body. Pull `context` / `ext` off the
|
|
753
|
+
// handler response (when present) and re-stamp them on top of the seeded
|
|
754
|
+
// payload — the fixture's own `context` / `ext` (if any) lose to the
|
|
755
|
+
// handler's, which is correct: a seeded snapshot can't know the current
|
|
756
|
+
// request's `request_id` / `adcp_version` echo.
|
|
757
|
+
const handlerContext = response.context;
|
|
758
|
+
const handlerExt = response.ext;
|
|
759
|
+
const merged = { ...picked };
|
|
760
|
+
if (handlerContext !== undefined)
|
|
761
|
+
merged.context = handlerContext;
|
|
762
|
+
if (handlerExt !== undefined)
|
|
763
|
+
merged.ext = handlerExt;
|
|
764
|
+
return merged;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Canonical dedup key for a `Format` — `${agent_url}|${id}`. Matches the AdCP
|
|
768
|
+
* format-id contract: two formats from the same agent with the same id are
|
|
769
|
+
* the same format, regardless of parameterized dimension / duration. Fixtures
|
|
770
|
+
* that seed multiple parameterized variants of the same template should use
|
|
771
|
+
* distinct `id` values per variant.
|
|
772
|
+
*/
|
|
773
|
+
function formatDedupKey(f) {
|
|
774
|
+
const fid = f.format_id;
|
|
775
|
+
if (!fid || typeof fid !== 'object')
|
|
776
|
+
return undefined;
|
|
777
|
+
const agentUrl = fid.agent_url;
|
|
778
|
+
const id = fid.id;
|
|
779
|
+
if (typeof agentUrl !== 'string' || typeof id !== 'string')
|
|
780
|
+
return undefined;
|
|
781
|
+
return `${agentUrl}|${id}`;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Merge seeded creative formats into a `list_creative_formats` response.
|
|
785
|
+
* Existing formats come first; seeded entries append after deduping by
|
|
786
|
+
* canonical `${agent_url}|${id}`. On collision the seeded entry wins.
|
|
787
|
+
* Returns a NEW response object.
|
|
788
|
+
*/
|
|
789
|
+
function mergeSeededCreativeFormatsIntoResponse(response, seeded) {
|
|
790
|
+
if (!seeded.length)
|
|
791
|
+
return response;
|
|
792
|
+
const seededKeys = new Set();
|
|
793
|
+
for (const f of seeded) {
|
|
794
|
+
const key = formatDedupKey(f);
|
|
795
|
+
if (key != null)
|
|
796
|
+
seededKeys.add(key);
|
|
797
|
+
}
|
|
798
|
+
const existing = Array.isArray(response.formats) ? response.formats : [];
|
|
799
|
+
const retained = existing.filter(f => {
|
|
800
|
+
const key = formatDedupKey(f);
|
|
801
|
+
return key == null || !seededKeys.has(key);
|
|
802
|
+
});
|
|
803
|
+
const merged = {
|
|
804
|
+
...response,
|
|
805
|
+
formats: [...retained, ...seeded],
|
|
806
|
+
};
|
|
807
|
+
if (response.sandbox !== false) {
|
|
808
|
+
merged.sandbox = true;
|
|
809
|
+
}
|
|
810
|
+
return merged;
|
|
811
|
+
}
|
|
812
|
+
// ---------------------------------------------------------------------------
|
|
813
|
+
// Property lists / collection lists / content standards
|
|
814
|
+
//
|
|
815
|
+
// Each entity has a "list" tool (returns `T[]` under a top-level key) AND a
|
|
816
|
+
// "get" tool (returns a singleton). The same seeded fixture array feeds both
|
|
817
|
+
// tools — the list tool merges with seeded-wins on collision; the get tool
|
|
818
|
+
// picks by id and replaces. PropertyList and CollectionList wrap the picked
|
|
819
|
+
// entity into the response's `list` field; ContentStandards' success arm IS
|
|
820
|
+
// the entity directly.
|
|
821
|
+
//
|
|
822
|
+
// All three follow the same dedup contract:
|
|
823
|
+
// PropertyList → top-level `list_id` (string, required)
|
|
824
|
+
// CollectionList → top-level `list_id` (string, required)
|
|
825
|
+
// ContentStandards → top-level `standards_id` (string, required)
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
/**
|
|
828
|
+
* Validate seeded property-list entries. Drops entries missing a non-empty
|
|
829
|
+
* string `list_id`.
|
|
830
|
+
*/
|
|
831
|
+
function filterValidSeededPropertyLists(raw, logger) {
|
|
832
|
+
return filterValidById(raw, 'list_id', 'getSeededPropertyLists', logger);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Validate seeded collection-list entries. Drops entries missing a non-empty
|
|
836
|
+
* string `list_id`.
|
|
837
|
+
*/
|
|
838
|
+
function filterValidSeededCollectionLists(raw, logger) {
|
|
839
|
+
return filterValidById(raw, 'list_id', 'getSeededCollectionLists', logger);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Validate seeded content-standards entries. Drops entries missing a
|
|
843
|
+
* non-empty string `standards_id`.
|
|
844
|
+
*/
|
|
845
|
+
function filterValidSeededContentStandards(raw, logger) {
|
|
846
|
+
return filterValidById(raw, 'standards_id', 'getSeededContentStandards', logger);
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Merge seeded property lists into a `list_property_lists` response.
|
|
850
|
+
* Existing entries come first; seeded entries append after deduping by
|
|
851
|
+
* `list_id`. On collision the seeded entry wins. Returns a NEW response
|
|
852
|
+
* object. When the handler set `pagination.total_count`, it's incremented
|
|
853
|
+
* by the count of new (non-colliding) seeded entries.
|
|
854
|
+
*
|
|
855
|
+
* `list_property_lists` does not carry a `query_summary` block (per AdCP
|
|
856
|
+
* 3.0.11) — pagination.total_count is the only count field to update.
|
|
857
|
+
*/
|
|
858
|
+
function mergeSeededPropertyListsIntoResponse(response, seeded) {
|
|
859
|
+
if (!seeded.length)
|
|
860
|
+
return response;
|
|
861
|
+
const seededIds = new Set();
|
|
862
|
+
for (const l of seeded)
|
|
863
|
+
seededIds.add(l.list_id);
|
|
864
|
+
const existing = Array.isArray(response.lists) ? response.lists : [];
|
|
865
|
+
const existingIds = new Set();
|
|
866
|
+
for (const l of existing) {
|
|
867
|
+
if (l && typeof l.list_id === 'string')
|
|
868
|
+
existingIds.add(l.list_id);
|
|
869
|
+
}
|
|
870
|
+
const retained = existing.filter(l => !seededIds.has(l?.list_id));
|
|
871
|
+
let newCount = 0;
|
|
872
|
+
for (const l of seeded)
|
|
873
|
+
if (!existingIds.has(l.list_id))
|
|
874
|
+
newCount += 1;
|
|
875
|
+
const merged = {
|
|
876
|
+
...response,
|
|
877
|
+
lists: [...retained, ...seeded],
|
|
878
|
+
};
|
|
879
|
+
const pagination = response.pagination;
|
|
880
|
+
if (pagination && typeof pagination === 'object' && typeof pagination.total_count === 'number') {
|
|
881
|
+
merged.pagination = {
|
|
882
|
+
...pagination,
|
|
883
|
+
total_count: pagination.total_count + newCount,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
return merged;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Merge seeded collection lists into a `list_collection_lists` response.
|
|
890
|
+
* Symmetric with {@link mergeSeededPropertyListsIntoResponse} — same dedup
|
|
891
|
+
* key (`list_id`), same pagination update policy.
|
|
892
|
+
*/
|
|
893
|
+
function mergeSeededCollectionListsIntoResponse(response, seeded) {
|
|
894
|
+
if (!seeded.length)
|
|
895
|
+
return response;
|
|
896
|
+
const seededIds = new Set();
|
|
897
|
+
for (const l of seeded)
|
|
898
|
+
seededIds.add(l.list_id);
|
|
899
|
+
const existing = Array.isArray(response.lists) ? response.lists : [];
|
|
900
|
+
const existingIds = new Set();
|
|
901
|
+
for (const l of existing) {
|
|
902
|
+
if (l && typeof l.list_id === 'string')
|
|
903
|
+
existingIds.add(l.list_id);
|
|
904
|
+
}
|
|
905
|
+
const retained = existing.filter(l => !seededIds.has(l?.list_id));
|
|
906
|
+
let newCount = 0;
|
|
907
|
+
for (const l of seeded)
|
|
908
|
+
if (!existingIds.has(l.list_id))
|
|
909
|
+
newCount += 1;
|
|
910
|
+
const merged = {
|
|
911
|
+
...response,
|
|
912
|
+
lists: [...retained, ...seeded],
|
|
913
|
+
};
|
|
914
|
+
const pagination = response.pagination;
|
|
915
|
+
if (pagination && typeof pagination === 'object' && typeof pagination.total_count === 'number') {
|
|
916
|
+
merged.pagination = {
|
|
917
|
+
...pagination,
|
|
918
|
+
total_count: pagination.total_count + newCount,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return merged;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Merge seeded content-standards into a `list_content_standards` response
|
|
925
|
+
* (success arm). Drops to a no-op if the response is the error arm (no
|
|
926
|
+
* `standards` array). On `standards_id` collision the seeded entry wins.
|
|
927
|
+
* Updates `pagination.total_count` when the handler set it. Returns a NEW
|
|
928
|
+
* response object.
|
|
929
|
+
*/
|
|
930
|
+
function mergeSeededContentStandardsIntoResponse(response, seeded) {
|
|
931
|
+
if (!seeded.length)
|
|
932
|
+
return response;
|
|
933
|
+
// Skip the error arm — the dispatcher already gates on !isErrorResponse,
|
|
934
|
+
// but the type is a union so we re-narrow defensively.
|
|
935
|
+
const successArm = response;
|
|
936
|
+
if (!Array.isArray(successArm.standards))
|
|
937
|
+
return response;
|
|
938
|
+
const seededIds = new Set();
|
|
939
|
+
for (const s of seeded)
|
|
940
|
+
seededIds.add(s.standards_id);
|
|
941
|
+
const existing = successArm.standards;
|
|
942
|
+
const existingIds = new Set();
|
|
943
|
+
for (const s of existing) {
|
|
944
|
+
if (s && typeof s.standards_id === 'string')
|
|
945
|
+
existingIds.add(s.standards_id);
|
|
946
|
+
}
|
|
947
|
+
const retained = existing.filter(s => !seededIds.has(s?.standards_id));
|
|
948
|
+
let newCount = 0;
|
|
949
|
+
for (const s of seeded)
|
|
950
|
+
if (!existingIds.has(s.standards_id))
|
|
951
|
+
newCount += 1;
|
|
952
|
+
const merged = {
|
|
953
|
+
...successArm,
|
|
954
|
+
standards: [...retained, ...seeded],
|
|
955
|
+
};
|
|
956
|
+
const pagination = successArm.pagination;
|
|
957
|
+
if (pagination && typeof pagination === 'object' && typeof pagination.total_count === 'number') {
|
|
958
|
+
merged.pagination = {
|
|
959
|
+
...pagination,
|
|
960
|
+
total_count: pagination.total_count + newCount,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
return merged;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Read the `list_id` from a `get_property_list` request. Required field per
|
|
967
|
+
* spec; this helper is defensive about runtime input.
|
|
968
|
+
*/
|
|
969
|
+
function readRequestListId(req) {
|
|
970
|
+
const id = req?.list_id;
|
|
971
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Read the `standards_id` from a `get_content_standards` request.
|
|
975
|
+
*/
|
|
976
|
+
function readRequestStandardsId(req) {
|
|
977
|
+
const id = req?.standards_id;
|
|
978
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Pick the seeded property list whose `list_id` matches the request.
|
|
982
|
+
* Returns `undefined` when nothing matches.
|
|
983
|
+
*/
|
|
984
|
+
function pickSeededPropertyListForRequest(request, seeded) {
|
|
985
|
+
if (!seeded.length)
|
|
986
|
+
return undefined;
|
|
987
|
+
const requestedId = readRequestListId(request);
|
|
988
|
+
if (requestedId == null)
|
|
989
|
+
return undefined;
|
|
990
|
+
for (const entry of seeded) {
|
|
991
|
+
if (entry.list_id === requestedId)
|
|
992
|
+
return entry;
|
|
993
|
+
}
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Replace a `get_property_list` response's `list` field with a seeded fixture
|
|
998
|
+
* when one matches. The handler's auxiliary fields (`identifiers`,
|
|
999
|
+
* `pagination`, `resolved_at`, `cache_valid_until`, `coverage_gaps`,
|
|
1000
|
+
* `context`, `ext`) pass through verbatim — those depend on request-time
|
|
1001
|
+
* pagination / resolve params that a static fixture can't know. Only `list`
|
|
1002
|
+
* is replaced. When no fixture matches, returns the handler response
|
|
1003
|
+
* unchanged.
|
|
1004
|
+
*/
|
|
1005
|
+
function replacePropertyListIfSeeded(request, response, seeded) {
|
|
1006
|
+
const picked = pickSeededPropertyListForRequest(request, seeded);
|
|
1007
|
+
if (!picked)
|
|
1008
|
+
return response;
|
|
1009
|
+
return { ...response, list: picked };
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Pick the seeded collection list whose `list_id` matches the request.
|
|
1013
|
+
* Returns `undefined` when nothing matches.
|
|
1014
|
+
*/
|
|
1015
|
+
function pickSeededCollectionListForRequest(request, seeded) {
|
|
1016
|
+
if (!seeded.length)
|
|
1017
|
+
return undefined;
|
|
1018
|
+
const requestedId = readRequestListId(request);
|
|
1019
|
+
if (requestedId == null)
|
|
1020
|
+
return undefined;
|
|
1021
|
+
for (const entry of seeded) {
|
|
1022
|
+
if (entry.list_id === requestedId)
|
|
1023
|
+
return entry;
|
|
1024
|
+
}
|
|
1025
|
+
return undefined;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Replace a `get_collection_list` response's `list` field with a seeded
|
|
1029
|
+
* fixture when one matches. Same envelope-preservation policy as
|
|
1030
|
+
* {@link replacePropertyListIfSeeded}.
|
|
1031
|
+
*/
|
|
1032
|
+
function replaceCollectionListIfSeeded(request, response, seeded) {
|
|
1033
|
+
const picked = pickSeededCollectionListForRequest(request, seeded);
|
|
1034
|
+
if (!picked)
|
|
1035
|
+
return response;
|
|
1036
|
+
return { ...response, list: picked };
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Pick the seeded content-standards entry whose `standards_id` matches the
|
|
1040
|
+
* request. Returns `undefined` when nothing matches.
|
|
1041
|
+
*/
|
|
1042
|
+
function pickSeededContentStandardsForRequest(request, seeded) {
|
|
1043
|
+
if (!seeded.length)
|
|
1044
|
+
return undefined;
|
|
1045
|
+
const requestedId = readRequestStandardsId(request);
|
|
1046
|
+
if (requestedId == null)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
for (const entry of seeded) {
|
|
1049
|
+
if (entry.standards_id === requestedId)
|
|
1050
|
+
return entry;
|
|
1051
|
+
}
|
|
1052
|
+
return undefined;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Replace a `get_content_standards` response with a seeded fixture when one
|
|
1056
|
+
* matches. Seeded fixture is authoritative on the `ContentStandards` body.
|
|
1057
|
+
* Framework-managed envelope fields (`context`, `ext`) round-trip from the
|
|
1058
|
+
* handler — matches the precedent set by {@link replaceAccountFinancialsIfSeeded}.
|
|
1059
|
+
*
|
|
1060
|
+
* When no fixture matches, returns the handler response unchanged. The
|
|
1061
|
+
* caller is responsible for skipping the error arm (the dispatcher gates on
|
|
1062
|
+
* `!isErrorResponse`).
|
|
1063
|
+
*/
|
|
1064
|
+
function replaceContentStandardsIfSeeded(request, response, seeded) {
|
|
1065
|
+
const picked = pickSeededContentStandardsForRequest(request, seeded);
|
|
1066
|
+
if (!picked)
|
|
1067
|
+
return response;
|
|
1068
|
+
// Both context and ext are framework-managed envelope fields.
|
|
1069
|
+
// Seeded fixture is authoritative on the ContentStandards body only.
|
|
1070
|
+
const handlerContext = response.context;
|
|
1071
|
+
const handlerExt = response.ext;
|
|
1072
|
+
const replaced = { ...picked };
|
|
1073
|
+
if (handlerContext !== undefined)
|
|
1074
|
+
replaced.context = handlerContext;
|
|
1075
|
+
if (handlerExt !== undefined)
|
|
1076
|
+
replaced.ext = handlerExt;
|
|
1077
|
+
return replaced;
|
|
1078
|
+
}
|
|
1079
|
+
// ---------------------------------------------------------------------------
|
|
1080
|
+
// Signals / creative delivery / creative features
|
|
1081
|
+
//
|
|
1082
|
+
// `get_signals` (signal-marketplace + signal-owned) → list-merge by signal_id
|
|
1083
|
+
// `get_creative_delivery` (creative-* delivery) → list-merge by creative_id, pagination.total update
|
|
1084
|
+
// `get_creative_features` (creative-* governance) → list-merge into success-arm `results` by feature_id
|
|
1085
|
+
//
|
|
1086
|
+
// All three follow the validate-and-drop / dedupe-and-stamp-sandbox
|
|
1087
|
+
// precedent set by the earlier bridges. The features bridge gates on the
|
|
1088
|
+
// success arm — error envelopes pass through unchanged.
|
|
1089
|
+
// ---------------------------------------------------------------------------
|
|
1090
|
+
/**
|
|
1091
|
+
* Canonical dedup key for a `SignalID`. `SignalID` is a discriminated union
|
|
1092
|
+
* (`{source:'catalog', data_provider_domain, id}` vs `{source:'agent',
|
|
1093
|
+
* agent_url, id}`) — two signals with the same `id` from different sources
|
|
1094
|
+
* are distinct, so dedup keys on the full source+origin+id tuple.
|
|
1095
|
+
*/
|
|
1096
|
+
function signalIdDedupKey(signal) {
|
|
1097
|
+
const sid = signal.signal_id;
|
|
1098
|
+
if (!sid || typeof sid !== 'object' || Array.isArray(sid))
|
|
1099
|
+
return undefined;
|
|
1100
|
+
const source = sid.source;
|
|
1101
|
+
const id = sid.id;
|
|
1102
|
+
if (typeof source !== 'string' || typeof id !== 'string' || id.length === 0)
|
|
1103
|
+
return undefined;
|
|
1104
|
+
if (source === 'catalog') {
|
|
1105
|
+
const origin = sid.data_provider_domain;
|
|
1106
|
+
if (typeof origin !== 'string' || origin.length === 0)
|
|
1107
|
+
return undefined;
|
|
1108
|
+
return `catalog|${origin}|${id}`;
|
|
1109
|
+
}
|
|
1110
|
+
if (source === 'agent') {
|
|
1111
|
+
const origin = sid.agent_url;
|
|
1112
|
+
if (typeof origin !== 'string' || origin.length === 0)
|
|
1113
|
+
return undefined;
|
|
1114
|
+
return `agent|${origin}|${id}`;
|
|
1115
|
+
}
|
|
1116
|
+
return undefined;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Validate seeded signals. Drops entries whose `signal_id` is not a valid
|
|
1120
|
+
* `SignalID` discriminated-union shape (`{source:'catalog',
|
|
1121
|
+
* data_provider_domain, id}` or `{source:'agent', agent_url, id}`). A missing
|
|
1122
|
+
* or malformed `signal_id` collides on `undefined === undefined` when
|
|
1123
|
+
* deduping, so we drop early.
|
|
1124
|
+
*/
|
|
1125
|
+
function filterValidSeededSignals(raw, logger) {
|
|
1126
|
+
if (!Array.isArray(raw)) {
|
|
1127
|
+
logger?.warn('testController.getSeededSignals did not return an array; skipping bridge', {
|
|
1128
|
+
received: typeof raw,
|
|
1129
|
+
});
|
|
1130
|
+
return [];
|
|
1131
|
+
}
|
|
1132
|
+
const valid = [];
|
|
1133
|
+
raw.forEach((entry, index) => {
|
|
1134
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1135
|
+
logger?.warn('testController.getSeededSignals entry is not an object; dropping', { index });
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const key = signalIdDedupKey(entry);
|
|
1139
|
+
if (!key) {
|
|
1140
|
+
logger?.warn('testController.getSeededSignals entry has invalid signal_id (expected {source:catalog,data_provider_domain,id} or {source:agent,agent_url,id}); dropping', { index });
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
valid.push(entry);
|
|
1144
|
+
});
|
|
1145
|
+
return valid;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Validate seeded creative-delivery entries. Drops entries missing a non-empty
|
|
1149
|
+
* string `creative_id`.
|
|
1150
|
+
*/
|
|
1151
|
+
function filterValidSeededCreativeDelivery(raw, logger) {
|
|
1152
|
+
return filterValidById(raw, 'creative_id', 'getSeededCreativeDelivery', logger);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Validate seeded creative-feature results. Drops entries missing a non-empty
|
|
1156
|
+
* string `feature_id` (the dedup key against the handler's `results` array).
|
|
1157
|
+
*/
|
|
1158
|
+
function filterValidSeededCreativeFeatures(raw, logger) {
|
|
1159
|
+
return filterValidById(raw, 'feature_id', 'getSeededCreativeFeatures', logger);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Merge seeded signals into a `get_signals` response. Existing handler signals
|
|
1163
|
+
* come first; seeded entries append after deduping by `signal_id`. On
|
|
1164
|
+
* collision the seeded entry wins. Stamps `sandbox: true` unless the handler
|
|
1165
|
+
* explicitly declared `sandbox: false`.
|
|
1166
|
+
*
|
|
1167
|
+
* `get_signals` carries `pagination: PaginationResponse` and no `query_summary`.
|
|
1168
|
+
* Pagination is not recomputed on the merge. `PaginationResponse.total_count`
|
|
1169
|
+
* is optional; recomputing it on partial pages would mis-represent the
|
|
1170
|
+
* cross-page total (the handler may have served page 1 of N, and the seeded
|
|
1171
|
+
* fixture sits outside that pagination context). Storyboards asserting on
|
|
1172
|
+
* totals should seed the handler's response, not the post-merge envelope.
|
|
1173
|
+
*/
|
|
1174
|
+
function mergeSeededSignalsIntoResponse(response, seeded) {
|
|
1175
|
+
if (!seeded.length)
|
|
1176
|
+
return response;
|
|
1177
|
+
const seededKeys = new Set();
|
|
1178
|
+
for (const s of seeded) {
|
|
1179
|
+
const key = signalIdDedupKey(s);
|
|
1180
|
+
if (key)
|
|
1181
|
+
seededKeys.add(key);
|
|
1182
|
+
}
|
|
1183
|
+
const existing = Array.isArray(response.signals) ? response.signals : [];
|
|
1184
|
+
const retained = existing.filter(s => {
|
|
1185
|
+
const key = s ? signalIdDedupKey(s) : undefined;
|
|
1186
|
+
return key == null || !seededKeys.has(key);
|
|
1187
|
+
});
|
|
1188
|
+
const merged = {
|
|
1189
|
+
...response,
|
|
1190
|
+
signals: [...retained, ...seeded],
|
|
1191
|
+
};
|
|
1192
|
+
if (response.sandbox !== false) {
|
|
1193
|
+
merged.sandbox = true;
|
|
1194
|
+
}
|
|
1195
|
+
return merged;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Merge seeded creative-delivery entries into a `get_creative_delivery`
|
|
1199
|
+
* response. Existing handler creatives come first; seeded entries append after
|
|
1200
|
+
* deduping by `creative_id`. On collision the seeded entry wins (matches the
|
|
1201
|
+
* precedent set by `mergeSeededMediaBuyDelivery` — storyboards seed
|
|
1202
|
+
* deliberately, so a seeded fixture for an existing id is an explicit author
|
|
1203
|
+
* override).
|
|
1204
|
+
*
|
|
1205
|
+
* `pagination.total` (optional per the AdCP 3.0.11 schema) is incremented by
|
|
1206
|
+
* the count of new non-colliding seeded entries. There is no top-level
|
|
1207
|
+
* aggregated-totals envelope on this response (unlike `get_media_buy_delivery`),
|
|
1208
|
+
* so no further recomputation is performed. Stamps `sandbox: true` unless the
|
|
1209
|
+
* handler explicitly declared `sandbox: false`. Returns a NEW response object.
|
|
1210
|
+
*/
|
|
1211
|
+
function mergeSeededCreativeDeliveryIntoResponse(response, seeded) {
|
|
1212
|
+
if (!seeded.length)
|
|
1213
|
+
return response;
|
|
1214
|
+
const seededIds = new Set();
|
|
1215
|
+
for (const c of seeded)
|
|
1216
|
+
seededIds.add(c.creative_id);
|
|
1217
|
+
const existing = Array.isArray(response.creatives) ? response.creatives : [];
|
|
1218
|
+
const existingIds = new Set();
|
|
1219
|
+
for (const c of existing) {
|
|
1220
|
+
if (c && typeof c.creative_id === 'string')
|
|
1221
|
+
existingIds.add(c.creative_id);
|
|
1222
|
+
}
|
|
1223
|
+
const retained = existing.filter(c => !seededIds.has(c?.creative_id));
|
|
1224
|
+
let newCount = 0;
|
|
1225
|
+
for (const c of seeded)
|
|
1226
|
+
if (!existingIds.has(c.creative_id))
|
|
1227
|
+
newCount += 1;
|
|
1228
|
+
const merged = {
|
|
1229
|
+
...response,
|
|
1230
|
+
creatives: [...retained, ...seeded],
|
|
1231
|
+
};
|
|
1232
|
+
if (response.sandbox !== false) {
|
|
1233
|
+
merged.sandbox = true;
|
|
1234
|
+
}
|
|
1235
|
+
// `pagination.total` is the schema-correct field name on
|
|
1236
|
+
// GetCreativeDeliveryResponse (distinct from `pagination.total_count` used
|
|
1237
|
+
// by list_creatives / get_media_buys / list_accounts).
|
|
1238
|
+
const pagination = response.pagination;
|
|
1239
|
+
if (pagination && typeof pagination === 'object' && typeof pagination.total === 'number') {
|
|
1240
|
+
merged.pagination = {
|
|
1241
|
+
...pagination,
|
|
1242
|
+
total: pagination.total + newCount,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return merged;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Merge seeded creative-feature results into a `get_creative_features`
|
|
1249
|
+
* response. The response is a `oneOf` envelope — success arm carries
|
|
1250
|
+
* `results: CreativeFeatureResult[]`, error arm carries `errors: Error[]`.
|
|
1251
|
+
* When the handler returned the error arm, this helper is a no-op; the error
|
|
1252
|
+
* envelope passes through unchanged. When the handler returned the success
|
|
1253
|
+
* arm, seeded results merge into the `results` array (dedup by `feature_id`,
|
|
1254
|
+
* seeded wins on collision).
|
|
1255
|
+
*
|
|
1256
|
+
* Framework-managed envelope fields (`context`, `ext`, `detail_url`,
|
|
1257
|
+
* `pricing_option_id`, `vendor_cost`, `currency`, `consumption`) round-trip
|
|
1258
|
+
* from the handler verbatim — the bridge only augments the per-feature
|
|
1259
|
+
* results array.
|
|
1260
|
+
*
|
|
1261
|
+
* Returns a NEW response object.
|
|
1262
|
+
*/
|
|
1263
|
+
function mergeSeededCreativeFeaturesIntoResponse(response, seeded) {
|
|
1264
|
+
if (!seeded.length)
|
|
1265
|
+
return response;
|
|
1266
|
+
// Discriminate the success vs error arms. The error arm carries `errors`
|
|
1267
|
+
// and no `results`; the success arm carries `results` (required per spec).
|
|
1268
|
+
const successArm = response;
|
|
1269
|
+
if (!Array.isArray(successArm.results))
|
|
1270
|
+
return response;
|
|
1271
|
+
const seededIds = new Set();
|
|
1272
|
+
for (const r of seeded)
|
|
1273
|
+
seededIds.add(r.feature_id);
|
|
1274
|
+
const existing = successArm.results;
|
|
1275
|
+
const retained = existing.filter(r => !seededIds.has(r?.feature_id));
|
|
1276
|
+
const merged = {
|
|
1277
|
+
...successArm,
|
|
1278
|
+
results: [...retained, ...seeded],
|
|
1279
|
+
};
|
|
1280
|
+
return merged;
|
|
1281
|
+
}
|
|
1282
|
+
// ---------------------------------------------------------------------------
|
|
1283
|
+
// Brand rights / sponsored intelligence
|
|
1284
|
+
//
|
|
1285
|
+
// `get_brand_identity` and `si_get_offering` are stateless singleton lookups
|
|
1286
|
+
// keyed by a top-level request id; the bridge picks the matching seeded
|
|
1287
|
+
// fixture and REPLACES the handler response body, preserving framework-managed
|
|
1288
|
+
// `context` / `ext`. `get_rights` is a discovery / search tool with an array
|
|
1289
|
+
// response; the bridge appends seeded entries with dedup by `rights_id`,
|
|
1290
|
+
// matching the `getSeededProducts` precedent.
|
|
1291
|
+
//
|
|
1292
|
+
// Mutating siblings (`acquire_rights`, `update_rights`, `si_initiate_session`,
|
|
1293
|
+
// `si_send_message`, `si_terminate_session`) are intentionally NOT bridged —
|
|
1294
|
+
// seeded read paths only.
|
|
1295
|
+
// ---------------------------------------------------------------------------
|
|
1296
|
+
/**
|
|
1297
|
+
* Validate seeded brand-identity entries. Drops entries missing a non-empty
|
|
1298
|
+
* string `brand_id`, and warn-drops duplicates (first occurrence wins) since
|
|
1299
|
+
* a fixture array with two entries for the same `brand_id` is almost always
|
|
1300
|
+
* a seed-store bug.
|
|
1301
|
+
*/
|
|
1302
|
+
function filterValidSeededBrandIdentity(raw, logger) {
|
|
1303
|
+
if (!Array.isArray(raw)) {
|
|
1304
|
+
logger?.warn('testController.getSeededBrandIdentity did not return an array; skipping bridge', {
|
|
1305
|
+
received: typeof raw,
|
|
1306
|
+
});
|
|
1307
|
+
return [];
|
|
1308
|
+
}
|
|
1309
|
+
const valid = [];
|
|
1310
|
+
const seen = new Set();
|
|
1311
|
+
raw.forEach((entry, index) => {
|
|
1312
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1313
|
+
logger?.warn('testController.getSeededBrandIdentity entry is not an object; dropping', { index });
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const brandId = entry.brand_id;
|
|
1317
|
+
if (typeof brandId !== 'string' || brandId.length === 0) {
|
|
1318
|
+
logger?.warn('testController.getSeededBrandIdentity entry missing brand_id; dropping', { index });
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (seen.has(brandId)) {
|
|
1322
|
+
logger?.warn('testController.getSeededBrandIdentity duplicate brand_id; dropping (first occurrence wins)', {
|
|
1323
|
+
index,
|
|
1324
|
+
brand_id: brandId,
|
|
1325
|
+
});
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
seen.add(brandId);
|
|
1329
|
+
valid.push(entry);
|
|
1330
|
+
});
|
|
1331
|
+
return valid;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Validate seeded rights entries. Drops entries missing a non-empty string
|
|
1335
|
+
* `rights_id` (the dedup key against the handler set).
|
|
1336
|
+
*/
|
|
1337
|
+
function filterValidSeededRights(raw, logger) {
|
|
1338
|
+
return filterValidById(raw, 'rights_id', 'getSeededRights', logger);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Validate seeded SI offering entries. Each entry must carry a non-empty
|
|
1342
|
+
* string at `offering.offering_id` — that's the field the bridge matches
|
|
1343
|
+
* against `request.offering_id`. Warn-drops duplicates by that key.
|
|
1344
|
+
*
|
|
1345
|
+
* An entry without `offering` (e.g. an `available: false` fixture from a
|
|
1346
|
+
* different storyboard) can't be matched by the bridge and is dropped — a
|
|
1347
|
+
* seeded fixture that can't address a request would be a dead write.
|
|
1348
|
+
*/
|
|
1349
|
+
function filterValidSeededSiOffering(raw, logger) {
|
|
1350
|
+
if (!Array.isArray(raw)) {
|
|
1351
|
+
logger?.warn('testController.getSeededSiOffering did not return an array; skipping bridge', {
|
|
1352
|
+
received: typeof raw,
|
|
1353
|
+
});
|
|
1354
|
+
return [];
|
|
1355
|
+
}
|
|
1356
|
+
const valid = [];
|
|
1357
|
+
const seen = new Set();
|
|
1358
|
+
raw.forEach((entry, index) => {
|
|
1359
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1360
|
+
logger?.warn('testController.getSeededSiOffering entry is not an object; dropping', { index });
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const offering = entry.offering;
|
|
1364
|
+
const offeringId = offering && typeof offering === 'object' && !Array.isArray(offering)
|
|
1365
|
+
? offering.offering_id
|
|
1366
|
+
: undefined;
|
|
1367
|
+
if (typeof offeringId !== 'string' || offeringId.length === 0) {
|
|
1368
|
+
logger?.warn('testController.getSeededSiOffering entry missing offering.offering_id; dropping', { index });
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (seen.has(offeringId)) {
|
|
1372
|
+
logger?.warn('testController.getSeededSiOffering duplicate offering.offering_id; dropping (first occurrence wins)', { index, offering_id: offeringId });
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
seen.add(offeringId);
|
|
1376
|
+
valid.push(entry);
|
|
1377
|
+
});
|
|
1378
|
+
return valid;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Merge seeded rights entries into a `get_rights` response (success arm).
|
|
1382
|
+
* Existing entries come first; seeded entries append after deduping by
|
|
1383
|
+
* `rights_id`. On collision the seeded entry wins. Returns a NEW response
|
|
1384
|
+
* object. `get_rights` does not carry `pagination` / `query_summary` blocks
|
|
1385
|
+
* per AdCP 3.0.11 — no count fields to update.
|
|
1386
|
+
*
|
|
1387
|
+
* Drops to a no-op if the response is the error arm (no `rights` array). The
|
|
1388
|
+
* dispatcher gates on `!isErrorResponse` upstream; we re-narrow defensively.
|
|
1389
|
+
*/
|
|
1390
|
+
function mergeSeededRightsIntoResponse(response, seeded) {
|
|
1391
|
+
if (!seeded.length)
|
|
1392
|
+
return response;
|
|
1393
|
+
const successArm = response;
|
|
1394
|
+
if (!Array.isArray(successArm.rights))
|
|
1395
|
+
return response;
|
|
1396
|
+
const seededIds = new Set();
|
|
1397
|
+
for (const r of seeded)
|
|
1398
|
+
seededIds.add(r.rights_id);
|
|
1399
|
+
const existing = successArm.rights;
|
|
1400
|
+
const retained = existing.filter(r => !seededIds.has(r?.rights_id));
|
|
1401
|
+
return { ...response, rights: [...retained, ...seeded] };
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Read the `brand_id` from a `get_brand_identity` request. Required field per
|
|
1405
|
+
* spec; defensive about runtime input.
|
|
1406
|
+
*/
|
|
1407
|
+
function readRequestBrandId(req) {
|
|
1408
|
+
const id = req?.brand_id;
|
|
1409
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Pick the seeded brand-identity entry whose `brand_id` matches the request.
|
|
1413
|
+
* Returns `undefined` when nothing matches.
|
|
1414
|
+
*/
|
|
1415
|
+
function pickSeededBrandIdentityForRequest(request, seeded) {
|
|
1416
|
+
if (!seeded.length)
|
|
1417
|
+
return undefined;
|
|
1418
|
+
const requestedId = readRequestBrandId(request);
|
|
1419
|
+
if (requestedId == null)
|
|
1420
|
+
return undefined;
|
|
1421
|
+
for (const entry of seeded) {
|
|
1422
|
+
if (entry.brand_id === requestedId)
|
|
1423
|
+
return entry;
|
|
1424
|
+
}
|
|
1425
|
+
return undefined;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Replace a `get_brand_identity` response with a seeded fixture when one
|
|
1429
|
+
* matches the request's `brand_id`. Seeded fixture is authoritative on the
|
|
1430
|
+
* `GetBrandIdentitySuccess` body. Framework-managed envelope fields
|
|
1431
|
+
* (`context`, `ext`) round-trip from the handler — matches the precedent set
|
|
1432
|
+
* by {@link replaceContentStandardsIfSeeded}.
|
|
1433
|
+
*
|
|
1434
|
+
* When no fixture matches, returns the handler response unchanged. Caller
|
|
1435
|
+
* is responsible for skipping the error arm (the dispatcher gates on
|
|
1436
|
+
* `!isErrorResponse`).
|
|
1437
|
+
*/
|
|
1438
|
+
function replaceBrandIdentityIfSeeded(request, response, seeded) {
|
|
1439
|
+
const picked = pickSeededBrandIdentityForRequest(request, seeded);
|
|
1440
|
+
if (!picked)
|
|
1441
|
+
return response;
|
|
1442
|
+
const handlerContext = response.context;
|
|
1443
|
+
const handlerExt = response.ext;
|
|
1444
|
+
const replaced = { ...picked };
|
|
1445
|
+
if (handlerContext !== undefined)
|
|
1446
|
+
replaced.context = handlerContext;
|
|
1447
|
+
if (handlerExt !== undefined)
|
|
1448
|
+
replaced.ext = handlerExt;
|
|
1449
|
+
return replaced;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Read the `offering_id` from an `si_get_offering` request. Required field
|
|
1453
|
+
* per spec; defensive about runtime input.
|
|
1454
|
+
*/
|
|
1455
|
+
function readRequestOfferingId(req) {
|
|
1456
|
+
const id = req?.offering_id;
|
|
1457
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Pick the seeded SI offering entry whose `offering.offering_id` matches the
|
|
1461
|
+
* request's `offering_id`. Returns `undefined` when nothing matches.
|
|
1462
|
+
*/
|
|
1463
|
+
function pickSeededSiOfferingForRequest(request, seeded) {
|
|
1464
|
+
if (!seeded.length)
|
|
1465
|
+
return undefined;
|
|
1466
|
+
const requestedId = readRequestOfferingId(request);
|
|
1467
|
+
if (requestedId == null)
|
|
1468
|
+
return undefined;
|
|
1469
|
+
for (const entry of seeded) {
|
|
1470
|
+
const id = entry.offering?.offering_id;
|
|
1471
|
+
if (typeof id === 'string' && id === requestedId)
|
|
1472
|
+
return entry;
|
|
1473
|
+
}
|
|
1474
|
+
return undefined;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Replace an `si_get_offering` response with a seeded fixture when one
|
|
1478
|
+
* matches the request's `offering_id`. Seeded fixture is authoritative on
|
|
1479
|
+
* the offering body (`available`, `offering_token`, `offering`, ...);
|
|
1480
|
+
* handler's `context` and `ext` round-trip — same envelope-preservation
|
|
1481
|
+
* policy as {@link replaceBrandIdentityIfSeeded}.
|
|
1482
|
+
*
|
|
1483
|
+
* When no fixture matches, returns the handler response unchanged.
|
|
1484
|
+
*
|
|
1485
|
+
* Note: seeded fixture is authoritative on the entire offering body,
|
|
1486
|
+
* including `offering_token`. The token is intentionally NOT preserved
|
|
1487
|
+
* from the handler — storyboards seed `offering_token` to drive
|
|
1488
|
+
* deterministic session continuation in downstream steps. When/if a
|
|
1489
|
+
* stateful SI session bridge ships (phase 5+; see
|
|
1490
|
+
* adcontextprotocol/adcp-client#1755), the storyboard contract becomes
|
|
1491
|
+
* coupled: seeded `offering_token` here must match the seeded session's
|
|
1492
|
+
* `offering_token`, or `si_initiate_session` will reject as stale-token.
|
|
1493
|
+
*/
|
|
1494
|
+
function replaceSiOfferingIfSeeded(request, response, seeded) {
|
|
1495
|
+
const picked = pickSeededSiOfferingForRequest(request, seeded);
|
|
1496
|
+
if (!picked)
|
|
1497
|
+
return response;
|
|
1498
|
+
const handlerContext = response.context;
|
|
1499
|
+
const handlerExt = response.ext;
|
|
1500
|
+
const replaced = { ...picked };
|
|
1501
|
+
if (handlerContext !== undefined)
|
|
1502
|
+
replaced.context = handlerContext;
|
|
1503
|
+
if (handlerExt !== undefined)
|
|
1504
|
+
replaced.ext = handlerExt;
|
|
1505
|
+
return replaced;
|
|
1506
|
+
}
|
|
119
1507
|
/**
|
|
120
1508
|
* Bridge the default test-controller store (a `Map<string, unknown>` that
|
|
121
1509
|
* holds seeded fixtures by `product_id`, populated by `seed_product` scenarios)
|
|
@@ -185,8 +1573,14 @@ function bridgeFromTestControllerStore(store, productDefaults = {}) {
|
|
|
185
1573
|
* ```
|
|
186
1574
|
*/
|
|
187
1575
|
function bridgeFromSessionStore(opts) {
|
|
188
|
-
const { loadSession, selectSeededProducts, productDefaults = {} } = opts;
|
|
189
|
-
|
|
1576
|
+
const { loadSession, selectSeededProducts, productDefaults = {}, selectSeededCreatives, selectSeededMediaBuys, selectSeededMediaBuyDelivery, selectSeededAccounts, selectSeededAccountFinancials, selectSeededCreativeFormats, selectSeededPropertyLists, selectSeededContentStandards, selectSeededCollectionLists, selectSeededSignals, selectSeededCreativeDelivery, selectSeededCreativeFeatures, selectSeededBrandIdentity, selectSeededRights, selectSeededSiOffering, } = opts;
|
|
1577
|
+
// Each per-tool callback resolves the session per-request (no memoisation —
|
|
1578
|
+
// same contract as the original `getSeededProducts` path) and awaits the
|
|
1579
|
+
// selector so callers can lazy-load seed collections on the selector path.
|
|
1580
|
+
// Selectors are wired only when the adopter provided them; absent selectors
|
|
1581
|
+
// leave the bridge callback omitted (opt-in by presence on the bridge
|
|
1582
|
+
// interface, same shape as `getSeededProducts`).
|
|
1583
|
+
const bridge = {
|
|
190
1584
|
getSeededProducts: async (ctx) => {
|
|
191
1585
|
const session = await loadSession(ctx.input);
|
|
192
1586
|
const entries = await selectSeededProducts(session);
|
|
@@ -203,5 +1597,111 @@ function bridgeFromSessionStore(opts) {
|
|
|
203
1597
|
return out;
|
|
204
1598
|
},
|
|
205
1599
|
};
|
|
1600
|
+
if (selectSeededCreatives) {
|
|
1601
|
+
bridge.getSeededCreatives = async (ctx) => {
|
|
1602
|
+
const session = await loadSession(ctx.input);
|
|
1603
|
+
const entries = await selectSeededCreatives(session);
|
|
1604
|
+
return entries ? Array.from(entries) : [];
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
if (selectSeededMediaBuys) {
|
|
1608
|
+
bridge.getSeededMediaBuys = async (ctx) => {
|
|
1609
|
+
const session = await loadSession(ctx.input);
|
|
1610
|
+
const entries = await selectSeededMediaBuys(session);
|
|
1611
|
+
return entries ? Array.from(entries) : [];
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
if (selectSeededMediaBuyDelivery) {
|
|
1615
|
+
bridge.getSeededMediaBuyDelivery = async (ctx) => {
|
|
1616
|
+
const session = await loadSession(ctx.input);
|
|
1617
|
+
const entries = await selectSeededMediaBuyDelivery(session);
|
|
1618
|
+
return entries ? Array.from(entries) : [];
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
if (selectSeededAccounts) {
|
|
1622
|
+
bridge.getSeededAccounts = async (ctx) => {
|
|
1623
|
+
const session = await loadSession(ctx.input);
|
|
1624
|
+
const entries = await selectSeededAccounts(session);
|
|
1625
|
+
return entries ? Array.from(entries) : [];
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
if (selectSeededAccountFinancials) {
|
|
1629
|
+
bridge.getSeededAccountFinancials = async (ctx) => {
|
|
1630
|
+
const session = await loadSession(ctx.input);
|
|
1631
|
+
const entries = await selectSeededAccountFinancials(session);
|
|
1632
|
+
return entries ? Array.from(entries) : [];
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
if (selectSeededCreativeFormats) {
|
|
1636
|
+
bridge.getSeededCreativeFormats = async (ctx) => {
|
|
1637
|
+
const session = await loadSession(ctx.input);
|
|
1638
|
+
const entries = await selectSeededCreativeFormats(session);
|
|
1639
|
+
return entries ? Array.from(entries) : [];
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
if (selectSeededPropertyLists) {
|
|
1643
|
+
bridge.getSeededPropertyLists = async (ctx) => {
|
|
1644
|
+
const session = await loadSession(ctx.input);
|
|
1645
|
+
const entries = await selectSeededPropertyLists(session);
|
|
1646
|
+
return entries ? Array.from(entries) : [];
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
if (selectSeededContentStandards) {
|
|
1650
|
+
bridge.getSeededContentStandards = async (ctx) => {
|
|
1651
|
+
const session = await loadSession(ctx.input);
|
|
1652
|
+
const entries = await selectSeededContentStandards(session);
|
|
1653
|
+
return entries ? Array.from(entries) : [];
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
if (selectSeededCollectionLists) {
|
|
1657
|
+
bridge.getSeededCollectionLists = async (ctx) => {
|
|
1658
|
+
const session = await loadSession(ctx.input);
|
|
1659
|
+
const entries = await selectSeededCollectionLists(session);
|
|
1660
|
+
return entries ? Array.from(entries) : [];
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
if (selectSeededSignals) {
|
|
1664
|
+
bridge.getSeededSignals = async (ctx) => {
|
|
1665
|
+
const session = await loadSession(ctx.input);
|
|
1666
|
+
const entries = await selectSeededSignals(session);
|
|
1667
|
+
return entries ? Array.from(entries) : [];
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
if (selectSeededCreativeDelivery) {
|
|
1671
|
+
bridge.getSeededCreativeDelivery = async (ctx) => {
|
|
1672
|
+
const session = await loadSession(ctx.input);
|
|
1673
|
+
const entries = await selectSeededCreativeDelivery(session);
|
|
1674
|
+
return entries ? Array.from(entries) : [];
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
if (selectSeededCreativeFeatures) {
|
|
1678
|
+
bridge.getSeededCreativeFeatures = async (ctx) => {
|
|
1679
|
+
const session = await loadSession(ctx.input);
|
|
1680
|
+
const entries = await selectSeededCreativeFeatures(session);
|
|
1681
|
+
return entries ? Array.from(entries) : [];
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
if (selectSeededBrandIdentity) {
|
|
1685
|
+
bridge.getSeededBrandIdentity = async (ctx) => {
|
|
1686
|
+
const session = await loadSession(ctx.input);
|
|
1687
|
+
const entries = await selectSeededBrandIdentity(session);
|
|
1688
|
+
return entries ? Array.from(entries) : [];
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
if (selectSeededRights) {
|
|
1692
|
+
bridge.getSeededRights = async (ctx) => {
|
|
1693
|
+
const session = await loadSession(ctx.input);
|
|
1694
|
+
const entries = await selectSeededRights(session);
|
|
1695
|
+
return entries ? Array.from(entries) : [];
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
if (selectSeededSiOffering) {
|
|
1699
|
+
bridge.getSeededSiOffering = async (ctx) => {
|
|
1700
|
+
const session = await loadSession(ctx.input);
|
|
1701
|
+
const entries = await selectSeededSiOffering(session);
|
|
1702
|
+
return entries ? Array.from(entries) : [];
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
return bridge;
|
|
206
1706
|
}
|
|
207
1707
|
//# sourceMappingURL=test-controller-bridge.js.map
|