@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.
Files changed (166) hide show
  1. package/bin/adcp.js +15 -3
  2. package/dist/lib/adapters/derived-account-store.d.ts +152 -0
  3. package/dist/lib/adapters/derived-account-store.d.ts.map +1 -0
  4. package/dist/lib/adapters/derived-account-store.js +135 -0
  5. package/dist/lib/adapters/derived-account-store.js.map +1 -0
  6. package/dist/lib/adapters/implicit-account-store.d.ts +3 -1
  7. package/dist/lib/adapters/implicit-account-store.d.ts.map +1 -1
  8. package/dist/lib/adapters/implicit-account-store.js +3 -1
  9. package/dist/lib/adapters/implicit-account-store.js.map +1 -1
  10. package/dist/lib/adapters/index.d.ts +1 -0
  11. package/dist/lib/adapters/index.d.ts.map +1 -1
  12. package/dist/lib/adapters/index.js +7 -1
  13. package/dist/lib/adapters/index.js.map +1 -1
  14. package/dist/lib/adapters/oauth-passthrough-resolver.d.ts +3 -1
  15. package/dist/lib/adapters/oauth-passthrough-resolver.d.ts.map +1 -1
  16. package/dist/lib/adapters/oauth-passthrough-resolver.js +3 -1
  17. package/dist/lib/adapters/oauth-passthrough-resolver.js.map +1 -1
  18. package/dist/lib/adapters/roster-account-store.d.ts +85 -24
  19. package/dist/lib/adapters/roster-account-store.d.ts.map +1 -1
  20. package/dist/lib/adapters/roster-account-store.js +52 -30
  21. package/dist/lib/adapters/roster-account-store.js.map +1 -1
  22. package/dist/lib/index.d.ts +3 -3
  23. package/dist/lib/index.d.ts.map +1 -1
  24. package/dist/lib/index.js +13 -8
  25. package/dist/lib/index.js.map +1 -1
  26. package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts +81 -0
  27. package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts.map +1 -0
  28. package/dist/lib/mock-server/creative-ad-server/seed-data.js +200 -0
  29. package/dist/lib/mock-server/creative-ad-server/seed-data.js.map +1 -0
  30. package/dist/lib/mock-server/creative-ad-server/server.d.ts +39 -0
  31. package/dist/lib/mock-server/creative-ad-server/server.d.ts.map +1 -0
  32. package/dist/lib/mock-server/creative-ad-server/server.js +618 -0
  33. package/dist/lib/mock-server/creative-ad-server/server.js.map +1 -0
  34. package/dist/lib/mock-server/index.d.ts.map +1 -1
  35. package/dist/lib/mock-server/index.js +180 -24
  36. package/dist/lib/mock-server/index.js.map +1 -1
  37. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts +66 -0
  38. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts.map +1 -0
  39. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js +193 -0
  40. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js.map +1 -0
  41. package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts +33 -0
  42. package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts.map +1 -0
  43. package/dist/lib/mock-server/sales-non-guaranteed/server.js +782 -0
  44. package/dist/lib/mock-server/sales-non-guaranteed/server.js.map +1 -0
  45. package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts +50 -0
  46. package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts.map +1 -0
  47. package/dist/lib/mock-server/sponsored-intelligence/seed-data.js +133 -0
  48. package/dist/lib/mock-server/sponsored-intelligence/seed-data.js.map +1 -0
  49. package/dist/lib/mock-server/sponsored-intelligence/server.d.ts +13 -0
  50. package/dist/lib/mock-server/sponsored-intelligence/server.d.ts.map +1 -0
  51. package/dist/lib/mock-server/sponsored-intelligence/server.js +609 -0
  52. package/dist/lib/mock-server/sponsored-intelligence/server.js.map +1 -0
  53. package/dist/lib/protocols/mcp.d.ts.map +1 -1
  54. package/dist/lib/protocols/mcp.js +1 -41
  55. package/dist/lib/protocols/mcp.js.map +1 -1
  56. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  57. package/dist/lib/server/account-mode.d.ts +113 -0
  58. package/dist/lib/server/account-mode.d.ts.map +1 -0
  59. package/dist/lib/server/account-mode.js +125 -0
  60. package/dist/lib/server/account-mode.js.map +1 -0
  61. package/dist/lib/server/adcp-server.js +41 -0
  62. package/dist/lib/server/adcp-server.js.map +1 -1
  63. package/dist/lib/server/auth.d.ts +35 -0
  64. package/dist/lib/server/auth.d.ts.map +1 -1
  65. package/dist/lib/server/auth.js.map +1 -1
  66. package/dist/lib/server/create-adcp-server.d.ts +26 -9
  67. package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
  68. package/dist/lib/server/create-adcp-server.js +46 -20
  69. package/dist/lib/server/create-adcp-server.js.map +1 -1
  70. package/dist/lib/server/ctx-metadata/store.d.ts +1 -1
  71. package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -1
  72. package/dist/lib/server/ctx-metadata/store.js +1 -0
  73. package/dist/lib/server/ctx-metadata/store.js.map +1 -1
  74. package/dist/lib/server/decisioning/account.d.ts +5 -0
  75. package/dist/lib/server/decisioning/account.d.ts.map +1 -1
  76. package/dist/lib/server/decisioning/account.js.map +1 -1
  77. package/dist/lib/server/decisioning/buyer-agent.d.ts +37 -4
  78. package/dist/lib/server/decisioning/buyer-agent.d.ts.map +1 -1
  79. package/dist/lib/server/decisioning/buyer-agent.js +12 -2
  80. package/dist/lib/server/decisioning/buyer-agent.js.map +1 -1
  81. package/dist/lib/server/decisioning/compose.d.ts +33 -2
  82. package/dist/lib/server/decisioning/compose.d.ts.map +1 -1
  83. package/dist/lib/server/decisioning/compose.js +13 -46
  84. package/dist/lib/server/decisioning/compose.js.map +1 -1
  85. package/dist/lib/server/decisioning/index.d.ts +2 -1
  86. package/dist/lib/server/decisioning/index.d.ts.map +1 -1
  87. package/dist/lib/server/decisioning/index.js +2 -1
  88. package/dist/lib/server/decisioning/index.js.map +1 -1
  89. package/dist/lib/server/decisioning/platform-helpers.d.ts +18 -0
  90. package/dist/lib/server/decisioning/platform-helpers.d.ts.map +1 -1
  91. package/dist/lib/server/decisioning/platform-helpers.js +20 -0
  92. package/dist/lib/server/decisioning/platform-helpers.js.map +1 -1
  93. package/dist/lib/server/decisioning/platform.d.ts +19 -21
  94. package/dist/lib/server/decisioning/platform.d.ts.map +1 -1
  95. package/dist/lib/server/decisioning/platform.js.map +1 -1
  96. package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
  97. package/dist/lib/server/decisioning/runtime/from-platform.js +334 -44
  98. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  99. package/dist/lib/server/decisioning/runtime/observed-modes.d.ts +40 -0
  100. package/dist/lib/server/decisioning/runtime/observed-modes.d.ts.map +1 -0
  101. package/dist/lib/server/decisioning/runtime/observed-modes.js +82 -0
  102. package/dist/lib/server/decisioning/runtime/observed-modes.js.map +1 -0
  103. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +2 -2
  104. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -1
  105. package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -1
  106. package/dist/lib/server/decisioning/runtime/validate-platform.js +9 -1
  107. package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -1
  108. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts +125 -0
  109. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts.map +1 -0
  110. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js +52 -0
  111. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js.map +1 -0
  112. package/dist/lib/server/decisioning/tenant-registry.d.ts +16 -0
  113. package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -1
  114. package/dist/lib/server/decisioning/tenant-registry.js.map +1 -1
  115. package/dist/lib/server/index.d.ts +4 -1
  116. package/dist/lib/server/index.d.ts.map +1 -1
  117. package/dist/lib/server/index.js +9 -3
  118. package/dist/lib/server/index.js.map +1 -1
  119. package/dist/lib/server/serve.js +20 -2
  120. package/dist/lib/server/serve.js.map +1 -1
  121. package/dist/lib/server/test-controller.d.ts +3 -0
  122. package/dist/lib/server/test-controller.d.ts.map +1 -1
  123. package/dist/lib/server/test-controller.js +23 -20
  124. package/dist/lib/server/test-controller.js.map +1 -1
  125. package/dist/lib/testing/comply-controller.d.ts +23 -2
  126. package/dist/lib/testing/comply-controller.d.ts.map +1 -1
  127. package/dist/lib/testing/comply-controller.js +19 -2
  128. package/dist/lib/testing/comply-controller.js.map +1 -1
  129. package/dist/lib/testing/index.d.ts +1 -1
  130. package/dist/lib/testing/index.d.ts.map +1 -1
  131. package/dist/lib/testing/index.js.map +1 -1
  132. package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
  133. package/dist/lib/testing/storyboard/validations.js +36 -54
  134. package/dist/lib/testing/storyboard/validations.js.map +1 -1
  135. package/dist/lib/testing/test-controller.d.ts +10 -4
  136. package/dist/lib/testing/test-controller.d.ts.map +1 -1
  137. package/dist/lib/testing/test-controller.js +9 -3
  138. package/dist/lib/testing/test-controller.js.map +1 -1
  139. package/dist/lib/types/index.d.ts +3 -2
  140. package/dist/lib/types/index.d.ts.map +1 -1
  141. package/dist/lib/types/index.js +3 -0
  142. package/dist/lib/types/index.js.map +1 -1
  143. package/dist/lib/utils/glob.d.ts +4 -2
  144. package/dist/lib/utils/glob.d.ts.map +1 -1
  145. package/dist/lib/utils/glob.js +4 -2
  146. package/dist/lib/utils/glob.js.map +1 -1
  147. package/dist/lib/version.d.ts +3 -3
  148. package/dist/lib/version.js +3 -3
  149. package/docs/llms.txt +2 -2
  150. package/examples/README.md +29 -13
  151. package/examples/hello-cluster.ts +62 -23
  152. package/examples/hello_creative_adapter_ad_server.ts +790 -0
  153. package/examples/hello_seller_adapter_guaranteed.ts +80 -22
  154. package/examples/hello_seller_adapter_non_guaranteed.ts +1020 -0
  155. package/examples/hello_si_adapter_brand.ts +572 -0
  156. package/package.json +3 -2
  157. package/skills/build-creative-agent/SKILL.md +103 -183
  158. package/skills/build-generative-seller-agent/SKILL.md +15 -9
  159. package/skills/build-governance-agent/SKILL.md +20 -11
  160. package/skills/build-retail-media-agent/SKILL.md +10 -8
  161. package/skills/build-seller-agent/SKILL.md +15 -13
  162. package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +9 -31
  163. package/skills/build-si-agent/SKILL.md +251 -196
  164. package/skills/build-signals-agent/SKILL.md +2 -0
  165. package/skills/call-adcp-agent/SKILL.md +7 -1
  166. 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}`);