@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,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hello_creative_adapter_ad_server — worked starting point for an
|
|
3
|
+
* AdCP creative agent (specialism `creative-ad-server`) that wraps an
|
|
4
|
+
* upstream stateful creative library + tag generation platform.
|
|
5
|
+
*
|
|
6
|
+
* Closes #1460 (sub-issue of #1381 hello-adapter-family completion).
|
|
7
|
+
* Closest neighbor: `hello_creative_adapter_template.ts`. The structural
|
|
8
|
+
* delta is *additive*: promote `CreativeBuilderPlatform` to
|
|
9
|
+
* `CreativeAdServerPlatform` (adds `listCreatives` + `getCreativeDelivery`),
|
|
10
|
+
* stateful `syncCreatives` library, replace template-driven `buildCreative`
|
|
11
|
+
* with tag-generation flow against a stored snippet template.
|
|
12
|
+
*
|
|
13
|
+
* Headline behavior:
|
|
14
|
+
* - `buildCreative` pulls a stored creative from the upstream library by
|
|
15
|
+
* id, calls `POST /v1/creatives/{id}/render` for macro substitution,
|
|
16
|
+
* and returns a `BuildCreativeSuccess` with `tag_url` pointing at a
|
|
17
|
+
* real iframe-embeddable `/serve/{id}` URL.
|
|
18
|
+
* - `previewCreative` (no-account tool) returns the same `tag_url` as a
|
|
19
|
+
* `preview_url` — adopters get a true URL they can iframe into the
|
|
20
|
+
* storyboard's preview pane.
|
|
21
|
+
* - `syncCreatives` writes to the upstream library; idempotency_key
|
|
22
|
+
* round-trips to upstream `client_request_id`.
|
|
23
|
+
* - `listCreatives` reads with cursor pagination, multi-id pass-through.
|
|
24
|
+
* - `getCreativeDelivery` synthesizes per-creative impressions/clicks.
|
|
25
|
+
*
|
|
26
|
+
* Fork this. Replace `upstream` with calls to your real backend. The
|
|
27
|
+
* AdCP-facing platform methods stay the same.
|
|
28
|
+
*
|
|
29
|
+
* FORK CHECKLIST
|
|
30
|
+
* 1. Replace every `// SWAP:` marker with calls to your backend.
|
|
31
|
+
* 2. Replace `KNOWN_PUBLISHERS` with your tenant directory.
|
|
32
|
+
* 3. Replace the `accounts.resolve(undefined)` fallback with the workspace
|
|
33
|
+
* tied to your principal (the env-driven default is a multi-tenant
|
|
34
|
+
* footgun in production; see recipe #11 in 6.7 migration guide).
|
|
35
|
+
* 4. Replace `projectFormat()` defaults with the format catalog your
|
|
36
|
+
* platform actually exposes (closed-shape `Format.renders[]` per #1325).
|
|
37
|
+
* 5. Validate: `node --test test/examples/hello-creative-adapter-ad-server.test.js`
|
|
38
|
+
* 6. **DELETE the `// TEST-ONLY` blocks** before deploying:
|
|
39
|
+
* - sandbox-arm in `accounts.resolve` (resolves storyboard runner's
|
|
40
|
+
* synthetic `{publisher: …, sandbox: true}` refs to a known network
|
|
41
|
+
* and stamps `mode: 'sandbox'` so the framework gate admits the
|
|
42
|
+
* comply controller)
|
|
43
|
+
* - `complyTest:` config block (seeds storyboard creatives via
|
|
44
|
+
* `comply_test_controller`)
|
|
45
|
+
*
|
|
46
|
+
* Demo:
|
|
47
|
+
* npx @adcp/sdk@latest mock-server creative-ad-server --port 4452
|
|
48
|
+
* UPSTREAM_URL=http://127.0.0.1:4452 \
|
|
49
|
+
* npx tsx examples/hello_creative_adapter_ad_server.ts
|
|
50
|
+
* adcp storyboard run http://127.0.0.1:3008/mcp creative_ad_server \
|
|
51
|
+
* --auth sk_harness_do_not_use_in_prod
|
|
52
|
+
* curl http://127.0.0.1:4452/_debug/traffic
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import {
|
|
56
|
+
AdcpError,
|
|
57
|
+
createAdcpServerFromPlatform,
|
|
58
|
+
createIdempotencyStore,
|
|
59
|
+
createUpstreamHttpClient,
|
|
60
|
+
memoryBackend,
|
|
61
|
+
serve,
|
|
62
|
+
verifyApiKey,
|
|
63
|
+
type AccountStore,
|
|
64
|
+
type Account,
|
|
65
|
+
type CreativeAdServerPlatform,
|
|
66
|
+
type DecisioningPlatform,
|
|
67
|
+
type SyncCreativesRow,
|
|
68
|
+
} from '@adcp/sdk/server';
|
|
69
|
+
import type {
|
|
70
|
+
BuildCreativeRequest,
|
|
71
|
+
BuildCreativeSuccess,
|
|
72
|
+
CreativeAsset,
|
|
73
|
+
GetCreativeDeliveryRequest,
|
|
74
|
+
GetCreativeDeliveryResponse,
|
|
75
|
+
ListCreativeFormatsResponse,
|
|
76
|
+
ListCreativesResponse,
|
|
77
|
+
PreviewCreativeRequest,
|
|
78
|
+
PreviewCreativeResponse,
|
|
79
|
+
} from '@adcp/sdk/types';
|
|
80
|
+
|
|
81
|
+
const UPSTREAM_URL = process.env['UPSTREAM_URL'] ?? 'http://127.0.0.1:4452';
|
|
82
|
+
const UPSTREAM_API_KEY = process.env['UPSTREAM_API_KEY'] ?? 'mock_creative_ad_server_key_do_not_use_in_prod';
|
|
83
|
+
const PORT = Number(process.env['PORT'] ?? 3008);
|
|
84
|
+
const ADCP_AUTH_TOKEN = process.env['ADCP_AUTH_TOKEN'] ?? 'sk_harness_do_not_use_in_prod';
|
|
85
|
+
const PUBLIC_AGENT_URL = process.env['PUBLIC_AGENT_URL'] ?? `http://127.0.0.1:${PORT}`;
|
|
86
|
+
|
|
87
|
+
const KNOWN_PUBLISHERS = ['creative-network.example', 'acmeoutdoor.example', 'pinnacle-agency.example'];
|
|
88
|
+
const SANDBOX_ID_PREFIX = 'sandbox_';
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Upstream client — SWAP for production.
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
interface UpstreamNetwork {
|
|
95
|
+
network_code: string;
|
|
96
|
+
display_name: string;
|
|
97
|
+
adcp_publisher: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface UpstreamFormat {
|
|
101
|
+
format_id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
channel: 'display' | 'video' | 'ctv' | 'audio';
|
|
104
|
+
render_kind: 'fixed' | 'parameterized';
|
|
105
|
+
width?: number;
|
|
106
|
+
height?: number;
|
|
107
|
+
duration_seconds?: number;
|
|
108
|
+
accepted_mimes: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface UpstreamCreative {
|
|
112
|
+
creative_id: string;
|
|
113
|
+
network_code: string;
|
|
114
|
+
advertiser_id: string;
|
|
115
|
+
format_id: string;
|
|
116
|
+
name: string;
|
|
117
|
+
snippet?: string;
|
|
118
|
+
click_url?: string;
|
|
119
|
+
status: 'active' | 'paused' | 'archived' | 'rejected';
|
|
120
|
+
created_at: string;
|
|
121
|
+
updated_at: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface UpstreamRenderResponse {
|
|
125
|
+
creative_id: string;
|
|
126
|
+
format_id: string;
|
|
127
|
+
tag_html: string;
|
|
128
|
+
tag_url: string;
|
|
129
|
+
preview_url: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface UpstreamDelivery {
|
|
133
|
+
creative_id: string;
|
|
134
|
+
reporting_period: { start: string; end: string };
|
|
135
|
+
totals: { impressions: number; clicks: number; ctr: number };
|
|
136
|
+
breakdown: Array<{ date: string; impressions: number; clicks: number }>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const http = createUpstreamHttpClient({
|
|
140
|
+
baseUrl: UPSTREAM_URL,
|
|
141
|
+
auth: { kind: 'static_bearer', token: UPSTREAM_API_KEY },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const networkHeader = (networkCode: string): Record<string, string> => ({ 'X-Network-Code': networkCode });
|
|
145
|
+
|
|
146
|
+
const upstream = {
|
|
147
|
+
async lookupNetwork(publisherDomain: string): Promise<UpstreamNetwork | null> {
|
|
148
|
+
const { body } = await http.get<UpstreamNetwork>('/_lookup/network', { adcp_publisher: publisherDomain });
|
|
149
|
+
return body;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async listFormats(networkCode: string): Promise<UpstreamFormat[]> {
|
|
153
|
+
const { body } = await http.get<{ formats: UpstreamFormat[] }>(
|
|
154
|
+
'/v1/formats',
|
|
155
|
+
undefined,
|
|
156
|
+
networkHeader(networkCode)
|
|
157
|
+
);
|
|
158
|
+
return body?.formats ?? [];
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async listCreatives(
|
|
162
|
+
networkCode: string,
|
|
163
|
+
opts?: {
|
|
164
|
+
advertiser_id?: string;
|
|
165
|
+
format_id?: string;
|
|
166
|
+
status?: string;
|
|
167
|
+
created_after?: string;
|
|
168
|
+
creative_ids?: string[];
|
|
169
|
+
cursor?: string;
|
|
170
|
+
limit?: number;
|
|
171
|
+
}
|
|
172
|
+
): Promise<{ creatives: UpstreamCreative[]; next_cursor?: string }> {
|
|
173
|
+
const params: Record<string, string> = {};
|
|
174
|
+
if (opts?.advertiser_id) params['advertiser_id'] = opts.advertiser_id;
|
|
175
|
+
if (opts?.format_id) params['format_id'] = opts.format_id;
|
|
176
|
+
if (opts?.status) params['status'] = opts.status;
|
|
177
|
+
if (opts?.created_after) params['created_after'] = opts.created_after;
|
|
178
|
+
if (opts?.creative_ids?.length) params['creative_ids'] = opts.creative_ids.join(',');
|
|
179
|
+
if (opts?.cursor) params['cursor'] = opts.cursor;
|
|
180
|
+
if (opts?.limit !== undefined) params['limit'] = String(opts.limit);
|
|
181
|
+
const { body } = await http.get<{ creatives: UpstreamCreative[]; next_cursor?: string }>(
|
|
182
|
+
'/v1/creatives',
|
|
183
|
+
params,
|
|
184
|
+
networkHeader(networkCode)
|
|
185
|
+
);
|
|
186
|
+
return { creatives: body?.creatives ?? [], ...(body?.next_cursor && { next_cursor: body.next_cursor }) };
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async getCreative(networkCode: string, creativeId: string): Promise<UpstreamCreative | null> {
|
|
190
|
+
const { body } = await http.get<UpstreamCreative>(
|
|
191
|
+
`/v1/creatives/${encodeURIComponent(creativeId)}`,
|
|
192
|
+
undefined,
|
|
193
|
+
networkHeader(networkCode)
|
|
194
|
+
);
|
|
195
|
+
return body;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async createCreative(
|
|
199
|
+
networkCode: string,
|
|
200
|
+
body: {
|
|
201
|
+
name: string;
|
|
202
|
+
advertiser_id: string;
|
|
203
|
+
format_id?: string;
|
|
204
|
+
upload_mime?: string;
|
|
205
|
+
width?: number;
|
|
206
|
+
height?: number;
|
|
207
|
+
snippet?: string;
|
|
208
|
+
click_url?: string;
|
|
209
|
+
client_request_id?: string;
|
|
210
|
+
/** Caller-supplied id override — TEST-ONLY path used by the comply
|
|
211
|
+
* seeder so storyboard fixtures can reference creatives by their
|
|
212
|
+
* declared alias. Production servers don't allow this. */
|
|
213
|
+
creative_id?: string;
|
|
214
|
+
}
|
|
215
|
+
): Promise<UpstreamCreative> {
|
|
216
|
+
const r = await http.post<UpstreamCreative>('/v1/creatives', body, networkHeader(networkCode));
|
|
217
|
+
if (r.body === null) {
|
|
218
|
+
throw new AdcpError('CREATIVE_REJECTED', { message: 'upstream creative creation rejected' });
|
|
219
|
+
}
|
|
220
|
+
return r.body;
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
async renderCreative(
|
|
224
|
+
networkCode: string,
|
|
225
|
+
creativeId: string,
|
|
226
|
+
context: Record<string, unknown>
|
|
227
|
+
): Promise<UpstreamRenderResponse> {
|
|
228
|
+
const r = await http.post<UpstreamRenderResponse>(
|
|
229
|
+
`/v1/creatives/${encodeURIComponent(creativeId)}/render`,
|
|
230
|
+
{ context },
|
|
231
|
+
networkHeader(networkCode)
|
|
232
|
+
);
|
|
233
|
+
if (r.body === null) {
|
|
234
|
+
throw new AdcpError('INVALID_REQUEST', { message: 'upstream render returned no body' });
|
|
235
|
+
}
|
|
236
|
+
return r.body;
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async getDelivery(
|
|
240
|
+
networkCode: string,
|
|
241
|
+
creativeId: string,
|
|
242
|
+
range: { start?: string; end?: string }
|
|
243
|
+
): Promise<UpstreamDelivery | null> {
|
|
244
|
+
const params: Record<string, string> = {};
|
|
245
|
+
if (range.start) params['start'] = range.start;
|
|
246
|
+
if (range.end) params['end'] = range.end;
|
|
247
|
+
const { body } = await http.get<UpstreamDelivery>(
|
|
248
|
+
`/v1/creatives/${encodeURIComponent(creativeId)}/delivery`,
|
|
249
|
+
params,
|
|
250
|
+
networkHeader(networkCode)
|
|
251
|
+
);
|
|
252
|
+
return body;
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// AdCP-side adapter — typed against CreativeAdServerPlatform.
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
interface NetworkMeta {
|
|
261
|
+
network_code: string;
|
|
262
|
+
publisher_domain: string;
|
|
263
|
+
[key: string]: unknown;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const FORMAT_AGENT_URL = PUBLIC_AGENT_URL;
|
|
267
|
+
|
|
268
|
+
/** Project upstream format → AdCP `Format` shape with closed-shape
|
|
269
|
+
* `renders[]` per #1325. Adapter consumes `GET /v1/formats` then
|
|
270
|
+
* projects each entry. */
|
|
271
|
+
function projectFormat(f: UpstreamFormat): ListCreativeFormatsResponse['formats'][number] {
|
|
272
|
+
// Display fixed formats: emit dimensions on a single 'main' render.
|
|
273
|
+
if (f.render_kind === 'fixed' && f.channel === 'display' && f.width !== undefined && f.height !== undefined) {
|
|
274
|
+
return {
|
|
275
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: f.format_id },
|
|
276
|
+
name: f.name,
|
|
277
|
+
renders: [
|
|
278
|
+
{
|
|
279
|
+
role: 'main',
|
|
280
|
+
dimensions: { width: f.width, height: f.height, unit: 'px' as const },
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
// Video / CTV: project at 1080p baseline.
|
|
286
|
+
if (f.render_kind === 'fixed' && (f.channel === 'video' || f.channel === 'ctv')) {
|
|
287
|
+
return {
|
|
288
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: f.format_id },
|
|
289
|
+
name: f.name,
|
|
290
|
+
renders: [
|
|
291
|
+
{
|
|
292
|
+
role: 'main',
|
|
293
|
+
dimensions: { width: 1920, height: 1080, unit: 'px' as const },
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Parameterized — placeholder dimensions for the worked example. Real
|
|
299
|
+
// adopters populate `accepts_parameters[]` (typed via the
|
|
300
|
+
// `parameterizedRender(...)` builder from `@adcp/sdk` per #1325) and
|
|
301
|
+
// surface dimensions per instantiation. The 1x1 baseline keeps the
|
|
302
|
+
// wire response schema-valid (`width`/`height` are exclusiveMinimum 0).
|
|
303
|
+
return {
|
|
304
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: f.format_id },
|
|
305
|
+
name: f.name,
|
|
306
|
+
renders: [{ role: 'main', dimensions: { width: 1, height: 1, unit: 'px' as const } }],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Project upstream creative → AdCP `CreativeAsset`. The list-creatives
|
|
311
|
+
* response schema requires `creative_id`, `name`, `format_id`, `status`,
|
|
312
|
+
* `created_date`, `updated_date`. We wrap the upstream snippet as a single
|
|
313
|
+
* inline html asset so adopters see how `assets` is keyed; production
|
|
314
|
+
* sellers project the upstream's structured asset graph (image_url,
|
|
315
|
+
* video_url, click_url, headline, etc.) here. */
|
|
316
|
+
function projectCreative(c: UpstreamCreative): CreativeAsset {
|
|
317
|
+
return {
|
|
318
|
+
creative_id: c.creative_id,
|
|
319
|
+
name: c.name,
|
|
320
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: c.format_id },
|
|
321
|
+
status: mapCreativeStatus(c.status),
|
|
322
|
+
created_date: c.created_at,
|
|
323
|
+
updated_date: c.updated_at,
|
|
324
|
+
assets: {
|
|
325
|
+
...(c.snippet !== undefined && {
|
|
326
|
+
snippet: { asset_type: 'html', content: c.snippet },
|
|
327
|
+
}),
|
|
328
|
+
...(c.click_url !== undefined && {
|
|
329
|
+
click_url: { asset_type: 'url', url: c.click_url },
|
|
330
|
+
}),
|
|
331
|
+
},
|
|
332
|
+
// CAST: schema-pickiness — `CreativeAsset` requires `assets` keys to
|
|
333
|
+
// satisfy the discriminated `AssetVariant` union. The `html` and `url`
|
|
334
|
+
// variants we emit ARE valid (asset_type: 'html' + content; asset_type:
|
|
335
|
+
// 'url' + url), but TS's structural inference can't prove it through
|
|
336
|
+
// the spread. Adopters who project richer asset graphs can drop the cast.
|
|
337
|
+
} as unknown as CreativeAsset;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function mapCreativeStatus(s: UpstreamCreative['status']): 'approved' | 'pending_review' | 'rejected' {
|
|
341
|
+
if (s === 'rejected') return 'rejected';
|
|
342
|
+
// active / paused / archived all surface as approved for the buyer's view —
|
|
343
|
+
// pause/archive are seller-side library hygiene, not buyer review state.
|
|
344
|
+
return 'approved';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
class CreativeAdServerAdapter implements DecisioningPlatform<Record<string, never>, NetworkMeta> {
|
|
348
|
+
capabilities = {
|
|
349
|
+
specialisms: ['creative-ad-server'] as const,
|
|
350
|
+
channels: ['display', 'olv', 'ctv'] as const,
|
|
351
|
+
pricingModels: [] as const,
|
|
352
|
+
config: {},
|
|
353
|
+
// Empty discovery block — `complyTest:` config below wires the seed.creative
|
|
354
|
+
// handler. The framework auto-derives the `scenarios` projection from the
|
|
355
|
+
// supplied adapters, so the explicit `scenarios: [...]` list isn't needed.
|
|
356
|
+
compliance_testing: {},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
accounts: AccountStore<NetworkMeta> = {
|
|
360
|
+
resolve: async ref => {
|
|
361
|
+
// No-account tools (`previewCreative`, `listCreativeFormats`) hand
|
|
362
|
+
// `undefined` to resolve. Return the default-listing network so
|
|
363
|
+
// the format catalog query has tenant context.
|
|
364
|
+
if (!ref) {
|
|
365
|
+
const network = await upstream.lookupNetwork(KNOWN_PUBLISHERS[0] ?? 'creative-network.example');
|
|
366
|
+
if (!network) return null;
|
|
367
|
+
return {
|
|
368
|
+
id: network.network_code,
|
|
369
|
+
name: network.display_name,
|
|
370
|
+
status: 'active',
|
|
371
|
+
brand: { domain: network.adcp_publisher },
|
|
372
|
+
ctx_metadata: { network_code: network.network_code, publisher_domain: network.adcp_publisher },
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if ('account_id' in ref) {
|
|
376
|
+
// ─── TEST-ONLY: opaque account_id → sandbox network ─────────────
|
|
377
|
+
// DELETE BEFORE DEPLOYING. The storyboard runner sends
|
|
378
|
+
// `account: { account_id: 'acct_acme_creative' }` for cascade
|
|
379
|
+
// scenarios; we route any account_id to the ACME sandbox network
|
|
380
|
+
// and stamp `mode: 'sandbox'` so the framework's comply gate
|
|
381
|
+
// admits the seeder. Production sellers replace this with a real
|
|
382
|
+
// `account_id → network_code` directory persisted from
|
|
383
|
+
// `sync_accounts`, and MUST NOT stamp `mode: 'sandbox'` — that
|
|
384
|
+
// flag exists exclusively for sandboxed test traffic.
|
|
385
|
+
const network = await upstream.lookupNetwork('acmeoutdoor.example');
|
|
386
|
+
if (!network) return null;
|
|
387
|
+
return {
|
|
388
|
+
id: ref.account_id,
|
|
389
|
+
name: network.display_name,
|
|
390
|
+
status: 'active',
|
|
391
|
+
mode: 'sandbox',
|
|
392
|
+
brand: { domain: network.adcp_publisher },
|
|
393
|
+
ctx_metadata: { network_code: network.network_code, publisher_domain: network.adcp_publisher },
|
|
394
|
+
};
|
|
395
|
+
// ─── /TEST-ONLY ──────────────────────────────────────────────────
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ─── TEST-ONLY: cascade-scenario sandbox-arm ─────────────────────
|
|
399
|
+
// DELETE BEFORE DEPLOYING. Stamps `mode: 'sandbox'` to admit the
|
|
400
|
+
// framework's comply_test_controller gate (#1435 phase 3).
|
|
401
|
+
if (ref.sandbox === true) {
|
|
402
|
+
const sandboxDomain = 'acmeoutdoor.example';
|
|
403
|
+
const network = await upstream.lookupNetwork(sandboxDomain);
|
|
404
|
+
if (!network) return null;
|
|
405
|
+
return {
|
|
406
|
+
id: `${SANDBOX_ID_PREFIX}${network.network_code}`,
|
|
407
|
+
name: `Sandbox: ${network.display_name}`,
|
|
408
|
+
status: 'active',
|
|
409
|
+
mode: 'sandbox',
|
|
410
|
+
...(ref.operator !== undefined && { operator: ref.operator }),
|
|
411
|
+
brand: { domain: ref.brand?.domain ?? sandboxDomain },
|
|
412
|
+
ctx_metadata: { network_code: network.network_code, publisher_domain: network.adcp_publisher },
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// ─── /TEST-ONLY ──────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
const publisherDomain = ref.brand?.domain;
|
|
418
|
+
if (!publisherDomain) return null;
|
|
419
|
+
const network = await upstream.lookupNetwork(publisherDomain);
|
|
420
|
+
if (!network) return null;
|
|
421
|
+
return {
|
|
422
|
+
id: network.network_code,
|
|
423
|
+
name: network.display_name,
|
|
424
|
+
status: 'active',
|
|
425
|
+
...(ref.operator !== undefined && { operator: ref.operator }),
|
|
426
|
+
brand: { domain: network.adcp_publisher },
|
|
427
|
+
ctx_metadata: { network_code: network.network_code, publisher_domain: network.adcp_publisher },
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
creative: CreativeAdServerPlatform<NetworkMeta> = {
|
|
433
|
+
/**
|
|
434
|
+
* Build / retrieve creative tags. Two invocation modes per the spec:
|
|
435
|
+
* - Library lookup: `req.creative_id` references an existing creative
|
|
436
|
+
* - Inline build: `req.creative_manifest` carries a fresh asset
|
|
437
|
+
*
|
|
438
|
+
* The worked example handles library-lookup mode; inline-build registers
|
|
439
|
+
* the creative first (push to upstream) then renders. SWAP for adapters
|
|
440
|
+
* that don't support inline build to throw INVALID_REQUEST instead.
|
|
441
|
+
*/
|
|
442
|
+
buildCreative: async (req: BuildCreativeRequest, ctx): Promise<BuildCreativeSuccess> => {
|
|
443
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
444
|
+
const creativeId = (req as { creative_id?: string }).creative_id;
|
|
445
|
+
const creativeManifest = (req as { creative_manifest?: { creative_id?: string; assets?: unknown } })
|
|
446
|
+
.creative_manifest;
|
|
447
|
+
const mediaBuyId = (req as { media_buy_id?: string }).media_buy_id;
|
|
448
|
+
const packageId = (req as { package_id?: string }).package_id;
|
|
449
|
+
|
|
450
|
+
let creative: UpstreamCreative | null;
|
|
451
|
+
if (creativeId) {
|
|
452
|
+
creative = await upstream.getCreative(networkCode, creativeId);
|
|
453
|
+
if (!creative) {
|
|
454
|
+
throw new AdcpError('CREATIVE_NOT_FOUND', {
|
|
455
|
+
message: `creative ${creativeId} not found in this seller's network`,
|
|
456
|
+
field: 'creative_id',
|
|
457
|
+
recovery: 'terminal',
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
} else if (creativeManifest) {
|
|
461
|
+
// Inline-build path: register the creative first.
|
|
462
|
+
const advertiserId = ctx.account.id;
|
|
463
|
+
const targetFormatId = (req as { target_format_id?: { id?: string } }).target_format_id?.id;
|
|
464
|
+
const created = await upstream.createCreative(networkCode, {
|
|
465
|
+
name: creativeManifest.creative_id ?? 'Inline build',
|
|
466
|
+
advertiser_id: advertiserId,
|
|
467
|
+
...(targetFormatId !== undefined && { format_id: targetFormatId }),
|
|
468
|
+
});
|
|
469
|
+
creative = created;
|
|
470
|
+
} else {
|
|
471
|
+
throw new AdcpError('INVALID_REQUEST', {
|
|
472
|
+
message: 'either creative_id or creative_manifest is required',
|
|
473
|
+
recovery: 'correctable',
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Tag generation — substitutes macros into the stored snippet.
|
|
478
|
+
// Production adopters thread placement-specific context here:
|
|
479
|
+
// click_url derived from media_buy + package, impression pixel from
|
|
480
|
+
// viewability vendor, cb (cache-buster) random.
|
|
481
|
+
const ctxMacros: Record<string, unknown> = {
|
|
482
|
+
...(mediaBuyId && { media_buy_id: mediaBuyId }),
|
|
483
|
+
...(packageId && { package_id: packageId }),
|
|
484
|
+
};
|
|
485
|
+
const rendered = await upstream.renderCreative(networkCode, creative.creative_id, ctxMacros);
|
|
486
|
+
|
|
487
|
+
// BuildCreativeResponse is a oneOf — variant 0 wraps a single
|
|
488
|
+
// `creative_manifest`, variant 1 carries `creative_manifests[]`,
|
|
489
|
+
// variant 2 is errors. We always emit single. The manifest's
|
|
490
|
+
// `additionalProperties: false` constraint excludes `creative_id`
|
|
491
|
+
// and `name` from the manifest body — only `format_id`, `assets`,
|
|
492
|
+
// `rights`, `industry_identifiers`, `provenance`, `ext` are allowed.
|
|
493
|
+
// Each `assets[key]` is a discriminated AssetVariant — `asset_type`
|
|
494
|
+
// selects the matching schema (html requires `content`, etc.).
|
|
495
|
+
return {
|
|
496
|
+
creative_manifest: {
|
|
497
|
+
format_id: { agent_url: FORMAT_AGENT_URL, id: creative.format_id },
|
|
498
|
+
assets: {
|
|
499
|
+
tag: { asset_type: 'html', content: rendered.tag_html },
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
// CAST: oneOf-emitter — picking variant 0 (single creative_manifest)
|
|
503
|
+
// of the BuildCreativeResponse oneOf. Adopters keep this cast.
|
|
504
|
+
} as unknown as BuildCreativeSuccess;
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Preview-only — sandbox URL. NoAccountCtx narrow because the wire
|
|
509
|
+
* request doesn't carry `account`. Resolver synthesizes a fallback
|
|
510
|
+
* (KNOWN_PUBLISHERS[0]) so we have tenant context.
|
|
511
|
+
*/
|
|
512
|
+
previewCreative: async (req: PreviewCreativeRequest, ctx): Promise<PreviewCreativeResponse> => {
|
|
513
|
+
// No-account narrow: ctx.account is `Account<NetworkMeta> | undefined`
|
|
514
|
+
// per the type. Defensive guard per migration recipe #11.
|
|
515
|
+
const acct = ctx.account as Account<NetworkMeta> | undefined;
|
|
516
|
+
if (!acct) {
|
|
517
|
+
throw new AdcpError('INVALID_REQUEST', {
|
|
518
|
+
message: 'preview requires a default workspace; account resolution returned null',
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const networkCode = acct.ctx_metadata.network_code;
|
|
522
|
+
const creativeId = (req as { creative_id?: string }).creative_id;
|
|
523
|
+
if (!creativeId) {
|
|
524
|
+
throw new AdcpError('INVALID_REQUEST', {
|
|
525
|
+
message: 'creative_id is required for preview',
|
|
526
|
+
field: 'creative_id',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const creative = await upstream.getCreative(networkCode, creativeId);
|
|
530
|
+
if (!creative) {
|
|
531
|
+
throw new AdcpError('CREATIVE_NOT_FOUND', {
|
|
532
|
+
message: `creative ${creativeId} not found`,
|
|
533
|
+
field: 'creative_id',
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const rendered = await upstream.renderCreative(networkCode, creativeId, {});
|
|
537
|
+
return {
|
|
538
|
+
response_type: 'single',
|
|
539
|
+
previews: [
|
|
540
|
+
{
|
|
541
|
+
preview_id: `prv_${creative.creative_id}`,
|
|
542
|
+
renders: [
|
|
543
|
+
{
|
|
544
|
+
render_id: `rnd_${creative.creative_id}`,
|
|
545
|
+
preview_url: rendered.preview_url,
|
|
546
|
+
role: 'primary',
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
input: { name: 'default' },
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
553
|
+
// CAST: oneOf-emitter — picking the `single` discriminator branch
|
|
554
|
+
// of the PreviewCreativeResponse 3-way union (single | batch | variant).
|
|
555
|
+
// See SHAPE-GOTCHAS §4. Adopters keep this cast.
|
|
556
|
+
} as unknown as PreviewCreativeResponse;
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
listCreativeFormats: async (_req, _ctx): Promise<ListCreativeFormatsResponse> => {
|
|
560
|
+
// No-account tool. Use the default workspace's catalog. Production
|
|
561
|
+
// sellers expose a global format catalog or the workspace tied to
|
|
562
|
+
// the API key's principal.
|
|
563
|
+
const networkCode = NETWORK_DEFAULT_CODE;
|
|
564
|
+
const upstreamFormats = await upstream.listFormats(networkCode);
|
|
565
|
+
return { formats: upstreamFormats.map(projectFormat) };
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
syncCreatives: async (creatives: CreativeAsset[], ctx): Promise<SyncCreativesRow[]> => {
|
|
569
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
570
|
+
const advertiserId = ctx.account.id;
|
|
571
|
+
const rows: SyncCreativesRow[] = [];
|
|
572
|
+
for (const c of creatives) {
|
|
573
|
+
const opaque = c as unknown as Record<string, unknown>;
|
|
574
|
+
const fmtRef = opaque['format_id'] as { id?: string } | string | undefined;
|
|
575
|
+
const formatId = typeof fmtRef === 'string' ? fmtRef : fmtRef?.id;
|
|
576
|
+
const idempotencyHint =
|
|
577
|
+
typeof opaque['creative_id'] === 'string' ? (opaque['creative_id'] as string) : undefined;
|
|
578
|
+
// Extract upstream-specific fields the AdCP `CreativeAsset` shape
|
|
579
|
+
// doesn't carry directly. Adopters whose backend takes the structured
|
|
580
|
+
// assets[] map should project differently; this worked example wires
|
|
581
|
+
// a single inline html snippet + click_url for storyboard simplicity.
|
|
582
|
+
const snippet = typeof opaque['snippet'] === 'string' ? (opaque['snippet'] as string) : undefined;
|
|
583
|
+
const clickUrl = typeof opaque['click_url'] === 'string' ? (opaque['click_url'] as string) : undefined;
|
|
584
|
+
try {
|
|
585
|
+
const created = await upstream.createCreative(networkCode, {
|
|
586
|
+
name: typeof opaque['name'] === 'string' ? (opaque['name'] as string) : 'Untitled',
|
|
587
|
+
advertiser_id: advertiserId,
|
|
588
|
+
...(formatId !== undefined && { format_id: formatId }),
|
|
589
|
+
...(snippet !== undefined && { snippet }),
|
|
590
|
+
...(clickUrl !== undefined && { click_url: clickUrl }),
|
|
591
|
+
...(idempotencyHint !== undefined && { client_request_id: idempotencyHint }),
|
|
592
|
+
});
|
|
593
|
+
rows.push({
|
|
594
|
+
creative_id: created.creative_id,
|
|
595
|
+
action: 'created',
|
|
596
|
+
status: 'approved',
|
|
597
|
+
});
|
|
598
|
+
} catch (e) {
|
|
599
|
+
rows.push({
|
|
600
|
+
creative_id: idempotencyHint ?? 'unknown',
|
|
601
|
+
action: 'failed',
|
|
602
|
+
status: 'rejected',
|
|
603
|
+
errors: [
|
|
604
|
+
{
|
|
605
|
+
code: e instanceof AdcpError ? e.code : 'CREATIVE_REJECTED',
|
|
606
|
+
message: e instanceof Error ? e.message : 'creative sync failed',
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return rows;
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
listCreatives: async (req, ctx) => {
|
|
616
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
617
|
+
// Multi-id pass-through per #1342: `filter.creative_ids` arrays must
|
|
618
|
+
// round-trip every id, not just the first.
|
|
619
|
+
const filter = (
|
|
620
|
+
req as { filter?: { creative_ids?: string[]; advertiser_id?: string; format_id?: string; status?: string } }
|
|
621
|
+
).filter;
|
|
622
|
+
const cursor = (req as { cursor?: string }).cursor;
|
|
623
|
+
const limit = (req as { limit?: number }).limit;
|
|
624
|
+
const result = await upstream.listCreatives(networkCode, {
|
|
625
|
+
...(filter?.creative_ids?.length && { creative_ids: filter.creative_ids }),
|
|
626
|
+
...(filter?.advertiser_id && { advertiser_id: filter.advertiser_id }),
|
|
627
|
+
...(filter?.format_id && { format_id: filter.format_id }),
|
|
628
|
+
...(filter?.status && { status: filter.status }),
|
|
629
|
+
...(cursor && { cursor }),
|
|
630
|
+
...(limit !== undefined && { limit }),
|
|
631
|
+
});
|
|
632
|
+
// CAST: schema-drift — `@adcp/sdk/types`'s `ListCreativesResponse`
|
|
633
|
+
// re-export resolves to `adcp.ts`'s legacy shape (no `query_summary`),
|
|
634
|
+
// while the platform interface requires `tools.generated`'s shape
|
|
635
|
+
// (with `query_summary` + `pagination`). The Awaited<ReturnType<>>
|
|
636
|
+
// dance pulls the platform-required type so both wire shapes stay
|
|
637
|
+
// schema-valid. Drop when codegen lands on a single canonical type.
|
|
638
|
+
type R = Awaited<ReturnType<NonNullable<CreativeAdServerPlatform<NetworkMeta>['listCreatives']>>>;
|
|
639
|
+
const projected = result.creatives.map(projectCreative);
|
|
640
|
+
const response: R = {
|
|
641
|
+
query_summary: {
|
|
642
|
+
total_matching: projected.length,
|
|
643
|
+
returned: projected.length,
|
|
644
|
+
},
|
|
645
|
+
pagination: {
|
|
646
|
+
has_more: result.next_cursor !== undefined,
|
|
647
|
+
...(result.next_cursor !== undefined && { cursor: result.next_cursor }),
|
|
648
|
+
total_count: projected.length,
|
|
649
|
+
},
|
|
650
|
+
creatives: projected,
|
|
651
|
+
} as unknown as R;
|
|
652
|
+
return response;
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
getCreativeDelivery: async (req: GetCreativeDeliveryRequest, ctx): Promise<GetCreativeDeliveryResponse> => {
|
|
656
|
+
const networkCode = ctx.account.ctx_metadata.network_code;
|
|
657
|
+
// Multi-id pass-through per #1342 + #1410. `filter.creative_ids` arrays
|
|
658
|
+
// must fan out per id; truncating to ids[0] is a correctness bug the
|
|
659
|
+
// framework dev-mode warns on. When `creative_ids` is omitted, the
|
|
660
|
+
// buyer is asking for all creatives in scope — the worked example
|
|
661
|
+
// returns an empty rows list (no fan-out target) so the storyboard
|
|
662
|
+
// sees a valid response shape; production sellers with paginated
|
|
663
|
+
// libraries iterate ALL creatives in the network (potentially
|
|
664
|
+
// expensive — most platforms require an explicit `creative_ids`
|
|
665
|
+
// filter or `media_buy_ids` filter).
|
|
666
|
+
const creativeIds = (req as { filter?: { creative_ids?: string[] } }).filter?.creative_ids ?? [];
|
|
667
|
+
const start = (req as { reporting_period?: { start?: string } }).reporting_period?.start;
|
|
668
|
+
const end = (req as { reporting_period?: { end?: string } }).reporting_period?.end;
|
|
669
|
+
|
|
670
|
+
const deliveries = await Promise.all(
|
|
671
|
+
creativeIds.map(async id => {
|
|
672
|
+
const d = await upstream.getDelivery(networkCode, id, {
|
|
673
|
+
...(start !== undefined && { start }),
|
|
674
|
+
...(end !== undefined && { end }),
|
|
675
|
+
});
|
|
676
|
+
if (!d) return null;
|
|
677
|
+
return d;
|
|
678
|
+
})
|
|
679
|
+
);
|
|
680
|
+
const present = deliveries.filter((d): d is UpstreamDelivery => d !== null);
|
|
681
|
+
// Reporting-period aggregation: take min(start) / max(end) across rows
|
|
682
|
+
// so the response window covers all returned creatives. SWAP if your
|
|
683
|
+
// backend ships heterogeneous windows per creative (some platforms do
|
|
684
|
+
// — surface the per-creative window inside `creatives[i].reporting_period`).
|
|
685
|
+
const earliest = present
|
|
686
|
+
.map(d => d.reporting_period.start)
|
|
687
|
+
.reduce((a, b) => (a < b ? a : b), present[0]?.reporting_period.start ?? new Date().toISOString());
|
|
688
|
+
const latest = present
|
|
689
|
+
.map(d => d.reporting_period.end)
|
|
690
|
+
.reduce((a, b) => (a > b ? a : b), present[0]?.reporting_period.end ?? new Date().toISOString());
|
|
691
|
+
return {
|
|
692
|
+
currency: 'USD',
|
|
693
|
+
reporting_period: { start: earliest, end: latest },
|
|
694
|
+
creatives: present.map(d => ({
|
|
695
|
+
creative_id: d.creative_id,
|
|
696
|
+
impressions: d.totals.impressions,
|
|
697
|
+
clicks: d.totals.clicks,
|
|
698
|
+
})),
|
|
699
|
+
// CAST: schema-drift — same `tools.generated` vs `adcp.ts` mismatch
|
|
700
|
+
// as `listCreatives` above. Drop when codegen converges. The wire
|
|
701
|
+
// shape is correct (currency + reporting_period + creatives[]).
|
|
702
|
+
} as unknown as GetCreativeDeliveryResponse;
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Default network used by no-account tools (`listCreativeFormats`).
|
|
708
|
+
// SWAP: real platforms either expose a global format catalog or derive
|
|
709
|
+
// the listing workspace from the API key's principal — a runtime env var
|
|
710
|
+
// is a multi-tenant footgun. See migration guide §11 (NoAccountCtx).
|
|
711
|
+
const NETWORK_DEFAULT_CODE = process.env['DEFAULT_LISTING_NETWORK'] ?? 'net_creative_us';
|
|
712
|
+
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
// Bootstrap
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
|
|
717
|
+
const platform = new CreativeAdServerAdapter();
|
|
718
|
+
const idempotencyStore = createIdempotencyStore({ backend: memoryBackend(), ttlSeconds: 86_400 });
|
|
719
|
+
|
|
720
|
+
// ─── TEST-ONLY: comply_test_controller seed adapter ────────────────────
|
|
721
|
+
// DELETE BEFORE DEPLOYING. The storyboard's `controller_seeding: true`
|
|
722
|
+
// fires `seed.creative` for each fixture entry; we forward to the upstream
|
|
723
|
+
// mock's POST /v1/creatives so the seeded creative is real in the library.
|
|
724
|
+
// Production sellers ship without this — their library state is owned by
|
|
725
|
+
// their UI / API ingestion, not the comply controller.
|
|
726
|
+
//
|
|
727
|
+
// Belt-and-suspenders: the framework gates `comply_test_controller` on
|
|
728
|
+
// `account.mode === 'sandbox'` (#1435 phase 3), so this seeder only runs
|
|
729
|
+
// when the buyer's request resolved to a sandbox-stamped account. The
|
|
730
|
+
// upstream mock additionally requires `MOCK_ALLOW_CREATIVE_ID_OVERRIDE=1`
|
|
731
|
+
// before honoring the `creative_id` field below — without that env, the
|
|
732
|
+
// mock generates a fresh server-side id and the seeder's alias is dropped
|
|
733
|
+
// (storyboard fails loudly, not silently). The compliance runner sets the
|
|
734
|
+
// env via this adapter's process; production deploys never set it.
|
|
735
|
+
async function seedCreativeOnUpstream(creativeId: string, fixture: Record<string, unknown>): Promise<void> {
|
|
736
|
+
// Pick the first known network — storyboard fixtures aren't network-scoped,
|
|
737
|
+
// so we route them to the default sandbox network.
|
|
738
|
+
const network = await upstream.lookupNetwork('acmeoutdoor.example');
|
|
739
|
+
if (!network) return;
|
|
740
|
+
try {
|
|
741
|
+
await upstream.createCreative(network.network_code, {
|
|
742
|
+
name: typeof fixture['name'] === 'string' ? fixture['name'] : creativeId,
|
|
743
|
+
advertiser_id: typeof fixture['advertiser_id'] === 'string' ? fixture['advertiser_id'] : 'adv_seeded',
|
|
744
|
+
format_id:
|
|
745
|
+
typeof fixture['format_id'] === 'object' &&
|
|
746
|
+
fixture['format_id'] !== null &&
|
|
747
|
+
typeof (fixture['format_id'] as { id?: string }).id === 'string'
|
|
748
|
+
? (fixture['format_id'] as { id: string }).id
|
|
749
|
+
: 'display_300x250', // fallback when fixture omits format_id; sandbox-test only
|
|
750
|
+
client_request_id: creativeId,
|
|
751
|
+
// Pass the storyboard-declared id through — the upstream mock honors
|
|
752
|
+
// it ONLY when `MOCK_ALLOW_CREATIVE_ID_OVERRIDE=1` is set in the
|
|
753
|
+
// mock's env. Production servers reject this field outright (the
|
|
754
|
+
// platform owns the id namespace).
|
|
755
|
+
creative_id: creativeId,
|
|
756
|
+
});
|
|
757
|
+
} catch {
|
|
758
|
+
// Idempotent — already seeded is fine.
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// ─── /TEST-ONLY ────────────────────────────────────────────────────────
|
|
762
|
+
|
|
763
|
+
serve(
|
|
764
|
+
({ taskStore }) =>
|
|
765
|
+
createAdcpServerFromPlatform(platform, {
|
|
766
|
+
name: 'hello-creative-adapter-ad-server',
|
|
767
|
+
version: '1.0.0',
|
|
768
|
+
taskStore,
|
|
769
|
+
idempotency: idempotencyStore,
|
|
770
|
+
resolveSessionKey: ctx => {
|
|
771
|
+
const acct = ctx.account as Account<NetworkMeta> | undefined;
|
|
772
|
+
return acct?.id ?? 'anonymous';
|
|
773
|
+
},
|
|
774
|
+
complyTest: {
|
|
775
|
+
seed: {
|
|
776
|
+
creative: async ({ creative_id, fixture }) => {
|
|
777
|
+
await seedCreativeOnUpstream(creative_id, fixture);
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
}),
|
|
782
|
+
{
|
|
783
|
+
port: PORT,
|
|
784
|
+
authenticate: verifyApiKey({
|
|
785
|
+
keys: { [ADCP_AUTH_TOKEN]: { principal: 'compliance-runner' } },
|
|
786
|
+
}),
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
console.log(`creative-ad-server adapter on http://127.0.0.1:${PORT}/mcp · upstream: ${UPSTREAM_URL}`);
|