@adcp/sdk 6.7.0 → 6.8.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.js +15 -3
- package/dist/lib/adapters/derived-account-store.d.ts +152 -0
- package/dist/lib/adapters/derived-account-store.d.ts.map +1 -0
- package/dist/lib/adapters/derived-account-store.js +135 -0
- package/dist/lib/adapters/derived-account-store.js.map +1 -0
- package/dist/lib/adapters/implicit-account-store.d.ts +3 -1
- package/dist/lib/adapters/implicit-account-store.d.ts.map +1 -1
- package/dist/lib/adapters/implicit-account-store.js +3 -1
- package/dist/lib/adapters/implicit-account-store.js.map +1 -1
- package/dist/lib/adapters/index.d.ts +1 -0
- package/dist/lib/adapters/index.d.ts.map +1 -1
- package/dist/lib/adapters/index.js +7 -1
- package/dist/lib/adapters/index.js.map +1 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.d.ts +3 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.d.ts.map +1 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.js +3 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.js.map +1 -1
- package/dist/lib/adapters/roster-account-store.d.ts +85 -24
- package/dist/lib/adapters/roster-account-store.d.ts.map +1 -1
- package/dist/lib/adapters/roster-account-store.js +52 -30
- package/dist/lib/adapters/roster-account-store.js.map +1 -1
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +13 -8
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts +81 -0
- package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts.map +1 -0
- package/dist/lib/mock-server/creative-ad-server/seed-data.js +200 -0
- package/dist/lib/mock-server/creative-ad-server/seed-data.js.map +1 -0
- package/dist/lib/mock-server/creative-ad-server/server.d.ts +39 -0
- package/dist/lib/mock-server/creative-ad-server/server.d.ts.map +1 -0
- package/dist/lib/mock-server/creative-ad-server/server.js +618 -0
- package/dist/lib/mock-server/creative-ad-server/server.js.map +1 -0
- package/dist/lib/mock-server/index.d.ts.map +1 -1
- package/dist/lib/mock-server/index.js +180 -24
- package/dist/lib/mock-server/index.js.map +1 -1
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts +66 -0
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts.map +1 -0
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js +193 -0
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js.map +1 -0
- package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts +33 -0
- package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts.map +1 -0
- package/dist/lib/mock-server/sales-non-guaranteed/server.js +782 -0
- package/dist/lib/mock-server/sales-non-guaranteed/server.js.map +1 -0
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts +50 -0
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts.map +1 -0
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.js +133 -0
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.js.map +1 -0
- package/dist/lib/mock-server/sponsored-intelligence/server.d.ts +13 -0
- package/dist/lib/mock-server/sponsored-intelligence/server.d.ts.map +1 -0
- package/dist/lib/mock-server/sponsored-intelligence/server.js +609 -0
- package/dist/lib/mock-server/sponsored-intelligence/server.js.map +1 -0
- package/dist/lib/protocols/mcp.d.ts.map +1 -1
- package/dist/lib/protocols/mcp.js +1 -41
- package/dist/lib/protocols/mcp.js.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/account-mode.d.ts +113 -0
- package/dist/lib/server/account-mode.d.ts.map +1 -0
- package/dist/lib/server/account-mode.js +125 -0
- package/dist/lib/server/account-mode.js.map +1 -0
- package/dist/lib/server/adcp-server.js +41 -0
- package/dist/lib/server/adcp-server.js.map +1 -1
- package/dist/lib/server/auth.d.ts +35 -0
- package/dist/lib/server/auth.d.ts.map +1 -1
- package/dist/lib/server/auth.js.map +1 -1
- package/dist/lib/server/create-adcp-server.d.ts +26 -9
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +46 -20
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/ctx-metadata/store.d.ts +1 -1
- package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -1
- package/dist/lib/server/ctx-metadata/store.js +1 -0
- package/dist/lib/server/ctx-metadata/store.js.map +1 -1
- package/dist/lib/server/decisioning/account.d.ts +5 -0
- package/dist/lib/server/decisioning/account.d.ts.map +1 -1
- package/dist/lib/server/decisioning/account.js.map +1 -1
- package/dist/lib/server/decisioning/buyer-agent.d.ts +37 -4
- package/dist/lib/server/decisioning/buyer-agent.d.ts.map +1 -1
- package/dist/lib/server/decisioning/buyer-agent.js +12 -2
- package/dist/lib/server/decisioning/buyer-agent.js.map +1 -1
- package/dist/lib/server/decisioning/compose.d.ts +33 -2
- package/dist/lib/server/decisioning/compose.d.ts.map +1 -1
- package/dist/lib/server/decisioning/compose.js +13 -46
- package/dist/lib/server/decisioning/compose.js.map +1 -1
- package/dist/lib/server/decisioning/index.d.ts +2 -1
- package/dist/lib/server/decisioning/index.d.ts.map +1 -1
- package/dist/lib/server/decisioning/index.js +2 -1
- package/dist/lib/server/decisioning/index.js.map +1 -1
- package/dist/lib/server/decisioning/platform-helpers.d.ts +18 -0
- package/dist/lib/server/decisioning/platform-helpers.d.ts.map +1 -1
- package/dist/lib/server/decisioning/platform-helpers.js +20 -0
- package/dist/lib/server/decisioning/platform-helpers.js.map +1 -1
- package/dist/lib/server/decisioning/platform.d.ts +19 -21
- 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/runtime/from-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +334 -44
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/observed-modes.d.ts +40 -0
- package/dist/lib/server/decisioning/runtime/observed-modes.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/observed-modes.js +82 -0
- package/dist/lib/server/decisioning/runtime/observed-modes.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +2 -2
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/validate-platform.js +9 -1
- package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -1
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts +125 -0
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js +52 -0
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js.map +1 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts +16 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -1
- package/dist/lib/server/decisioning/tenant-registry.js.map +1 -1
- package/dist/lib/server/index.d.ts +4 -1
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +9 -3
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/serve.js +20 -2
- package/dist/lib/server/serve.js.map +1 -1
- package/dist/lib/server/test-controller.d.ts +3 -0
- package/dist/lib/server/test-controller.d.ts.map +1 -1
- package/dist/lib/server/test-controller.js +23 -20
- package/dist/lib/server/test-controller.js.map +1 -1
- package/dist/lib/testing/comply-controller.d.ts +23 -2
- package/dist/lib/testing/comply-controller.d.ts.map +1 -1
- package/dist/lib/testing/comply-controller.js +19 -2
- package/dist/lib/testing/comply-controller.js.map +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.d.ts.map +1 -1
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/validations.js +36 -54
- package/dist/lib/testing/storyboard/validations.js.map +1 -1
- package/dist/lib/testing/test-controller.d.ts +10 -4
- package/dist/lib/testing/test-controller.d.ts.map +1 -1
- package/dist/lib/testing/test-controller.js +9 -3
- package/dist/lib/testing/test-controller.js.map +1 -1
- package/dist/lib/types/index.d.ts +3 -2
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +3 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/utils/glob.d.ts +4 -2
- package/dist/lib/utils/glob.d.ts.map +1 -1
- package/dist/lib/utils/glob.js +4 -2
- package/dist/lib/utils/glob.js.map +1 -1
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/docs/llms.txt +2 -2
- package/examples/README.md +29 -13
- package/examples/hello-cluster.ts +62 -23
- package/examples/hello_creative_adapter_ad_server.ts +790 -0
- package/examples/hello_seller_adapter_guaranteed.ts +80 -22
- package/examples/hello_seller_adapter_non_guaranteed.ts +1020 -0
- package/examples/hello_si_adapter_brand.ts +572 -0
- package/package.json +3 -2
- package/skills/build-creative-agent/SKILL.md +103 -183
- package/skills/build-generative-seller-agent/SKILL.md +15 -9
- package/skills/build-governance-agent/SKILL.md +20 -11
- package/skills/build-retail-media-agent/SKILL.md +10 -8
- package/skills/build-seller-agent/SKILL.md +15 -13
- package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +9 -31
- package/skills/build-si-agent/SKILL.md +251 -196
- package/skills/build-signals-agent/SKILL.md +2 -0
- package/skills/call-adcp-agent/SKILL.md +7 -1
- package/skills/call-adcp-agent.previous/SKILL.md +0 -261
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hello_seller_adapter_non_guaranteed — worked starting point for an
|
|
3
|
+
* AdCP non-guaranteed sales agent (specialism `sales-non-guaranteed`)
|
|
4
|
+
* that wraps an upstream programmatic-auction platform with sync
|
|
5
|
+
* confirmation over static Bearer.
|
|
6
|
+
*
|
|
7
|
+
* Closes #1458 (sub-issue of #1381 umbrella). Closest neighbor in the
|
|
8
|
+
* worked-reference family is `hello_seller_adapter_guaranteed.ts` —
|
|
9
|
+
* this adapter is the deletion-fork (rip out HITL approval, sync
|
|
10
|
+
* confirmation throughout). The auction shape applies to DSP-side
|
|
11
|
+
* sellers, retail-media remnant, header-bidding inventory, and any
|
|
12
|
+
* non-walled-garden seller.
|
|
13
|
+
*
|
|
14
|
+
* Headline behavior: `create_media_buy` returns `media_buy_id` synchronously
|
|
15
|
+
* on `success` — auction is immediate, no IO-review task. Floor-priced
|
|
16
|
+
* products (`pricing_options[].fixed_price = product.min_cpm`); pacing
|
|
17
|
+
* propagated to upstream order; spend-only forecast surfaced inline.
|
|
18
|
+
*
|
|
19
|
+
* Fork this. Replace `upstream` with calls to your real backend. The
|
|
20
|
+
* AdCP-facing platform methods stay the same.
|
|
21
|
+
*
|
|
22
|
+
* Auction mode is the deletion-fork of the guaranteed sibling: `createMediaBuy`
|
|
23
|
+
* returns sync, no `ctx.handoffToTask`, no IO poll loop, no task envelope.
|
|
24
|
+
* If your backend has HITL approval, fork the guaranteed example instead.
|
|
25
|
+
*
|
|
26
|
+
* FORK CHECKLIST
|
|
27
|
+
* 1. Replace every `// SWAP:` marker with calls to your backend.
|
|
28
|
+
* 2. Replace `KNOWN_PUBLISHERS` with your tenant directory.
|
|
29
|
+
* 3. Replace `projectProduct()` defaults — `publisher_properties` selector,
|
|
30
|
+
* `pricing_options[]`, `reporting_capabilities` — with values your
|
|
31
|
+
* seller actually commits to.
|
|
32
|
+
* 4. Replace `advertiserId = networkCode` collapse with a real lookup
|
|
33
|
+
* (production splits network and advertiser ids).
|
|
34
|
+
* 5. Validate: `node --test test/examples/hello-seller-adapter-non-guaranteed.test.js`
|
|
35
|
+
* 6. **DELETE the `// TEST-ONLY` blocks** before deploying:
|
|
36
|
+
* - sandbox-arm in `accounts.resolve` (resolves storyboard runner's
|
|
37
|
+
* synthetic `{brand, sandbox: true}` refs to a known network and
|
|
38
|
+
* stamps `mode: 'sandbox'` on the returned Account so the framework
|
|
39
|
+
* gate admits the comply controller)
|
|
40
|
+
* - `complyTest:` config block on `createAdcpServerFromPlatform`
|
|
41
|
+
* - in-memory `seededMediaBuys` / `simulatedDelivery` / `adapterStatusOverrides`
|
|
42
|
+
* These exist so the conformance harness can drive cascade scenarios
|
|
43
|
+
* deterministically. Production sellers ship without them.
|
|
44
|
+
*
|
|
45
|
+
* Demo:
|
|
46
|
+
* npx @adcp/sdk@latest mock-server sales-non-guaranteed --port 4451
|
|
47
|
+
* UPSTREAM_URL=http://127.0.0.1:4451 \
|
|
48
|
+
* npx tsx examples/hello_seller_adapter_non_guaranteed.ts
|
|
49
|
+
* adcp storyboard run http://127.0.0.1:3007/mcp sales_non_guaranteed \
|
|
50
|
+
* --auth sk_harness_do_not_use_in_prod
|
|
51
|
+
* curl http://127.0.0.1:4451/_debug/traffic
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import {
|
|
55
|
+
createAdcpServerFromPlatform,
|
|
56
|
+
serve,
|
|
57
|
+
verifyApiKey,
|
|
58
|
+
createIdempotencyStore,
|
|
59
|
+
createUpstreamHttpClient,
|
|
60
|
+
memoryBackend,
|
|
61
|
+
AdcpError,
|
|
62
|
+
createMediaBuyStore,
|
|
63
|
+
InMemoryStateStore,
|
|
64
|
+
type DecisioningPlatform,
|
|
65
|
+
type SalesCorePlatform,
|
|
66
|
+
type SalesIngestionPlatform,
|
|
67
|
+
type AccountStore,
|
|
68
|
+
type Account,
|
|
69
|
+
type SyncCreativesRow,
|
|
70
|
+
type SyncAccountsResultRow,
|
|
71
|
+
} from '@adcp/sdk/server';
|
|
72
|
+
import type {
|
|
73
|
+
GetProductsRequest,
|
|
74
|
+
GetProductsResponse,
|
|
75
|
+
CreateMediaBuyRequest,
|
|
76
|
+
CreateMediaBuySuccess,
|
|
77
|
+
UpdateMediaBuyRequest,
|
|
78
|
+
UpdateMediaBuySuccess,
|
|
79
|
+
GetMediaBuysRequest,
|
|
80
|
+
GetMediaBuysResponse,
|
|
81
|
+
GetMediaBuyDeliveryRequest,
|
|
82
|
+
GetMediaBuyDeliveryResponse,
|
|
83
|
+
ListCreativeFormatsResponse,
|
|
84
|
+
} from '@adcp/sdk/types';
|
|
85
|
+
|
|
86
|
+
// `Product` isn't re-exported from `@adcp/sdk/types`; derive from response.
|
|
87
|
+
type Product = GetProductsResponse['products'][number];
|
|
88
|
+
|
|
89
|
+
const UPSTREAM_URL = process.env['UPSTREAM_URL'] ?? 'http://127.0.0.1:4451';
|
|
90
|
+
const UPSTREAM_API_KEY = process.env['UPSTREAM_API_KEY'] ?? 'mock_sales_non_guaranteed_key_do_not_use_in_prod';
|
|
91
|
+
const PORT = Number(process.env['PORT'] ?? 3007);
|
|
92
|
+
const ADCP_AUTH_TOKEN = process.env['ADCP_AUTH_TOKEN'] ?? 'sk_harness_do_not_use_in_prod';
|
|
93
|
+
const PUBLIC_AGENT_URL = process.env['PUBLIC_AGENT_URL'] ?? `http://127.0.0.1:${PORT}`;
|
|
94
|
+
|
|
95
|
+
const KNOWN_PUBLISHERS = ['remnant-network.example', 'acmeoutdoor.example', 'pinnacle-agency.example'];
|
|
96
|
+
|
|
97
|
+
// TEST-ONLY: id-prefix used by the sandbox arm in `accounts.resolve` so
|
|
98
|
+
// production sellers don't need this; remove the sandbox arm + this
|
|
99
|
+
// constant before deploying. See FORK CHECKLIST item 6.
|
|
100
|
+
const SANDBOX_ID_PREFIX = 'sandbox_';
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Upstream client — SWAP for production.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
interface UpstreamNetwork {
|
|
107
|
+
network_code: string;
|
|
108
|
+
display_name: string;
|
|
109
|
+
adcp_publisher: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface UpstreamProductPricing {
|
|
113
|
+
min_cpm: number;
|
|
114
|
+
target_cpm?: number;
|
|
115
|
+
currency: string;
|
|
116
|
+
min_spend?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface UpstreamForecastPoint {
|
|
120
|
+
budget?: number;
|
|
121
|
+
metrics: {
|
|
122
|
+
impressions?: { low: number; mid: number; high: number };
|
|
123
|
+
clicks?: { low: number; mid: number; high: number };
|
|
124
|
+
spend?: { low: number; mid: number; high: number };
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface UpstreamForecast {
|
|
129
|
+
product_id: string;
|
|
130
|
+
forecast_range_unit: 'spend';
|
|
131
|
+
method: 'modeled';
|
|
132
|
+
currency: string;
|
|
133
|
+
points: UpstreamForecastPoint[];
|
|
134
|
+
min_budget_warning?: { required: number; reason: string };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface UpstreamProduct {
|
|
138
|
+
product_id: string;
|
|
139
|
+
name: string;
|
|
140
|
+
network_code: string;
|
|
141
|
+
delivery_type: 'non_guaranteed';
|
|
142
|
+
channel: 'video' | 'ctv' | 'display' | 'audio';
|
|
143
|
+
format_ids: string[];
|
|
144
|
+
ad_unit_ids: string[];
|
|
145
|
+
pricing: UpstreamProductPricing;
|
|
146
|
+
forecast?: UpstreamForecast;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface UpstreamLineItem {
|
|
150
|
+
line_item_id: string;
|
|
151
|
+
order_id: string;
|
|
152
|
+
product_id: string;
|
|
153
|
+
status: 'ready' | 'paused' | 'delivering' | 'completed';
|
|
154
|
+
budget: number;
|
|
155
|
+
ad_unit_targeting: string[];
|
|
156
|
+
creative_ids: string[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface UpstreamOrder {
|
|
160
|
+
order_id: string;
|
|
161
|
+
network_code: string;
|
|
162
|
+
name: string;
|
|
163
|
+
status: 'confirmed' | 'delivering' | 'completed' | 'canceled' | 'rejected';
|
|
164
|
+
advertiser_id: string;
|
|
165
|
+
currency: string;
|
|
166
|
+
budget: number;
|
|
167
|
+
pacing: 'even' | 'asap' | 'front_loaded';
|
|
168
|
+
flight_start?: string;
|
|
169
|
+
flight_end?: string;
|
|
170
|
+
rejection_reason?: string;
|
|
171
|
+
line_items?: UpstreamLineItem[];
|
|
172
|
+
created_at?: string;
|
|
173
|
+
updated_at?: string;
|
|
174
|
+
replayed?: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface UpstreamCreative {
|
|
178
|
+
creative_id: string;
|
|
179
|
+
network_code: string;
|
|
180
|
+
name: string;
|
|
181
|
+
format_id: string;
|
|
182
|
+
advertiser_id: string;
|
|
183
|
+
status: 'active' | 'paused' | 'archived';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface UpstreamDelivery {
|
|
187
|
+
order_id: string;
|
|
188
|
+
currency: string;
|
|
189
|
+
pacing: 'even' | 'asap' | 'front_loaded';
|
|
190
|
+
reporting_period: { start?: string; end?: string };
|
|
191
|
+
totals: { impressions: number; clicks: number; spend: number; budget_remaining: number };
|
|
192
|
+
line_items: Array<{
|
|
193
|
+
line_item_id: string;
|
|
194
|
+
product_id: string;
|
|
195
|
+
impressions: number;
|
|
196
|
+
clicks: number;
|
|
197
|
+
spend: number;
|
|
198
|
+
currency: string;
|
|
199
|
+
effective_cpm: number;
|
|
200
|
+
pacing_pct: number;
|
|
201
|
+
}>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const http = createUpstreamHttpClient({
|
|
205
|
+
baseUrl: UPSTREAM_URL,
|
|
206
|
+
auth: { kind: 'static_bearer', token: UPSTREAM_API_KEY },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const networkHeader = (networkCode: string): Record<string, string> => ({ 'X-Network-Code': networkCode });
|
|
210
|
+
|
|
211
|
+
const upstream = {
|
|
212
|
+
// SWAP: tenant lookup. Mock exposes /_lookup; production typically a
|
|
213
|
+
// network registry / service-account scope endpoint.
|
|
214
|
+
async lookupNetwork(publisherDomain: string): Promise<UpstreamNetwork | null> {
|
|
215
|
+
const { body } = await http.get<UpstreamNetwork>('/_lookup/network', { adcp_publisher: publisherDomain });
|
|
216
|
+
return body;
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// SWAP: product catalog. Mock filters by network_code via header + optional
|
|
220
|
+
// ?channel. When `flight_start`/`flight_end`/`budget` are passed, the mock
|
|
221
|
+
// returns per-product `forecast` inline — single round-trip instead of N
|
|
222
|
+
// follow-up `/v1/forecast` calls.
|
|
223
|
+
async listProducts(
|
|
224
|
+
networkCode: string,
|
|
225
|
+
opts?: {
|
|
226
|
+
channel?: 'video' | 'ctv' | 'display' | 'audio';
|
|
227
|
+
flightStart?: string;
|
|
228
|
+
flightEnd?: string;
|
|
229
|
+
budget?: number;
|
|
230
|
+
}
|
|
231
|
+
): Promise<UpstreamProduct[]> {
|
|
232
|
+
const params: Record<string, string> = {};
|
|
233
|
+
if (opts?.channel) params['channel'] = opts.channel;
|
|
234
|
+
if (opts?.flightStart) params['flight_start'] = opts.flightStart;
|
|
235
|
+
if (opts?.flightEnd) params['flight_end'] = opts.flightEnd;
|
|
236
|
+
if (opts?.budget !== undefined) params['budget'] = String(opts.budget);
|
|
237
|
+
const { body } = await http.get<{ products: UpstreamProduct[] }>(
|
|
238
|
+
'/v1/products',
|
|
239
|
+
params,
|
|
240
|
+
networkHeader(networkCode)
|
|
241
|
+
);
|
|
242
|
+
return body?.products ?? [];
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// SWAP: per-product forecast. Use this when your backend separates the
|
|
246
|
+
// catalog and forecast surfaces. For the worked-mock case we fold forecast
|
|
247
|
+
// into `listProducts` above; this method shows the discrete shape.
|
|
248
|
+
async getForecast(
|
|
249
|
+
networkCode: string,
|
|
250
|
+
body: {
|
|
251
|
+
product_id: string;
|
|
252
|
+
targeting?: Record<string, unknown>;
|
|
253
|
+
flight_dates?: { start?: string; end?: string };
|
|
254
|
+
budget?: number;
|
|
255
|
+
}
|
|
256
|
+
): Promise<UpstreamForecast | null> {
|
|
257
|
+
const r = await http.post<UpstreamForecast>('/v1/forecast', body, networkHeader(networkCode));
|
|
258
|
+
return r.body;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// SWAP: list orders. Returns { orders: [...] }.
|
|
262
|
+
async listOrders(networkCode: string): Promise<UpstreamOrder[]> {
|
|
263
|
+
const { body } = await http.get<{ orders: UpstreamOrder[] }>('/v1/orders', undefined, networkHeader(networkCode));
|
|
264
|
+
return body?.orders ?? [];
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// SWAP: sync create. Mock returns 201 with status='confirmed'. No HITL
|
|
268
|
+
// task — auction-cleared programmatic. `client_request_id` round-trips
|
|
269
|
+
// to upstream for at-most-once execution; replay returns same order_id
|
|
270
|
+
// with `replayed: true`.
|
|
271
|
+
async createOrder(
|
|
272
|
+
networkCode: string,
|
|
273
|
+
body: {
|
|
274
|
+
name: string;
|
|
275
|
+
advertiser_id: string;
|
|
276
|
+
currency: string;
|
|
277
|
+
budget: number;
|
|
278
|
+
pacing?: 'even' | 'asap' | 'front_loaded';
|
|
279
|
+
flight_start?: string;
|
|
280
|
+
flight_end?: string;
|
|
281
|
+
line_items?: Array<{ product_id: string; budget: number }>;
|
|
282
|
+
client_request_id?: string;
|
|
283
|
+
}
|
|
284
|
+
): Promise<UpstreamOrder> {
|
|
285
|
+
const r = await http.post<UpstreamOrder>('/v1/orders', body, networkHeader(networkCode));
|
|
286
|
+
if (r.body === null) {
|
|
287
|
+
throw new AdcpError('INVALID_REQUEST', { message: 'order creation rejected by upstream' });
|
|
288
|
+
}
|
|
289
|
+
return r.body;
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async getOrder(networkCode: string, orderId: string): Promise<UpstreamOrder | null> {
|
|
293
|
+
const { body } = await http.get<UpstreamOrder>(
|
|
294
|
+
`/v1/orders/${encodeURIComponent(orderId)}`,
|
|
295
|
+
undefined,
|
|
296
|
+
networkHeader(networkCode)
|
|
297
|
+
);
|
|
298
|
+
return body;
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
async listLineItems(networkCode: string, orderId: string): Promise<UpstreamLineItem[]> {
|
|
302
|
+
const { body } = await http.get<{ line_items: UpstreamLineItem[] }>(
|
|
303
|
+
`/v1/orders/${encodeURIComponent(orderId)}/lineitems`,
|
|
304
|
+
undefined,
|
|
305
|
+
networkHeader(networkCode)
|
|
306
|
+
);
|
|
307
|
+
return body?.line_items ?? [];
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
async createLineItem(
|
|
311
|
+
networkCode: string,
|
|
312
|
+
orderId: string,
|
|
313
|
+
body: { product_id: string; budget: number; ad_unit_ids?: string[]; client_request_id?: string }
|
|
314
|
+
): Promise<{ line_item_id: string }> {
|
|
315
|
+
const r = await http.post<{ line_item_id: string }>(
|
|
316
|
+
`/v1/orders/${encodeURIComponent(orderId)}/lineitems`,
|
|
317
|
+
body,
|
|
318
|
+
networkHeader(networkCode)
|
|
319
|
+
);
|
|
320
|
+
if (r.body === null) {
|
|
321
|
+
throw new AdcpError('INVALID_REQUEST', { message: 'line item creation rejected by upstream' });
|
|
322
|
+
}
|
|
323
|
+
return r.body;
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
async createCreative(
|
|
327
|
+
networkCode: string,
|
|
328
|
+
body: { name: string; format_id: string; advertiser_id: string; client_request_id?: string }
|
|
329
|
+
): Promise<UpstreamCreative> {
|
|
330
|
+
const r = await http.post<UpstreamCreative>('/v1/creatives', body, networkHeader(networkCode));
|
|
331
|
+
if (r.body === null) {
|
|
332
|
+
throw new AdcpError('INVALID_REQUEST', { message: 'creative creation rejected by upstream' });
|
|
333
|
+
}
|
|
334
|
+
return r.body;
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
async getDelivery(networkCode: string, orderId: string): Promise<UpstreamDelivery | null> {
|
|
338
|
+
const { body } = await http.get<UpstreamDelivery>(
|
|
339
|
+
`/v1/orders/${encodeURIComponent(orderId)}/delivery`,
|
|
340
|
+
undefined,
|
|
341
|
+
networkHeader(networkCode)
|
|
342
|
+
);
|
|
343
|
+
return body;
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// AdCP-side adapter — typed against SalesCorePlatform & SalesIngestionPlatform.
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
interface NetworkMeta {
|
|
352
|
+
network_code: string;
|
|
353
|
+
publisher_domain: string;
|
|
354
|
+
[key: string]: unknown;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const FORMAT_AGENT_URL = PUBLIC_AGENT_URL;
|
|
358
|
+
|
|
359
|
+
/** Project upstream product onto AdCP `Product`. Auction-cleared inventory
|
|
360
|
+
* surfaces `min_cpm` as the `pricing_options[].fixed_price` (floor); buyers
|
|
361
|
+
* bid at or above. Production sellers can layer `auction` pricing models or
|
|
362
|
+
* deal-id-keyed alternative pricing options on top. */
|
|
363
|
+
function projectProduct(p: UpstreamProduct, publisherDomain: string): Product {
|
|
364
|
+
return {
|
|
365
|
+
product_id: p.product_id,
|
|
366
|
+
name: p.name,
|
|
367
|
+
description: `${p.name} — programmatic remnant ${p.channel}`,
|
|
368
|
+
publisher_properties: [{ publisher_domain: publisherDomain, selection_type: 'all' }],
|
|
369
|
+
channels: [
|
|
370
|
+
p.channel === 'video'
|
|
371
|
+
? 'olv'
|
|
372
|
+
: p.channel === 'ctv'
|
|
373
|
+
? 'ctv'
|
|
374
|
+
: p.channel === 'audio'
|
|
375
|
+
? 'streaming_audio'
|
|
376
|
+
: 'display',
|
|
377
|
+
],
|
|
378
|
+
format_ids: p.format_ids.map(id => ({ agent_url: FORMAT_AGENT_URL, id })),
|
|
379
|
+
delivery_type: 'non_guaranteed',
|
|
380
|
+
pricing_options: [
|
|
381
|
+
{
|
|
382
|
+
pricing_option_id: 'cpm_floor',
|
|
383
|
+
pricing_model: 'cpm',
|
|
384
|
+
currency: p.pricing.currency,
|
|
385
|
+
// Floor pricing — sellers accept any bid ≥ this. Auction-cleared
|
|
386
|
+
// effective CPM lands somewhere between `min_cpm` and `target_cpm`
|
|
387
|
+
// depending on bid pressure; we surface the floor as the firm
|
|
388
|
+
// commitment.
|
|
389
|
+
fixed_price: p.pricing.min_cpm,
|
|
390
|
+
...(p.pricing.min_spend !== undefined && { min_spend: p.pricing.min_spend }),
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
reporting_capabilities: {
|
|
394
|
+
available_reporting_frequencies: ['daily'],
|
|
395
|
+
expected_delay_minutes: 60,
|
|
396
|
+
timezone: 'UTC',
|
|
397
|
+
supports_webhooks: false,
|
|
398
|
+
available_metrics: ['impressions', 'clicks', 'spend'],
|
|
399
|
+
date_range_support: 'date_range',
|
|
400
|
+
},
|
|
401
|
+
// Pass through per-query forecast verbatim — mock returns AdCP-shape
|
|
402
|
+
// already (`points`, `metrics.{impressions,clicks,spend}.{low,mid,high}`,
|
|
403
|
+
// `forecast_range_unit: 'spend'`, `method: 'modeled'`). Real auction-
|
|
404
|
+
// backed sellers (FreeWheel, Magnite SSP) need adapter-side translation.
|
|
405
|
+
...(p.forecast && { forecast: p.forecast }),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function mapMediaBuyStatus(
|
|
410
|
+
orderStatus: UpstreamOrder['status']
|
|
411
|
+
): 'pending_creatives' | 'pending_start' | 'active' | 'paused' | 'completed' | 'canceled' {
|
|
412
|
+
switch (orderStatus) {
|
|
413
|
+
case 'delivering':
|
|
414
|
+
return 'active';
|
|
415
|
+
case 'completed':
|
|
416
|
+
return 'completed';
|
|
417
|
+
case 'canceled':
|
|
418
|
+
case 'rejected':
|
|
419
|
+
return 'canceled';
|
|
420
|
+
case 'confirmed':
|
|
421
|
+
default:
|
|
422
|
+
// Auction-cleared but no creatives attached yet → pending_creatives.
|
|
423
|
+
// Buyer transitions to `active` after sync_creatives lands and the
|
|
424
|
+
// first impression delivers. Framework surfaces the status change
|
|
425
|
+
// via publishStatusChange on resource_type: 'media_buy'.
|
|
426
|
+
return 'pending_creatives';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Status overrides persisted at the adapter level. Real backends mutate the
|
|
431
|
+
// upstream Order on creative-attach (the line-item status flips, the order
|
|
432
|
+
// status flips with it). The mock doesn't model that transition; we track
|
|
433
|
+
// the override here so `get_media_buys` returns the advanced status the
|
|
434
|
+
// storyboard validates. SWAP: drop this map and read upstream state — your
|
|
435
|
+
// backend already persists what we're tracking here.
|
|
436
|
+
//
|
|
437
|
+
// Keyed by `<network_code>::<order_id>` so a forking adopter who keeps the
|
|
438
|
+
// map for in-flight scenarios doesn't accidentally surface tenant A's
|
|
439
|
+
// override on tenant B's `media_buy_id` collision (mock IDs are 32 bits).
|
|
440
|
+
const adapterStatusOverrides = new Map<string, 'pending_start' | 'active'>();
|
|
441
|
+
const overrideKey = (networkCode: string, orderId: string): string => `${networkCode}::${orderId}`;
|
|
442
|
+
|
|
443
|
+
class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, never>, NetworkMeta> {
|
|
444
|
+
capabilities = {
|
|
445
|
+
specialisms: ['sales-non-guaranteed'] as const,
|
|
446
|
+
channels: ['olv', 'ctv', 'display', 'streaming_audio'] as const,
|
|
447
|
+
pricingModels: ['cpm'] as const,
|
|
448
|
+
config: {},
|
|
449
|
+
compliance_testing: {
|
|
450
|
+
scenarios: ['force_media_buy_status', 'simulate_delivery'] as const,
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
accounts: AccountStore<NetworkMeta> = {
|
|
455
|
+
resolve: async ref => {
|
|
456
|
+
if (!ref) return null;
|
|
457
|
+
// SWAP: persist `account_id → network_code` during sync_accounts and
|
|
458
|
+
// serve account_id lookups from there.
|
|
459
|
+
if ('account_id' in ref) return null;
|
|
460
|
+
|
|
461
|
+
// ─── TEST-ONLY: cascade-scenario sandbox-arm ─────────────────────
|
|
462
|
+
// DELETE THIS BLOCK BEFORE DEPLOYING. The compliance runner injects
|
|
463
|
+
// synthetic refs like `{brand: {domain: 'test.example'}, sandbox: true}`
|
|
464
|
+
// for `media_buy_seller/*` cascade scenarios. Routes those refs to a
|
|
465
|
+
// fixed seeded network so the rest of the adapter has something
|
|
466
|
+
// concrete to operate on. Production sellers either reject sandbox
|
|
467
|
+
// refs entirely or route them to a dedicated sandbox tenant.
|
|
468
|
+
//
|
|
469
|
+
// Gate is `ref.sandbox === true` from the wire `AccountReference`.
|
|
470
|
+
// Stamping `mode: 'sandbox'` on the returned `Account` is what admits
|
|
471
|
+
// the framework's `comply_test_controller` gate — see #1435 phase 2/3.
|
|
472
|
+
// No env var is consulted; production traffic that doesn't set
|
|
473
|
+
// `sandbox: true` on the wire never hits this branch.
|
|
474
|
+
if (ref.sandbox === true) {
|
|
475
|
+
const sandboxDomain = 'acmeoutdoor.example';
|
|
476
|
+
const network = await upstream.lookupNetwork(sandboxDomain);
|
|
477
|
+
if (!network) return null;
|
|
478
|
+
return {
|
|
479
|
+
id: `${SANDBOX_ID_PREFIX}${network.network_code}`,
|
|
480
|
+
name: `Sandbox: ${network.display_name}`,
|
|
481
|
+
status: 'active',
|
|
482
|
+
mode: 'sandbox',
|
|
483
|
+
...(ref.operator !== undefined && { operator: ref.operator }),
|
|
484
|
+
brand: { domain: ref.brand.domain ?? sandboxDomain },
|
|
485
|
+
ctx_metadata: { network_code: network.network_code, publisher_domain: network.adcp_publisher },
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// ─── /TEST-ONLY ──────────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
const publisherDomain = ref.brand.domain;
|
|
491
|
+
if (!publisherDomain) return null;
|
|
492
|
+
const network = await upstream.lookupNetwork(publisherDomain);
|
|
493
|
+
if (!network) return null;
|
|
494
|
+
const operator = ref.operator;
|
|
495
|
+
return {
|
|
496
|
+
id: network.network_code,
|
|
497
|
+
name: network.display_name,
|
|
498
|
+
status: 'active',
|
|
499
|
+
...(operator !== undefined && { operator }),
|
|
500
|
+
brand: { domain: network.adcp_publisher },
|
|
501
|
+
ctx_metadata: { network_code: network.network_code, publisher_domain: network.adcp_publisher },
|
|
502
|
+
};
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
upsert: async refs => {
|
|
506
|
+
const out: SyncAccountsResultRow[] = [];
|
|
507
|
+
for (const ref of refs) {
|
|
508
|
+
if ('account_id' in ref) {
|
|
509
|
+
out.push({
|
|
510
|
+
brand: { domain: '' },
|
|
511
|
+
operator: '',
|
|
512
|
+
action: 'failed',
|
|
513
|
+
status: 'rejected',
|
|
514
|
+
errors: [{ code: 'INVALID_REQUEST', message: 'sync_accounts requires brand+operator, not account_id' }],
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const domain = ref.brand.domain;
|
|
519
|
+
const operator = ref.operator;
|
|
520
|
+
const network = domain ? await upstream.lookupNetwork(domain) : null;
|
|
521
|
+
if (!network) {
|
|
522
|
+
out.push({
|
|
523
|
+
brand: { domain },
|
|
524
|
+
operator,
|
|
525
|
+
action: 'failed',
|
|
526
|
+
status: 'rejected',
|
|
527
|
+
errors: [{ code: 'ACCOUNT_NOT_FOUND', message: `No publisher network registered for ${domain}` }],
|
|
528
|
+
});
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
out.push({
|
|
532
|
+
account_id: network.network_code,
|
|
533
|
+
name: network.display_name,
|
|
534
|
+
brand: { domain: network.adcp_publisher },
|
|
535
|
+
operator,
|
|
536
|
+
action: 'unchanged',
|
|
537
|
+
status: 'active',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
list: async () => {
|
|
544
|
+
const items: Array<Account<NetworkMeta>> = [];
|
|
545
|
+
for (const domain of KNOWN_PUBLISHERS) {
|
|
546
|
+
const n = await upstream.lookupNetwork(domain);
|
|
547
|
+
if (!n) continue;
|
|
548
|
+
items.push({
|
|
549
|
+
id: n.network_code,
|
|
550
|
+
name: n.display_name,
|
|
551
|
+
status: 'active',
|
|
552
|
+
brand: { domain: n.adcp_publisher },
|
|
553
|
+
ctx_metadata: { network_code: n.network_code, publisher_domain: n.adcp_publisher },
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
return { items, has_more: false };
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Required: explicit `SalesCorePlatform<Meta> & SalesIngestionPlatform<Meta>`
|
|
561
|
+
// annotation per migration recipe #11 — `defineSalesPlatform` widens to
|
|
562
|
+
// all-optional and `RequiredPlatformsFor<'sales-non-guaranteed'>` requires
|
|
563
|
+
// the closed shape on the way out.
|
|
564
|
+
sales: SalesCorePlatform<NetworkMeta> & SalesIngestionPlatform<NetworkMeta> = {
|
|
565
|
+
getProducts: async (req: GetProductsRequest, ctx): Promise<GetProductsResponse> => {
|
|
566
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
567
|
+
const publisherDomain = ctx.account.ctx_metadata.publisher_domain;
|
|
568
|
+
// When the buyer provides structured filters (flight dates, budget),
|
|
569
|
+
// forward them so each product comes back with a per-query forecast.
|
|
570
|
+
// Single upstream round-trip surfaces both the catalog and the
|
|
571
|
+
// forecast curve.
|
|
572
|
+
const briefBudget = (req.filters?.budget_range as { max?: number } | undefined)?.max;
|
|
573
|
+
const products = await upstream.listProducts(networkCode, {
|
|
574
|
+
...(req.filters?.start_date && { flightStart: req.filters.start_date }),
|
|
575
|
+
...(req.filters?.end_date && { flightEnd: req.filters.end_date }),
|
|
576
|
+
...(briefBudget !== undefined && { budget: briefBudget }),
|
|
577
|
+
});
|
|
578
|
+
return { products: products.map(p => projectProduct(p, publisherDomain)) };
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Sync `create_media_buy`. Returns `media_buy_id` on the sync-success
|
|
583
|
+
* arm immediately — auction-cleared inventory has no IO-review step.
|
|
584
|
+
* `idempotency_key` round-trips to upstream `client_request_id`.
|
|
585
|
+
*/
|
|
586
|
+
createMediaBuy: async (req: CreateMediaBuyRequest, ctx) => {
|
|
587
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
588
|
+
// SWAP: production splits network_code (publisher tenant) and
|
|
589
|
+
// advertiser_id (brand seat). Mock collapses both — real DSPs/SSPs
|
|
590
|
+
// resolve advertiser_id from `req.brand` against the publisher's
|
|
591
|
+
// advertiser directory.
|
|
592
|
+
const advertiserId = networkCode;
|
|
593
|
+
|
|
594
|
+
const totalBudget =
|
|
595
|
+
req.total_budget?.amount ??
|
|
596
|
+
(req.packages ?? []).reduce((s, p) => s + ((p as { budget?: number }).budget ?? 0), 0);
|
|
597
|
+
const currency = req.total_budget?.currency ?? 'USD';
|
|
598
|
+
|
|
599
|
+
// Budget-floor enforcement happens upstream — the mock returns
|
|
600
|
+
// budget_too_low if any line item is below product.min_spend.
|
|
601
|
+
// Adopters who want to enforce client-side (skip the upstream call
|
|
602
|
+
// for an obvious reject) can iterate `req.packages` and check
|
|
603
|
+
// against a cached product catalog before calling the upstream.
|
|
604
|
+
|
|
605
|
+
const packagesRequest = (req.packages ?? []) as Array<{
|
|
606
|
+
product_id?: string;
|
|
607
|
+
budget?: number;
|
|
608
|
+
}>;
|
|
609
|
+
|
|
610
|
+
// Build the line_items array up front so the upstream POST is one
|
|
611
|
+
// round-trip. Production splits this when line-item validation needs
|
|
612
|
+
// to happen per-LI server-side; the mock accepts the array inline.
|
|
613
|
+
const lineItemsBody: Array<{ product_id: string; budget: number }> = [];
|
|
614
|
+
for (let i = 0; i < packagesRequest.length; i++) {
|
|
615
|
+
const pkg = packagesRequest[i];
|
|
616
|
+
if (!pkg) continue;
|
|
617
|
+
if (!pkg.product_id) {
|
|
618
|
+
throw new AdcpError('INVALID_REQUEST', {
|
|
619
|
+
message: `package[${i}]: product_id required`,
|
|
620
|
+
field: `packages[${i}].product_id`,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
lineItemsBody.push({ product_id: pkg.product_id, budget: pkg.budget ?? 0 });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// SWAP: pacing extraction — production reads from req.packages[i].pacing
|
|
627
|
+
// or req.delivery_settings, varying by your contract surface. AdCP 3.0.5
|
|
628
|
+
// doesn't carry an order-level `pacing` on the wire — the mock accepts
|
|
629
|
+
// it because real platforms (Meta, TTD, etc.) all expose pacing on
|
|
630
|
+
// their own non-guaranteed shape. Default 'even' if unspecified;
|
|
631
|
+
// reject typos rather than silently passing them through.
|
|
632
|
+
const PACING_VALUES = ['even', 'asap', 'front_loaded'] as const;
|
|
633
|
+
const rawPacing = (req as { pacing?: unknown }).pacing;
|
|
634
|
+
let pacing: (typeof PACING_VALUES)[number] = 'even';
|
|
635
|
+
if (typeof rawPacing === 'string') {
|
|
636
|
+
if (!(PACING_VALUES as readonly string[]).includes(rawPacing)) {
|
|
637
|
+
throw new AdcpError('INVALID_REQUEST', {
|
|
638
|
+
message: `pacing must be one of ${PACING_VALUES.join(', ')} (got: ${rawPacing})`,
|
|
639
|
+
field: 'pacing',
|
|
640
|
+
recovery: 'correctable',
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
pacing = rawPacing as (typeof PACING_VALUES)[number];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let order: UpstreamOrder;
|
|
647
|
+
try {
|
|
648
|
+
order = await upstream.createOrder(networkCode, {
|
|
649
|
+
name: `MediaBuy from ${req.brand?.domain ?? 'unknown'}`,
|
|
650
|
+
advertiser_id: advertiserId,
|
|
651
|
+
currency,
|
|
652
|
+
budget: totalBudget,
|
|
653
|
+
pacing,
|
|
654
|
+
...(req.start_time && { flight_start: req.start_time }),
|
|
655
|
+
...(req.end_time && { flight_end: req.end_time }),
|
|
656
|
+
line_items: lineItemsBody,
|
|
657
|
+
client_request_id: req.idempotency_key,
|
|
658
|
+
});
|
|
659
|
+
} catch (e) {
|
|
660
|
+
// Surface upstream-typed error bodies as AdcpError with the
|
|
661
|
+
// appropriate code. Mock returns `code: 'budget_too_low'` /
|
|
662
|
+
// `code: 'product_not_found'` / etc.; we map a few of these
|
|
663
|
+
// explicitly so adopters see typed errors at the buyer boundary.
|
|
664
|
+
if (e instanceof AdcpError) throw e;
|
|
665
|
+
throw new AdcpError('SERVICE_UNAVAILABLE', {
|
|
666
|
+
message: (e as Error).message ?? 'upstream order creation failed',
|
|
667
|
+
recovery: 'transient',
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Project the response. The upstream order carries `line_items[]`
|
|
672
|
+
// already (since we created them inline); each maps to a wire
|
|
673
|
+
// `package`.
|
|
674
|
+
const packagesOut: CreateMediaBuySuccess['packages'] = (order.line_items ?? []).map((li, i) => ({
|
|
675
|
+
package_id: li.line_item_id,
|
|
676
|
+
product_id: li.product_id,
|
|
677
|
+
budget: li.budget,
|
|
678
|
+
// Re-thread the buyer's package_id if supplied — adopters who
|
|
679
|
+
// care about preserving buyer-side ids should round-trip them
|
|
680
|
+
// here. SWAP: persist the mapping.
|
|
681
|
+
...(packagesRequest[i] !== undefined &&
|
|
682
|
+
(packagesRequest[i] as { buyer_ref?: string }).buyer_ref !== undefined && {
|
|
683
|
+
buyer_ref: (packagesRequest[i] as { buyer_ref?: string }).buyer_ref,
|
|
684
|
+
}),
|
|
685
|
+
}));
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
media_buy_id: order.order_id,
|
|
689
|
+
// pending_creatives — buy is auction-confirmed but no creatives
|
|
690
|
+
// attached yet. Buyer transitions to `active` after sync_creatives.
|
|
691
|
+
status: 'pending_creatives',
|
|
692
|
+
confirmed_at: order.created_at ?? new Date().toISOString(),
|
|
693
|
+
packages: packagesOut,
|
|
694
|
+
};
|
|
695
|
+
},
|
|
696
|
+
|
|
697
|
+
updateMediaBuy: async (id: string, patch: UpdateMediaBuyRequest, ctx): Promise<UpdateMediaBuySuccess> => {
|
|
698
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
699
|
+
// Validate existence first — production sellers PATCH the upstream
|
|
700
|
+
// order to apply pacing / budget / status changes; the worked
|
|
701
|
+
// example echoes the current state for clarity. SWAP: wire your
|
|
702
|
+
// backend's order-mutation endpoint here.
|
|
703
|
+
const existing = await upstream.getOrder(networkCode, id);
|
|
704
|
+
if (!existing) {
|
|
705
|
+
throw new AdcpError('MEDIA_BUY_NOT_FOUND', {
|
|
706
|
+
message: `media_buy ${id} not found in this seller's network`,
|
|
707
|
+
recovery: 'terminal',
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
// Validate any patch.packages reference real line items. Storyboard
|
|
711
|
+
// exercises bogus package_id and asserts PACKAGE_NOT_FOUND on the wire.
|
|
712
|
+
const patchPackages = (
|
|
713
|
+
patch as {
|
|
714
|
+
packages?: Array<{ package_id?: string; creative_assignments?: unknown[] }>;
|
|
715
|
+
}
|
|
716
|
+
).packages;
|
|
717
|
+
let hasCreativeAssignment = false;
|
|
718
|
+
if (patchPackages?.length) {
|
|
719
|
+
const lineItems = await upstream.listLineItems(networkCode, id);
|
|
720
|
+
const knownPackageIds = new Set(lineItems.map(li => li.line_item_id));
|
|
721
|
+
for (const p of patchPackages) {
|
|
722
|
+
if (p.package_id && !knownPackageIds.has(p.package_id)) {
|
|
723
|
+
throw new AdcpError('PACKAGE_NOT_FOUND', {
|
|
724
|
+
message: `Package ${p.package_id} not found in media buy ${id}`,
|
|
725
|
+
field: 'packages.package_id',
|
|
726
|
+
recovery: 'terminal',
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (Array.isArray(p.creative_assignments) && p.creative_assignments.length > 0) {
|
|
730
|
+
hasCreativeAssignment = true;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// Status advances when the buyer attaches creatives — pending_creatives
|
|
735
|
+
// → pending_start (or active if the flight already started). Production
|
|
736
|
+
// backends would also persist the assignment to the upstream line item;
|
|
737
|
+
// the worked example just advances the response state.
|
|
738
|
+
const baseStatus = mapMediaBuyStatus(existing.status);
|
|
739
|
+
let nextStatus: ReturnType<typeof mapMediaBuyStatus> =
|
|
740
|
+
adapterStatusOverrides.get(overrideKey(networkCode, id)) ?? baseStatus;
|
|
741
|
+
if (hasCreativeAssignment && nextStatus === 'pending_creatives') {
|
|
742
|
+
const flightStarted = existing.flight_start !== undefined && Date.parse(existing.flight_start) <= Date.now();
|
|
743
|
+
nextStatus = flightStarted ? 'active' : 'pending_start';
|
|
744
|
+
adapterStatusOverrides.set(overrideKey(networkCode, id), nextStatus);
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
media_buy_id: existing.order_id,
|
|
748
|
+
status: nextStatus,
|
|
749
|
+
};
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
getMediaBuyDelivery: async (req: GetMediaBuyDeliveryRequest, ctx): Promise<GetMediaBuyDeliveryResponse> => {
|
|
753
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
754
|
+
const requestedIds = req.media_buy_ids ?? [];
|
|
755
|
+
// Multi-id pass-through per #1342 contract — fan out per id; framework
|
|
756
|
+
// dev-mode warn (post-#1410) fires automatically if we accidentally
|
|
757
|
+
// truncate to ids[0]. Filter undefined results so an unknown id
|
|
758
|
+
// produces no row rather than an error (matches upstream semantics).
|
|
759
|
+
const deliveries = await Promise.all(
|
|
760
|
+
requestedIds.map(async id => {
|
|
761
|
+
const d = await upstream.getDelivery(networkCode, id);
|
|
762
|
+
if (!d) return null;
|
|
763
|
+
return {
|
|
764
|
+
media_buy_id: d.order_id,
|
|
765
|
+
currency: d.currency,
|
|
766
|
+
reporting_period: d.reporting_period,
|
|
767
|
+
totals: d.totals,
|
|
768
|
+
packages: d.line_items.map(li => ({
|
|
769
|
+
package_id: li.line_item_id,
|
|
770
|
+
product_id: li.product_id,
|
|
771
|
+
impressions: li.impressions,
|
|
772
|
+
clicks: li.clicks,
|
|
773
|
+
spend: li.spend,
|
|
774
|
+
currency: li.currency,
|
|
775
|
+
})),
|
|
776
|
+
};
|
|
777
|
+
})
|
|
778
|
+
);
|
|
779
|
+
const filtered = deliveries.filter((d): d is NonNullable<typeof d> => d !== null);
|
|
780
|
+
// Surface a debugging-friendly trace when buyers ask about ids the
|
|
781
|
+
// upstream doesn't know — silently dropping rows looks like delivery
|
|
782
|
+
// simply hasn't started yet, which buries the actual error.
|
|
783
|
+
const missing = requestedIds.filter((_, i) => deliveries[i] === null);
|
|
784
|
+
if (missing.length > 0) {
|
|
785
|
+
// eslint-disable-next-line no-console
|
|
786
|
+
console.warn(
|
|
787
|
+
`[sales-non-guaranteed] get_media_buy_delivery: ${missing.length} unknown media_buy_id(s) returned no delivery rows: ${missing.join(', ')}`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
const response: GetMediaBuyDeliveryResponse = {
|
|
791
|
+
currency: filtered[0]?.currency ?? 'USD',
|
|
792
|
+
reporting_period: {
|
|
793
|
+
start: filtered[0]?.reporting_period.start ?? new Date().toISOString(),
|
|
794
|
+
end: filtered[0]?.reporting_period.end ?? new Date().toISOString(),
|
|
795
|
+
},
|
|
796
|
+
aggregated_totals: {
|
|
797
|
+
impressions: filtered.reduce((s, d) => s + d.totals.impressions, 0),
|
|
798
|
+
spend: filtered.reduce((s, d) => s + d.totals.spend, 0),
|
|
799
|
+
clicks: filtered.reduce((s, d) => s + d.totals.clicks, 0),
|
|
800
|
+
media_buy_count: filtered.length,
|
|
801
|
+
},
|
|
802
|
+
media_buy_deliveries: filtered.map(d => ({
|
|
803
|
+
media_buy_id: d.media_buy_id,
|
|
804
|
+
// Required field on media_buy_deliveries[i]. Auction inventory in
|
|
805
|
+
// active delivery defaults to 'active'; production sellers map
|
|
806
|
+
// upstream order state through `mapMediaBuyStatus` like elsewhere.
|
|
807
|
+
status: 'active' as const,
|
|
808
|
+
totals: d.totals,
|
|
809
|
+
by_package: d.packages.map(p => ({
|
|
810
|
+
package_id: p.package_id,
|
|
811
|
+
impressions: p.impressions,
|
|
812
|
+
spend: p.spend,
|
|
813
|
+
})),
|
|
814
|
+
})),
|
|
815
|
+
};
|
|
816
|
+
return response;
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
getMediaBuys: async (req: GetMediaBuysRequest, ctx): Promise<GetMediaBuysResponse> => {
|
|
820
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
821
|
+
const requestedIds = req.media_buy_ids ?? [];
|
|
822
|
+
let orders: UpstreamOrder[];
|
|
823
|
+
if (requestedIds.length > 0) {
|
|
824
|
+
orders = (await Promise.all(requestedIds.map(id => upstream.getOrder(networkCode, id)))).filter(
|
|
825
|
+
(o): o is UpstreamOrder => o !== null
|
|
826
|
+
);
|
|
827
|
+
} else {
|
|
828
|
+
orders = await upstream.listOrders(networkCode);
|
|
829
|
+
}
|
|
830
|
+
// Fetch per-order line items. Mock's GET /v1/orders/{id} omits them;
|
|
831
|
+
// GAM-style backends similarly split Order vs LineItem services. SWAP:
|
|
832
|
+
// batch via `?include=lineitems` if your platform supports it.
|
|
833
|
+
const media_buys = await Promise.all(
|
|
834
|
+
orders.map(async o => {
|
|
835
|
+
const lineItems = await upstream.listLineItems(networkCode, o.order_id);
|
|
836
|
+
const baseStatus = mapMediaBuyStatus(o.status);
|
|
837
|
+
const status = adapterStatusOverrides.get(overrideKey(networkCode, o.order_id)) ?? baseStatus;
|
|
838
|
+
return {
|
|
839
|
+
media_buy_id: o.order_id,
|
|
840
|
+
status,
|
|
841
|
+
currency: o.currency,
|
|
842
|
+
...(o.budget !== undefined && { total_budget: o.budget }),
|
|
843
|
+
...(o.updated_at !== undefined && { confirmed_at: o.updated_at }),
|
|
844
|
+
...(o.created_at !== undefined && { created_at: o.created_at }),
|
|
845
|
+
...(o.updated_at !== undefined && { updated_at: o.updated_at }),
|
|
846
|
+
...(o.flight_start && { start_time: o.flight_start }),
|
|
847
|
+
...(o.flight_end && { end_time: o.flight_end }),
|
|
848
|
+
packages: lineItems.map(li => ({
|
|
849
|
+
package_id: li.line_item_id,
|
|
850
|
+
product_id: li.product_id,
|
|
851
|
+
budget: li.budget,
|
|
852
|
+
currency: o.currency,
|
|
853
|
+
})),
|
|
854
|
+
};
|
|
855
|
+
})
|
|
856
|
+
);
|
|
857
|
+
const response: GetMediaBuysResponse = { media_buys };
|
|
858
|
+
return response;
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
syncCreatives: async (creatives, ctx) => {
|
|
862
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
863
|
+
const advertiserId = networkCode; // SWAP: same collapse caveat as createMediaBuy.
|
|
864
|
+
const out: SyncCreativesRow[] = [];
|
|
865
|
+
for (const c of creatives) {
|
|
866
|
+
const formatRef = (c as { format_id?: { id?: string } | string }).format_id;
|
|
867
|
+
const formatId = typeof formatRef === 'string' ? formatRef : formatRef?.id;
|
|
868
|
+
if (!formatId) {
|
|
869
|
+
out.push({
|
|
870
|
+
creative_id: (c as { creative_id?: string }).creative_id ?? 'unknown',
|
|
871
|
+
action: 'failed',
|
|
872
|
+
status: 'rejected',
|
|
873
|
+
errors: [{ code: 'CREATIVE_REJECTED', message: 'format_id is required' }],
|
|
874
|
+
});
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
const creativeIdHint = (c as { creative_id?: string }).creative_id;
|
|
879
|
+
const created = await upstream.createCreative(networkCode, {
|
|
880
|
+
name: (c as { name?: string }).name ?? 'Untitled',
|
|
881
|
+
format_id: formatId,
|
|
882
|
+
advertiser_id: advertiserId,
|
|
883
|
+
...(creativeIdHint !== undefined && { client_request_id: creativeIdHint }),
|
|
884
|
+
});
|
|
885
|
+
out.push({
|
|
886
|
+
creative_id: created.creative_id,
|
|
887
|
+
action: 'created',
|
|
888
|
+
status: 'approved',
|
|
889
|
+
});
|
|
890
|
+
} catch (e) {
|
|
891
|
+
out.push({
|
|
892
|
+
creative_id: (c as { creative_id?: string }).creative_id ?? 'unknown',
|
|
893
|
+
action: 'failed',
|
|
894
|
+
status: 'rejected',
|
|
895
|
+
errors: [
|
|
896
|
+
{ code: 'CREATIVE_REJECTED', message: (e as Error).message ?? 'upstream creative creation failed' },
|
|
897
|
+
],
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return out;
|
|
902
|
+
},
|
|
903
|
+
|
|
904
|
+
listCreativeFormats: async (_req, _ctx): Promise<ListCreativeFormatsResponse> => {
|
|
905
|
+
// Publisher-owned format catalog. The mock doesn't have a discrete
|
|
906
|
+
// formats endpoint (formats live inline on Product); production sellers
|
|
907
|
+
// typically expose `/v1/formats` separately. SWAP: replace with your
|
|
908
|
+
// backend's format catalog.
|
|
909
|
+
return {
|
|
910
|
+
formats: [
|
|
911
|
+
{
|
|
912
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: 'display_300x250' },
|
|
913
|
+
name: 'Display 300x250 (medrec)',
|
|
914
|
+
renders: [{ role: 'main', dimensions: { width: 300, height: 250, unit: 'px' } }],
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: 'display_728x90' },
|
|
918
|
+
name: 'Display 728x90 (leaderboard)',
|
|
919
|
+
renders: [{ role: 'main', dimensions: { width: 728, height: 90, unit: 'px' } }],
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: 'video_30s' },
|
|
923
|
+
name: 'Video 30s outstream / instream',
|
|
924
|
+
renders: [{ role: 'main', dimensions: { width: 1920, height: 1080, unit: 'px' } }],
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: 'video_15s' },
|
|
928
|
+
name: 'Video 15s',
|
|
929
|
+
renders: [{ role: 'main', dimensions: { width: 1920, height: 1080, unit: 'px' } }],
|
|
930
|
+
},
|
|
931
|
+
],
|
|
932
|
+
};
|
|
933
|
+
},
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ---------------------------------------------------------------------------
|
|
938
|
+
// Bootstrap.
|
|
939
|
+
// ---------------------------------------------------------------------------
|
|
940
|
+
|
|
941
|
+
const platform = new SalesNonGuaranteedAdapter();
|
|
942
|
+
const idempotencyStore = createIdempotencyStore({ backend: memoryBackend(), ttlSeconds: 86_400 });
|
|
943
|
+
|
|
944
|
+
// Persist `packages[].targeting_overlay` from create_media_buy and echo it
|
|
945
|
+
// on get_media_buys. Required for any seller claiming property-lists /
|
|
946
|
+
// collection-lists. SWAP `InMemoryStateStore` for `PostgresStateStore` in
|
|
947
|
+
// production — in-memory loss after restart silently strips the echo.
|
|
948
|
+
const stateStore = new InMemoryStateStore();
|
|
949
|
+
const mediaBuyStore = createMediaBuyStore({ store: stateStore });
|
|
950
|
+
|
|
951
|
+
// ─── TEST-ONLY: comply-controller in-memory state ───────────────────────
|
|
952
|
+
// DELETE BEFORE DEPLOYING. Module-scope maps shared across accounts; only
|
|
953
|
+
// the conformance harness reaches them via the gate below.
|
|
954
|
+
const seededMediaBuys = new Map<string, { status: string; revision: number }>();
|
|
955
|
+
const simulatedDelivery = new Map<
|
|
956
|
+
string,
|
|
957
|
+
{ impressions: number; clicks: number; reported_spend: { amount: number; currency: string } }
|
|
958
|
+
>();
|
|
959
|
+
// ─── /TEST-ONLY ──────────────────────────────────────────────────────────
|
|
960
|
+
|
|
961
|
+
serve(
|
|
962
|
+
({ taskStore }) =>
|
|
963
|
+
createAdcpServerFromPlatform(platform, {
|
|
964
|
+
name: 'hello-seller-adapter-non-guaranteed',
|
|
965
|
+
version: '1.0.0',
|
|
966
|
+
taskStore,
|
|
967
|
+
idempotency: idempotencyStore,
|
|
968
|
+
mediaBuyStore,
|
|
969
|
+
resolveSessionKey: ctx => {
|
|
970
|
+
const acct = ctx.account as Account<NetworkMeta> | undefined;
|
|
971
|
+
return acct?.id ?? 'anonymous';
|
|
972
|
+
},
|
|
973
|
+
// ─── TEST-ONLY: comply_test_controller wiring ──────────────────────
|
|
974
|
+
// DELETE BEFORE DEPLOYING. The framework auto-gates on the resolved
|
|
975
|
+
// account's `mode === 'sandbox'` (see `accounts.resolve` synthesis arm
|
|
976
|
+
// above) — adapters no longer carry their own gate callback. #1435 phase 3.
|
|
977
|
+
complyTest: {
|
|
978
|
+
seed: {
|
|
979
|
+
media_buy: ({ media_buy_id, fixture }) => {
|
|
980
|
+
const existing = seededMediaBuys.get(media_buy_id);
|
|
981
|
+
const status = typeof fixture['status'] === 'string' ? (fixture['status'] as string) : 'pending_creatives';
|
|
982
|
+
seededMediaBuys.set(media_buy_id, { status, revision: existing?.revision ?? 0 });
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
force: {
|
|
986
|
+
media_buy_status: ({ media_buy_id, status, rejection_reason }) => {
|
|
987
|
+
const buy = seededMediaBuys.get(media_buy_id);
|
|
988
|
+
const previous = buy?.status ?? 'pending_creatives';
|
|
989
|
+
seededMediaBuys.set(media_buy_id, { status, revision: (buy?.revision ?? 0) + 1 });
|
|
990
|
+
void rejection_reason;
|
|
991
|
+
return { success: true, previous_state: previous, current_state: status };
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
simulate: {
|
|
995
|
+
delivery: ({ media_buy_id, impressions, clicks, reported_spend }) => {
|
|
996
|
+
const prev = simulatedDelivery.get(media_buy_id) ?? {
|
|
997
|
+
impressions: 0,
|
|
998
|
+
clicks: 0,
|
|
999
|
+
reported_spend: { amount: 0, currency: 'USD' },
|
|
1000
|
+
};
|
|
1001
|
+
simulatedDelivery.set(media_buy_id, {
|
|
1002
|
+
impressions: prev.impressions + (impressions ?? 0),
|
|
1003
|
+
clicks: prev.clicks + (clicks ?? 0),
|
|
1004
|
+
reported_spend: reported_spend ?? prev.reported_spend,
|
|
1005
|
+
});
|
|
1006
|
+
return { success: true, simulated: { media_buy_id, impressions, clicks, reported_spend } };
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
// ─── /TEST-ONLY ──────────────────────────────────────────────────
|
|
1011
|
+
}),
|
|
1012
|
+
{
|
|
1013
|
+
port: PORT,
|
|
1014
|
+
authenticate: verifyApiKey({
|
|
1015
|
+
keys: { [ADCP_AUTH_TOKEN]: { principal: 'compliance-runner' } },
|
|
1016
|
+
}),
|
|
1017
|
+
}
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
console.log(`sales-non-guaranteed adapter on http://127.0.0.1:${PORT}/mcp · upstream: ${UPSTREAM_URL}`);
|