@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,618 @@
1
+ "use strict";
2
+ /**
3
+ * `creative-ad-server` upstream-shape mock-server. Stateful creative
4
+ * library (POST writes, GET reads), tag generation with macro substitution,
5
+ * synth delivery reporting. Closes #1459 (sub-issue of #1381).
6
+ *
7
+ * Pattern source:
8
+ * - Library state shape: `sales-guaranteed/server.ts` handleCreateCreative /
9
+ * handleListCreatives — network-scoped, idempotency on client_request_id.
10
+ * - Format catalog projection: `creative-template/server.ts` (templates
11
+ * in seed-data, projected at request-time).
12
+ *
13
+ * Specialism deltas vs `creative-template`:
14
+ * - Stateful library (writes persist across calls).
15
+ * - Format auto-detection from upload mime (handleCreateCreative).
16
+ * - Tag generation (POST /v1/creatives/{id}/render) substitutes macros
17
+ * into a stored snippet template — `{click_url}`, `{impression_pixel}`,
18
+ * `{cb}`, `{advertiser_id}`, etc.
19
+ * - Real `/serve/{creative_id}` HTML response — adopters get a true
20
+ * iframe-embeddable URL on previewCreative, not a synthetic string.
21
+ * - Delivery reporting (GET /v1/creatives/{id}/delivery) — synth
22
+ * impressions/clicks/CTR scaled by days-active, deterministic-seeded
23
+ * on (creative_id, day).
24
+ * - Multi-tenancy via X-Network-Code header (mirrors sales-guaranteed).
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.projectFormat = void 0;
28
+ exports.bootCreativeAdServer = bootCreativeAdServer;
29
+ const node_http_1 = require("node:http");
30
+ const node_crypto_1 = require("node:crypto");
31
+ const seed_data_1 = require("./seed-data");
32
+ Object.defineProperty(exports, "projectFormat", { enumerable: true, get: function () { return seed_data_1.projectFormat; } });
33
+ const PAGE_SIZE_DEFAULT = 50;
34
+ async function bootCreativeAdServer(options) {
35
+ const apiKey = options.apiKey ?? seed_data_1.DEFAULT_API_KEY;
36
+ const networks = options.networks ?? seed_data_1.NETWORKS;
37
+ const formats = options.formats ?? seed_data_1.FORMATS;
38
+ const seededCreatives = options.creatives ?? seed_data_1.CREATIVES;
39
+ const creatives = new Map();
40
+ for (const seed of seededCreatives) {
41
+ creatives.set(seed.creative_id, {
42
+ ...seed,
43
+ body_fingerprint: sha256(JSON.stringify(seed)),
44
+ });
45
+ }
46
+ // Idempotency table — keyed `<network_code>::creative::<client_request_id>`.
47
+ const idempotency = new Map();
48
+ // Traffic counters keyed by `<METHOD> <route-template>`. Harness queries
49
+ // `GET /_debug/traffic` after the storyboard run and asserts headline
50
+ // routes were hit ≥1. Façade adapters that skip the upstream produce
51
+ // zero counters and fail the assertion.
52
+ const traffic = new Map();
53
+ const bump = (routeTemplate) => {
54
+ traffic.set(routeTemplate, (traffic.get(routeTemplate) ?? 0) + 1);
55
+ };
56
+ const server = (0, node_http_1.createServer)((req, res) => {
57
+ handleRequest(req, res).catch(err => {
58
+ writeJson(res, 500, { code: 'internal_error', message: err?.message ?? 'unexpected error' });
59
+ });
60
+ });
61
+ await new Promise((resolve, reject) => {
62
+ server.once('error', reject);
63
+ server.listen(options.port, '127.0.0.1', () => {
64
+ server.removeListener('error', reject);
65
+ resolve();
66
+ });
67
+ });
68
+ const addr = server.address();
69
+ const boundPort = typeof addr === 'object' && addr ? addr.port : options.port;
70
+ const url = `http://127.0.0.1:${boundPort}`;
71
+ return {
72
+ url,
73
+ close: () => new Promise((resolve, reject) => {
74
+ server.close(err => (err ? reject(err) : resolve()));
75
+ }),
76
+ };
77
+ async function handleRequest(req, res) {
78
+ const reqUrl = new URL(req.url ?? '/', url);
79
+ const path = reqUrl.pathname;
80
+ const method = req.method ?? 'GET';
81
+ // Façade-detection traffic dump — harness-only, no auth required.
82
+ if (method === 'GET' && path === '/_debug/traffic') {
83
+ writeJson(res, 200, { traffic: Object.fromEntries(traffic) });
84
+ return;
85
+ }
86
+ // Network discovery — no auth (happens before tenant context is known).
87
+ if (method === 'GET' && path === '/_lookup/network') {
88
+ bump('GET /_lookup/network');
89
+ const adcpPublisher = reqUrl.searchParams.get('adcp_publisher');
90
+ if (!adcpPublisher) {
91
+ writeJson(res, 400, { code: 'invalid_request', message: 'adcp_publisher query parameter is required.' });
92
+ return;
93
+ }
94
+ const match = networks.find(n => n.adcp_publisher === adcpPublisher);
95
+ if (!match) {
96
+ writeJson(res, 404, {
97
+ code: 'network_not_found',
98
+ message: `No upstream network registered for adcp_publisher=${adcpPublisher}.`,
99
+ });
100
+ return;
101
+ }
102
+ writeJson(res, 200, {
103
+ adcp_publisher: match.adcp_publisher,
104
+ network_code: match.network_code,
105
+ display_name: match.display_name,
106
+ });
107
+ return;
108
+ }
109
+ // /serve/{creative_id} — real HTML response. No bearer auth: this is
110
+ // the URL real ad servers expose to publisher iframes; gating it on
111
+ // bearer tokens would defeat the test-mode CDN-style pattern. The
112
+ // creative_id itself is the capability — leak prevention is whoever
113
+ // gets the URL gets the render. Production servers of course gate
114
+ // this through signed URLs / referrer checks.
115
+ const serveMatch = path.match(/^\/serve\/([^/]+)$/);
116
+ if (method === 'GET' && serveMatch && serveMatch[1]) {
117
+ bump('GET /serve/{id}');
118
+ const creativeId = decodeURIComponent(serveMatch[1]);
119
+ const cr = creatives.get(creativeId);
120
+ if (!cr) {
121
+ res.writeHead(404, { 'Content-Type': 'text/html' });
122
+ res.end('<!doctype html><title>Not found</title>');
123
+ return;
124
+ }
125
+ const ctxParam = reqUrl.searchParams.get('ctx') ?? '';
126
+ const html = renderServeHtml(cr, formats, url, ctxParam);
127
+ res.writeHead(200, { 'Content-Type': 'text/html' });
128
+ res.end(html);
129
+ return;
130
+ }
131
+ // Auth gate for /v1/* surface.
132
+ const auth = req.headers['authorization'];
133
+ if (!auth || !auth.startsWith('Bearer ') || auth.slice(7) !== apiKey) {
134
+ writeJson(res, 401, { code: 'unauthorized', message: 'Missing or invalid bearer credential.' });
135
+ return;
136
+ }
137
+ const networkHeader = req.headers['x-network-code'];
138
+ const networkCode = Array.isArray(networkHeader) ? networkHeader[0] : networkHeader;
139
+ if (!networkCode) {
140
+ writeJson(res, 403, { code: 'network_required', message: 'X-Network-Code header is required on every request.' });
141
+ return;
142
+ }
143
+ const network = networks.find(n => n.network_code === networkCode);
144
+ if (!network) {
145
+ writeJson(res, 403, { code: 'unknown_network', message: `Unknown network: ${networkCode}` });
146
+ return;
147
+ }
148
+ if (method === 'GET' && path === '/v1/formats') {
149
+ bump('GET /v1/formats');
150
+ const filtered = formats.filter(f => f.network_code === network.network_code);
151
+ writeJson(res, 200, {
152
+ formats: filtered.map(f => ({
153
+ format_id: f.format_id,
154
+ name: f.name,
155
+ channel: f.channel,
156
+ render_kind: f.render_kind,
157
+ ...(f.width !== undefined && { width: f.width }),
158
+ ...(f.height !== undefined && { height: f.height }),
159
+ ...(f.duration_seconds !== undefined && { duration_seconds: f.duration_seconds }),
160
+ accepted_mimes: f.accepted_mimes,
161
+ })),
162
+ });
163
+ return;
164
+ }
165
+ if (method === 'GET' && path === '/v1/creatives') {
166
+ bump('GET /v1/creatives');
167
+ return handleListCreatives(reqUrl, network, res);
168
+ }
169
+ if (method === 'POST' && path === '/v1/creatives') {
170
+ bump('POST /v1/creatives');
171
+ return handleCreateCreative(req, network, res);
172
+ }
173
+ const creativeMatch = path.match(/^\/v1\/creatives\/([^/]+)(\/.*)?$/);
174
+ if (creativeMatch && creativeMatch[1]) {
175
+ const creativeId = decodeURIComponent(creativeMatch[1]);
176
+ const subPath = creativeMatch[2] ?? '/';
177
+ const creative = creatives.get(creativeId);
178
+ if (!creative || creative.network_code !== network.network_code) {
179
+ writeJson(res, 404, { code: 'creative_not_found', message: `Creative ${creativeId} not found.` });
180
+ return;
181
+ }
182
+ if (method === 'GET' && subPath === '/') {
183
+ bump('GET /v1/creatives/{id}');
184
+ writeJson(res, 200, stripFingerprint(creative));
185
+ return;
186
+ }
187
+ if (method === 'PATCH' && subPath === '/') {
188
+ bump('PATCH /v1/creatives/{id}');
189
+ return handleUpdateCreative(req, creative, res);
190
+ }
191
+ if (method === 'POST' && subPath === '/render') {
192
+ bump('POST /v1/creatives/{id}/render');
193
+ return handleRenderCreative(req, creative, res);
194
+ }
195
+ if (method === 'GET' && subPath === '/delivery') {
196
+ bump('GET /v1/creatives/{id}/delivery');
197
+ return handleGetDelivery(reqUrl, creative, res);
198
+ }
199
+ }
200
+ writeJson(res, 404, { code: 'not_found', message: `No route for ${method} ${path}` });
201
+ }
202
+ // ────────────────────────────────────────────────────────────
203
+ // Creatives library
204
+ // ────────────────────────────────────────────────────────────
205
+ function handleListCreatives(reqUrl, network, res) {
206
+ let visible = Array.from(creatives.values()).filter(c => c.network_code === network.network_code);
207
+ const advertiserId = reqUrl.searchParams.get('advertiser_id');
208
+ if (advertiserId)
209
+ visible = visible.filter(c => c.advertiser_id === advertiserId);
210
+ const formatId = reqUrl.searchParams.get('format_id');
211
+ if (formatId)
212
+ visible = visible.filter(c => c.format_id === formatId);
213
+ const status = reqUrl.searchParams.get('status');
214
+ if (status)
215
+ visible = visible.filter(c => c.status === status);
216
+ const createdAfter = reqUrl.searchParams.get('created_after');
217
+ if (createdAfter)
218
+ visible = visible.filter(c => c.created_at >= createdAfter);
219
+ const creativeIdsParam = reqUrl.searchParams.get('creative_ids');
220
+ if (creativeIdsParam) {
221
+ const ids = new Set(creativeIdsParam.split(','));
222
+ visible = visible.filter(c => ids.has(c.creative_id));
223
+ }
224
+ visible.sort((a, b) => a.created_at.localeCompare(b.created_at));
225
+ // Pagination cursor — opaque base64 of the next-row created_at.
226
+ const limit = Math.min(Math.max(parsePositiveNumber(reqUrl.searchParams.get('limit')) ?? PAGE_SIZE_DEFAULT, 1), 200);
227
+ const cursor = reqUrl.searchParams.get('cursor');
228
+ let startIdx = 0;
229
+ if (cursor) {
230
+ try {
231
+ const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
232
+ startIdx = visible.findIndex(c => c.created_at > decoded);
233
+ if (startIdx < 0)
234
+ startIdx = visible.length;
235
+ }
236
+ catch {
237
+ writeJson(res, 400, { code: 'invalid_cursor', message: 'cursor is not a valid pagination token.' });
238
+ return;
239
+ }
240
+ }
241
+ const page = visible.slice(startIdx, startIdx + limit);
242
+ const next = visible[startIdx + limit];
243
+ const nextCursor = next ? Buffer.from(page[page.length - 1]?.created_at ?? '', 'utf8').toString('base64url') : null;
244
+ writeJson(res, 200, {
245
+ creatives: page.map(stripFingerprint),
246
+ ...(nextCursor && { next_cursor: nextCursor }),
247
+ });
248
+ }
249
+ async function handleCreateCreative(req, network, res) {
250
+ const body = await readJsonObject(req, res);
251
+ if (!body)
252
+ return;
253
+ const name = typeof body.name === 'string' ? body.name : null;
254
+ const advertiserId = typeof body.advertiser_id === 'string' ? body.advertiser_id : null;
255
+ const explicitFormatId = typeof body.format_id === 'string' ? body.format_id : null;
256
+ const snippet = typeof body.snippet === 'string' ? body.snippet : undefined;
257
+ const clickUrl = typeof body.click_url === 'string' ? body.click_url : undefined;
258
+ const uploadMime = typeof body.upload_mime === 'string' ? body.upload_mime : undefined;
259
+ const widthHint = typeof body.width === 'number' ? body.width : undefined;
260
+ const heightHint = typeof body.height === 'number' ? body.height : undefined;
261
+ const clientRequestId = typeof body.client_request_id === 'string' ? body.client_request_id : undefined;
262
+ if (!name || !advertiserId) {
263
+ writeJson(res, 400, { code: 'invalid_request', message: 'name and advertiser_id are required.' });
264
+ return;
265
+ }
266
+ // Format auto-detection when format_id isn't supplied. Sniff by mime
267
+ // type + dimensions hint. Real ad servers use richer detection
268
+ // (binary header inspection, codec sniff for video); this is the
269
+ // minimum to demonstrate the auto-detect surface.
270
+ let format;
271
+ if (explicitFormatId) {
272
+ format = formats.find(f => f.format_id === explicitFormatId && f.network_code === network.network_code);
273
+ if (!format) {
274
+ writeJson(res, 404, {
275
+ code: 'format_not_found',
276
+ message: `Format ${explicitFormatId} not found on network ${network.network_code}.`,
277
+ });
278
+ return;
279
+ }
280
+ }
281
+ else if (uploadMime) {
282
+ const candidates = formats.filter(f => f.network_code === network.network_code && f.accepted_mimes.includes(uploadMime));
283
+ // Prefer dimension-matched fixed format when hint provided.
284
+ if (widthHint !== undefined && heightHint !== undefined) {
285
+ format = candidates.find(c => c.width === widthHint && c.height === heightHint);
286
+ }
287
+ if (!format)
288
+ format = candidates[0];
289
+ if (!format) {
290
+ writeJson(res, 422, {
291
+ code: 'format_auto_detect_failed',
292
+ message: `Could not auto-detect format for upload_mime=${uploadMime}; pass an explicit format_id.`,
293
+ field: 'format_id',
294
+ });
295
+ return;
296
+ }
297
+ }
298
+ else {
299
+ writeJson(res, 400, {
300
+ code: 'invalid_request',
301
+ message: 'either format_id or upload_mime is required.',
302
+ field: 'format_id',
303
+ });
304
+ return;
305
+ }
306
+ const fingerprint = sha256(JSON.stringify({ name, advertiserId, formatId: format.format_id, snippet, clickUrl }));
307
+ if (clientRequestId) {
308
+ const key = `${network.network_code}::creative::${clientRequestId}`;
309
+ const existing = idempotency.get(key);
310
+ if (existing) {
311
+ const existingCr = creatives.get(existing);
312
+ if (existingCr) {
313
+ if (existingCr.body_fingerprint !== fingerprint) {
314
+ writeJson(res, 409, {
315
+ code: 'idempotency_conflict',
316
+ message: `client_request_id ${clientRequestId} previously used for a different body.`,
317
+ });
318
+ return;
319
+ }
320
+ writeJson(res, 200, { ...stripFingerprint(existingCr), replayed: true });
321
+ return;
322
+ }
323
+ }
324
+ }
325
+ // Allow caller-supplied `creative_id` override — real ad servers
326
+ // reject this (the platform owns the namespace), but cascade-test
327
+ // seeders need to write under a known id so storyboard fixtures
328
+ // can reference them by alias. Production sellers ship without this
329
+ // override path.
330
+ //
331
+ // SECURITY GATES:
332
+ // 1. Env allowlist — `MOCK_ALLOW_CREATIVE_ID_OVERRIDE=1` is required.
333
+ // Default-off prevents an adopter who forks this mock and points it
334
+ // at a public host from accepting buyer-supplied ids. The compliance
335
+ // runner sets it via the seeder env; production never does.
336
+ // 2. Cross-tenant collision check — if the id already exists on a
337
+ // DIFFERENT network, refuse with 409. Without this, tenant B can
338
+ // POST `creative_id: <tenant-A-id>` and overwrite tenant A's record;
339
+ // reads stay network-gated but `/serve/{id}` is auth-free, so the
340
+ // overwrite would be served as canonical.
341
+ const overrideAllowed = typeof body.creative_id === 'string' && process.env['MOCK_ALLOW_CREATIVE_ID_OVERRIDE'] === '1';
342
+ const explicitCreativeId = overrideAllowed ? body.creative_id : null;
343
+ if (explicitCreativeId) {
344
+ const existing = creatives.get(explicitCreativeId);
345
+ if (existing && existing.network_code !== network.network_code) {
346
+ writeJson(res, 409, {
347
+ code: 'creative_id_conflict',
348
+ message: `creative_id ${explicitCreativeId} already exists on a different network; cannot overwrite cross-tenant.`,
349
+ field: 'creative_id',
350
+ });
351
+ return;
352
+ }
353
+ }
354
+ const creativeId = explicitCreativeId ?? `cr_${(0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 16)}`;
355
+ const now = new Date().toISOString();
356
+ const cr = {
357
+ creative_id: creativeId,
358
+ network_code: network.network_code,
359
+ advertiser_id: advertiserId,
360
+ format_id: format.format_id,
361
+ name,
362
+ ...(snippet !== undefined && { snippet }),
363
+ ...(clickUrl !== undefined && { click_url: clickUrl }),
364
+ status: 'active',
365
+ created_at: now,
366
+ updated_at: now,
367
+ body_fingerprint: fingerprint,
368
+ };
369
+ creatives.set(creativeId, cr);
370
+ if (clientRequestId) {
371
+ idempotency.set(`${network.network_code}::creative::${clientRequestId}`, creativeId);
372
+ }
373
+ writeJson(res, 201, stripFingerprint(cr));
374
+ }
375
+ async function handleUpdateCreative(req, cr, res) {
376
+ const body = await readJsonObject(req, res);
377
+ if (!body)
378
+ return;
379
+ if (typeof body.snippet === 'string')
380
+ cr.snippet = body.snippet;
381
+ if (typeof body.click_url === 'string')
382
+ cr.click_url = body.click_url;
383
+ if (typeof body.name === 'string')
384
+ cr.name = body.name;
385
+ if (typeof body.status === 'string') {
386
+ const allowed = ['active', 'paused', 'archived', 'rejected'];
387
+ if (!allowed.includes(body.status)) {
388
+ writeJson(res, 400, { code: 'invalid_status', message: `status must be one of ${allowed.join(', ')}.` });
389
+ return;
390
+ }
391
+ cr.status = body.status;
392
+ }
393
+ cr.updated_at = new Date().toISOString();
394
+ cr.body_fingerprint = sha256(JSON.stringify({
395
+ name: cr.name,
396
+ advertiserId: cr.advertiser_id,
397
+ formatId: cr.format_id,
398
+ snippet: cr.snippet,
399
+ clickUrl: cr.click_url,
400
+ }));
401
+ writeJson(res, 200, stripFingerprint(cr));
402
+ }
403
+ // ────────────────────────────────────────────────────────────
404
+ // Tag generation — macro substitution
405
+ // ────────────────────────────────────────────────────────────
406
+ async function handleRenderCreative(req, cr, res) {
407
+ const body = await readJsonObject(req, res);
408
+ if (!body)
409
+ return;
410
+ const ctx = isObject(body.context) ? body.context : {};
411
+ const format = formats.find(f => f.format_id === cr.format_id && f.network_code === cr.network_code);
412
+ if (!format) {
413
+ writeJson(res, 500, {
414
+ code: 'format_not_found',
415
+ message: `Creative ${cr.creative_id} references unknown format ${cr.format_id}.`,
416
+ });
417
+ return;
418
+ }
419
+ const template = cr.snippet ?? format.snippet_template;
420
+ // POST /render returns substituted HTML in the response body; the buyer
421
+ // is expected to apply their own context (signed clickthroughs, viewability
422
+ // shims) before serving. Escape values defensively because the same
423
+ // bytes can land in a publisher's iframe via `tag_url` if the buyer
424
+ // skips the secondary binding step.
425
+ const substituted = substituteMacros(template, cr, format, ctx, true);
426
+ const tagUrl = `${url}/serve/${encodeURIComponent(cr.creative_id)}?ctx=${encodeURIComponent(serializeCtx(ctx))}`;
427
+ writeJson(res, 200, {
428
+ creative_id: cr.creative_id,
429
+ format_id: cr.format_id,
430
+ tag_html: substituted,
431
+ tag_url: tagUrl,
432
+ preview_url: tagUrl,
433
+ });
434
+ }
435
+ // ────────────────────────────────────────────────────────────
436
+ // Delivery — synth impressions/clicks scaled by days-active
437
+ // ────────────────────────────────────────────────────────────
438
+ function handleGetDelivery(reqUrl, cr, res) {
439
+ const startStr = reqUrl.searchParams.get('start');
440
+ const endStr = reqUrl.searchParams.get('end');
441
+ const now = Date.now();
442
+ const start = startStr ? Date.parse(startStr) : Date.parse(cr.created_at);
443
+ const end = endStr ? Date.parse(endStr) : now;
444
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
445
+ writeJson(res, 400, { code: 'invalid_request', message: 'start/end must be ISO 8601 with end ≥ start.' });
446
+ return;
447
+ }
448
+ const format = formats.find(f => f.format_id === cr.format_id && f.network_code === cr.network_code);
449
+ const channel = format?.channel ?? 'display';
450
+ // Per-format CTR baselines (industry-typical):
451
+ // display ~0.10%, video ~1.5%, ctv ~3%, audio ~0.5%
452
+ const ctrBaseline = channel === 'video' ? 0.015 : channel === 'ctv' ? 0.03 : channel === 'audio' ? 0.005 : 0.001;
453
+ const days = Math.max(1, Math.ceil((end - start) / (24 * 60 * 60 * 1000)));
454
+ const breakdown = [];
455
+ let totalImpressions = 0;
456
+ let totalClicks = 0;
457
+ for (let d = 0; d < days; d++) {
458
+ const dayStart = new Date(start + d * 24 * 60 * 60 * 1000);
459
+ const dateIso = dayStart.toISOString().slice(0, 10);
460
+ const seedHex = sha256(`${cr.creative_id}::${dateIso}`).slice(0, 8);
461
+ const seed = parseInt(seedHex, 16);
462
+ // Deterministic pseudo-random impressions in [10_000, 100_000].
463
+ const impressions = 10_000 + (seed % 90_001);
464
+ const clicks = Math.round(impressions * ctrBaseline);
465
+ breakdown.push({ date: dateIso, impressions, clicks });
466
+ totalImpressions += impressions;
467
+ totalClicks += clicks;
468
+ }
469
+ writeJson(res, 200, {
470
+ creative_id: cr.creative_id,
471
+ reporting_period: {
472
+ start: new Date(start).toISOString(),
473
+ end: new Date(end).toISOString(),
474
+ },
475
+ totals: {
476
+ impressions: totalImpressions,
477
+ clicks: totalClicks,
478
+ ctr: totalImpressions > 0 ? totalClicks / totalImpressions : 0,
479
+ },
480
+ breakdown,
481
+ });
482
+ }
483
+ }
484
+ // ────────────────────────────────────────────────────────────
485
+ // Helpers
486
+ // ────────────────────────────────────────────────────────────
487
+ function writeJson(res, status, body) {
488
+ res.writeHead(status, { 'Content-Type': 'application/json' });
489
+ res.end(JSON.stringify(body));
490
+ }
491
+ async function readJsonObject(req, res) {
492
+ const text = await new Promise((resolve, reject) => {
493
+ const chunks = [];
494
+ req.on('data', c => chunks.push(c));
495
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
496
+ req.on('error', reject);
497
+ });
498
+ if (!text)
499
+ return {};
500
+ try {
501
+ const parsed = JSON.parse(text);
502
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
503
+ writeJson(res, 400, { code: 'invalid_request', message: 'request body must be a JSON object.' });
504
+ return null;
505
+ }
506
+ return parsed;
507
+ }
508
+ catch {
509
+ writeJson(res, 400, { code: 'invalid_request', message: 'request body is not valid JSON.' });
510
+ return null;
511
+ }
512
+ }
513
+ function isObject(v) {
514
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
515
+ }
516
+ function parsePositiveNumber(s) {
517
+ if (s === null)
518
+ return undefined;
519
+ const n = Number(s);
520
+ return Number.isFinite(n) && n > 0 ? n : undefined;
521
+ }
522
+ function sha256(s) {
523
+ return (0, node_crypto_1.createHash)('sha256').update(s).digest('hex');
524
+ }
525
+ function stripFingerprint(cr) {
526
+ const { body_fingerprint: _bf, ...rest } = cr;
527
+ void _bf;
528
+ return rest;
529
+ }
530
+ /** Substitute `{macro}` placeholders into the template.
531
+ *
532
+ * When `htmlEscapeValues` is true, every macro value is HTML-escaped before
533
+ * substitution. The raw-substitution path (htmlEscapeValues=false) returns
534
+ * values verbatim — used by `POST /v1/creatives/{id}/render` callers who
535
+ * apply their own context binding (signed clickthroughs, viewability shims).
536
+ * The escape path is used by `GET /serve/{id}` because that endpoint emits
537
+ * the substituted HTML directly to the browser with no caller intervention,
538
+ * so caller-controlled `ctx` values (e.g. `click_url`) must NOT be able to
539
+ * break out of attribute context and inject `<script>`.
540
+ */
541
+ function substituteMacros(template, cr, format, ctx, htmlEscapeValues) {
542
+ const click = typeof ctx['click_url'] === 'string' ? ctx['click_url'] : (cr.click_url ?? 'https://example.com/click');
543
+ const assetUrl = typeof ctx['asset_url'] === 'string'
544
+ ? ctx['asset_url']
545
+ : 'https://test-assets.adcontextprotocol.org/placeholder.jpg';
546
+ const impressionPixel = typeof ctx['impression_pixel'] === 'string'
547
+ ? ctx['impression_pixel']
548
+ : `https://imp.example/i?cr=${encodeURIComponent(cr.creative_id)}`;
549
+ const cb = typeof ctx['cb'] === 'string' ? ctx['cb'] : String(Date.now());
550
+ const macros = {
551
+ click_url: String(click),
552
+ asset_url: String(assetUrl),
553
+ impression_pixel: String(impressionPixel),
554
+ cb: String(cb),
555
+ advertiser_id: cr.advertiser_id,
556
+ creative_id: cr.creative_id,
557
+ width: String(format.width ?? 0),
558
+ height: String(format.height ?? 0),
559
+ duration_seconds: String(format.duration_seconds ?? 0),
560
+ };
561
+ return template.replace(/\{(\w+)\}/g, (m, key) => {
562
+ const value = macros[key];
563
+ if (value === undefined)
564
+ return m;
565
+ return htmlEscapeValues ? escapeHtml(value) : value;
566
+ });
567
+ }
568
+ function serializeCtx(ctx) {
569
+ // Compact deterministic serialization for the /serve URL — sorted
570
+ // keys + JSON. Real ad servers use a binary keyed lookup; this is
571
+ // human-debuggable for storyboard output.
572
+ const sorted = {};
573
+ for (const k of Object.keys(ctx).sort())
574
+ sorted[k] = ctx[k];
575
+ return JSON.stringify(sorted);
576
+ }
577
+ function renderServeHtml(cr, formats, baseUrl, ctxRaw) {
578
+ const format = formats.find(f => f.format_id === cr.format_id && f.network_code === cr.network_code);
579
+ let ctx = {};
580
+ try {
581
+ if (ctxRaw)
582
+ ctx = JSON.parse(ctxRaw);
583
+ }
584
+ catch {
585
+ // best-effort — treat as empty ctx
586
+ }
587
+ const template = cr.snippet ?? format?.snippet_template ?? '';
588
+ // Escape macro values when rendering for the auth-free /serve endpoint.
589
+ // Caller-controlled `ctx` (click_url, impression_pixel, etc.) flows
590
+ // straight into attribute context; without escaping, a value like
591
+ // `"><script>...</script>` breaks out and executes under this origin.
592
+ const body = format ? substituteMacros(template, cr, format, ctx, true) : template;
593
+ return `<!doctype html>
594
+ <html><head><meta charset="utf-8"><title>${escapeHtml(cr.name)} — preview</title></head>
595
+ <body style="margin:0;padding:0">
596
+ <!-- creative_id=${escapeHtml(cr.creative_id)} format_id=${escapeHtml(cr.format_id)} served from ${escapeHtml(baseUrl)} -->
597
+ ${body}
598
+ </body></html>`;
599
+ }
600
+ function escapeHtml(s) {
601
+ return s.replace(/[&<>"']/g, c => {
602
+ switch (c) {
603
+ case '&':
604
+ return '&amp;';
605
+ case '<':
606
+ return '&lt;';
607
+ case '>':
608
+ return '&gt;';
609
+ case '"':
610
+ return '&quot;';
611
+ case "'":
612
+ return '&#39;';
613
+ default:
614
+ return c;
615
+ }
616
+ });
617
+ }
618
+ //# sourceMappingURL=server.js.map