@adcp/sdk 14.0.0-beta.26 → 14.0.0-beta.28

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 (66) hide show
  1. package/dist/lib/core/AgentClient.d.mts +5 -1
  2. package/dist/lib/core/AgentClient.d.ts +5 -1
  3. package/dist/lib/core/SingleAgentClient.js +2 -0
  4. package/dist/lib/core/SingleAgentClient.mjs +2 -0
  5. package/dist/lib/index.d.mts +1 -0
  6. package/dist/lib/index.d.ts +1 -0
  7. package/dist/lib/index.js +2 -0
  8. package/dist/lib/index.mjs +1 -0
  9. package/dist/lib/reporting/evidence.d.mts +4 -0
  10. package/dist/lib/reporting/evidence.d.ts +4 -0
  11. package/dist/lib/reporting/evidence.js +136 -0
  12. package/dist/lib/reporting/evidence.mjs +109 -0
  13. package/dist/lib/reporting/index.d.mts +4 -0
  14. package/dist/lib/reporting/index.d.ts +4 -0
  15. package/dist/lib/reporting/index.js +45 -0
  16. package/dist/lib/reporting/index.mjs +24 -0
  17. package/dist/lib/reporting/inspection.d.mts +88 -0
  18. package/dist/lib/reporting/inspection.d.ts +88 -0
  19. package/dist/lib/reporting/inspection.js +1125 -0
  20. package/dist/lib/reporting/inspection.mjs +1094 -0
  21. package/dist/lib/reporting/reconciliation.d.mts +199 -0
  22. package/dist/lib/reporting/reconciliation.d.ts +199 -0
  23. package/dist/lib/reporting/reconciliation.js +870 -0
  24. package/dist/lib/reporting/reconciliation.mjs +844 -0
  25. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  26. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +1 -0
  27. package/dist/lib/server/decisioning/runtime/protocol-for-tool.mjs +1 -0
  28. package/dist/lib/server/wire-spec-fields.generated.d.mts +11 -1
  29. package/dist/lib/server/wire-spec-fields.generated.d.ts +11 -1
  30. package/dist/lib/server/wire-spec-fields.generated.js +10 -0
  31. package/dist/lib/server/wire-spec-fields.generated.mjs +10 -0
  32. package/dist/lib/testing/storyboard/runner.js +13 -2
  33. package/dist/lib/testing/storyboard/runner.mjs +13 -2
  34. package/dist/lib/testing/storyboard/types.d.mts +8 -0
  35. package/dist/lib/testing/storyboard/types.d.ts +8 -0
  36. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  37. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  38. package/dist/lib/types/create-media-buy.d.ts +21 -16
  39. package/dist/lib/types/get-media-buys.d.ts +21 -16
  40. package/dist/lib/types/index.d.mts +1 -0
  41. package/dist/lib/types/index.d.ts +1 -0
  42. package/dist/lib/types/list-accounts.d.ts +21 -16
  43. package/dist/lib/types/list-creatives.d.ts +21 -16
  44. package/dist/lib/types/schemas.generated.d.ts +688 -952
  45. package/dist/lib/types/schemas.generated.js +542 -515
  46. package/dist/lib/types/schemas.generated.mjs +542 -515
  47. package/dist/lib/types/sync-accounts.d.ts +21 -16
  48. package/dist/lib/types/sync-creatives.d.ts +21 -16
  49. package/dist/lib/types/tools.generated.d.mts +2 -6
  50. package/dist/lib/types/tools.generated.d.ts +2 -6
  51. package/dist/lib/utils/reporting-status-response.d.mts +2 -0
  52. package/dist/lib/utils/reporting-status-response.d.ts +2 -0
  53. package/dist/lib/utils/reporting-status-response.js +338 -0
  54. package/dist/lib/utils/reporting-status-response.mjs +319 -0
  55. package/dist/lib/utils/response-schemas.js +6 -1
  56. package/dist/lib/utils/response-schemas.mjs +6 -1
  57. package/dist/lib/validation/sync-creatives.d.mts +42 -108
  58. package/dist/lib/validation/sync-creatives.d.ts +42 -108
  59. package/dist/lib/version.d.mts +3 -3
  60. package/dist/lib/version.d.ts +3 -3
  61. package/dist/lib/version.js +3 -3
  62. package/dist/lib/version.mjs +3 -3
  63. package/docs/TYPE-SUMMARY.md +2 -2
  64. package/docs/guides/REPORTING-RECONCILIATION.md +107 -0
  65. package/docs/llms.txt +2 -2
  66. package/package.json +1 -1
@@ -0,0 +1,1094 @@
1
+ import { fileURLToPath as __adcpFileURLToPath } from "node:url";
2
+ import { dirname as __adcpDirname } from "node:path";
3
+ import { createRequire as __adcpCreateRequire } from "node:module";
4
+ const __dirname = __adcpDirname(__adcpFileURLToPath(import.meta.url));
5
+ const require2 = __adcpCreateRequire(import.meta.url);
6
+ import { createHash, timingSafeEqual } from "crypto";
7
+ import { gunzipSync } from "zlib";
8
+ import addFormats from "ajv-formats";
9
+ import { createCanonicalReferenceResolver } from "../canonical-references/index.mjs";
10
+ import { SSRF_TRANSIENT_CODES, SsrfRefusedError, ssrfSafeFetch } from "../net/ssrf-fetch.mjs";
11
+ import { parseStrictJson } from "../signing/agent-resolver/strict-json.mjs";
12
+ import { canonicalize } from "../utils/jcs.mjs";
13
+ import { findUnsafeRegexPattern } from "../v2/format-schema/regex-safety.mjs";
14
+ const Ajv2020 = require2("ajv/dist/2020");
15
+ class ReportingInspectionError extends Error {
16
+ constructor(code, message, retryable = false, details, options, observation) {
17
+ super(message, options);
18
+ this.code = code;
19
+ this.retryable = retryable;
20
+ this.details = details;
21
+ this.observation = observation;
22
+ this.name = "ReportingInspectionError";
23
+ }
24
+ code;
25
+ retryable;
26
+ details;
27
+ observation;
28
+ }
29
+ const FORBIDDEN_HTTP_CREDENTIAL_HEADERS = /* @__PURE__ */ new Set([
30
+ "connection",
31
+ "content-length",
32
+ "host",
33
+ "proxy-authorization",
34
+ "transfer-encoding"
35
+ ]);
36
+ function createHttpsReportingResourceReader(options = {}) {
37
+ return {
38
+ async read(request) {
39
+ let target;
40
+ try {
41
+ const base = new URL(request.location);
42
+ target = request.role === "manifest" ? base : new URL(request.objectRef, base);
43
+ if (base.username || base.password || target.username || target.password) {
44
+ throw inspectionError("RESOURCE_READ_FAILED", "Reporting resource URLs may not contain userinfo");
45
+ }
46
+ if (request.role === "object" && target.origin !== base.origin) {
47
+ throw inspectionError(
48
+ "RESOURCE_READ_FAILED",
49
+ "Manifest object_ref must resolve on the configured destination origin"
50
+ );
51
+ }
52
+ } catch (error) {
53
+ if (error instanceof ReportingInspectionError) throw error;
54
+ throw inspectionError("RESOURCE_READ_FAILED", "Reporting resource location is not a valid URL");
55
+ }
56
+ const headers = {};
57
+ for (const [name, value] of Object.entries(request.credential?.headers ?? {})) {
58
+ const normalized = name.toLowerCase();
59
+ if (FORBIDDEN_HTTP_CREDENTIAL_HEADERS.has(normalized)) {
60
+ throw inspectionError("RESOURCE_READ_FAILED", `Credential provider may not set the ${normalized} header`);
61
+ }
62
+ headers[normalized] = value;
63
+ }
64
+ if (Object.keys(headers).length > 0 && !request.credential?.allowedOrigins.includes(target.origin)) {
65
+ throw inspectionError("RESOURCE_READ_FAILED", "Reporting credential is not authorized for the resource origin");
66
+ }
67
+ if (options.tls && !options.tlsAllowedOrigins?.includes(target.origin)) {
68
+ throw inspectionError(
69
+ "RESOURCE_READ_FAILED",
70
+ "Reporting mTLS credential is not authorized for the resource origin"
71
+ );
72
+ }
73
+ headers.accept = request.role === "manifest" ? "application/json" : "application/octet-stream, text/csv, application/json";
74
+ try {
75
+ const response = await ssrfSafeFetch(target.href, {
76
+ method: "GET",
77
+ headers,
78
+ allowPrivateIp: options.allowPrivateIp,
79
+ timeoutMs: options.timeoutMs,
80
+ trustedFetchFn: options.trustedFetchFn,
81
+ tls: options.tls,
82
+ signal: request.signal,
83
+ maxBodyBytes: Math.min(request.maxBytes, options.maxBodyBytes ?? request.maxBytes)
84
+ });
85
+ if (response.status < 200 || response.status >= 300) {
86
+ const retryable = response.status === 408 || response.status === 429 || response.status >= 500 || request.role === "manifest" && response.status === 404;
87
+ throw inspectionError(
88
+ retryable ? "RESOURCE_NOT_READY" : "RESOURCE_READ_FAILED",
89
+ `Reporting ${request.role} read returned HTTP ${response.status}`,
90
+ retryable
91
+ );
92
+ }
93
+ return { body: response.body, contentType: response.headers["content-type"] };
94
+ } catch (error) {
95
+ if (error instanceof ReportingInspectionError) throw error;
96
+ if (error instanceof SsrfRefusedError) {
97
+ const retryable = SSRF_TRANSIENT_CODES.has(error.code) && error.code !== "body_exceeds_limit";
98
+ throw inspectionError(
99
+ error.code === "body_exceeds_limit" ? "RESOURCE_TOO_LARGE" : "RESOURCE_READ_FAILED",
100
+ "Reporting resource read was refused by the safe fetch policy",
101
+ retryable
102
+ );
103
+ }
104
+ throw inspectionError("RESOURCE_READ_FAILED", "Reporting resource read failed", true);
105
+ }
106
+ }
107
+ };
108
+ }
109
+ const DEFAULT_MAX_MANIFEST_BYTES = 1024 * 1024;
110
+ const DEFAULT_MAX_OBJECT_BYTES = 128 * 1024 * 1024;
111
+ const DEFAULT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
112
+ const DEFAULT_MAX_FILES = 1e3;
113
+ const DEFAULT_MAX_ROWS = 1e6;
114
+ const DEFAULT_MAX_ROW_BYTES = 1024 * 1024;
115
+ const DEFAULT_MAX_INSPECTION_MS = 6e4;
116
+ const DEFAULT_MAX_SCHEMA_COMPILE_MS = 1e3;
117
+ const REPORTING_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
118
+ const ENTITY_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,255}$/;
119
+ const CONTROL_TOTAL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/;
120
+ const CANONICAL_DECIMAL_PATTERN = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/;
121
+ const ISO_DURATION_PATTERN = /^P(?=\d|T)(?=.*\d)(?:\d+Y)?(?:\d+M)?(?:\d+D)?(?:T(?=\d)(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$/;
122
+ const DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
123
+ const RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
124
+ function createReportingManifestInspector(options) {
125
+ const maxInspectionMs = boundedPositiveInteger(
126
+ options.maxInspectionMs ?? DEFAULT_MAX_INSPECTION_MS,
127
+ "maxInspectionMs",
128
+ 36e5
129
+ );
130
+ const maxSchemaCompileMs = boundedPositiveInteger(
131
+ options.maxSchemaCompileMs ?? DEFAULT_MAX_SCHEMA_COMPILE_MS,
132
+ "maxSchemaCompileMs",
133
+ 6e4
134
+ );
135
+ const resolver = options.referenceResolver ?? createCanonicalReferenceResolver();
136
+ const formatDecoders = {
137
+ jsonl: decodeJsonLines,
138
+ csv: decodeCsv,
139
+ ...options.formatDecoders
140
+ };
141
+ const compressionDecoders = {
142
+ none: (body) => body,
143
+ gzip: (body, maxOutputBytes) => Uint8Array.from(gunzipSync(body, { maxOutputLength: maxOutputBytes })),
144
+ ...options.compressionDecoders
145
+ };
146
+ return async (context) => {
147
+ const deadline = Date.now() + maxInspectionMs;
148
+ const resource = context.materialization.resource;
149
+ if (!resource || resource.kind !== "manifest" || !resource.manifest_sha256 || resource.manifest_version !== "1.0") {
150
+ throw inspectionError("UNSUPPORTED_RESOURCE", "Built-in inspection requires a v1 manifest resource");
151
+ }
152
+ const credential = options.credentialProvider ? await withinInspectionDeadline(options.credentialProvider.getCredentials(context), deadline) : void 0;
153
+ const maxManifestBytes = options.maxManifestBytes ?? DEFAULT_MAX_MANIFEST_BYTES;
154
+ const manifestRead = await withinInspectionDeadline(
155
+ options.reader.read({
156
+ role: "manifest",
157
+ location: resource.location,
158
+ maxBytes: maxManifestBytes,
159
+ credential,
160
+ context,
161
+ signal: deadlineSignal(deadline)
162
+ }),
163
+ deadline
164
+ );
165
+ if (manifestRead.body.byteLength > maxManifestBytes) {
166
+ throw inspectionError("RESOURCE_TOO_LARGE", "Reporting manifest exceeds the configured byte budget");
167
+ }
168
+ const manifestDigest = sha256(manifestRead.body);
169
+ if (!constantTimeHexEqual(manifestDigest, resource.manifest_sha256)) {
170
+ throw inspectionError(
171
+ "MANIFEST_DIGEST_MISMATCH",
172
+ "Manifest bytes do not match reporting_resource.manifest_sha256"
173
+ );
174
+ }
175
+ const manifest = parseManifest(manifestRead.body);
176
+ validateManifestBinding(manifest, context);
177
+ const maxObjectBytes = options.maxObjectBytes ?? DEFAULT_MAX_OBJECT_BYTES;
178
+ const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
179
+ const maxRows = options.maxRows ?? DEFAULT_MAX_ROWS;
180
+ if (manifest.total_size_bytes > maxTotalBytes || manifest.files.some((file) => file.size_bytes > maxObjectBytes) || manifest.files.length > (options.maxFiles ?? DEFAULT_MAX_FILES) || manifest.row_count > maxRows) {
181
+ throw inspectionError(
182
+ "RESOURCE_TOO_LARGE",
183
+ "Manifest declares data beyond the configured inspection byte budget"
184
+ );
185
+ }
186
+ const schema = await resolveRowSchema(context, resolver, options.referenceAllowedOrigins, deadline);
187
+ const validateRow = compileRowSchema(schema, maxSchemaCompileMs, deadline);
188
+ const reportDefinition = await validateReportDefinition(
189
+ context,
190
+ resolver,
191
+ options.referenceAllowedOrigins,
192
+ deadline
193
+ );
194
+ const decoder = formatDecoders[manifest.format];
195
+ if (!decoder) throw inspectionError("FORMAT_UNSUPPORTED", `No decoder is configured for ${manifest.format}`);
196
+ const decompress = compressionDecoders[manifest.compression];
197
+ if (!decompress) {
198
+ throw inspectionError("COMPRESSION_UNSUPPORTED", `No decoder is configured for ${manifest.compression}`);
199
+ }
200
+ const seen = /* @__PURE__ */ new Set();
201
+ const manifestObjectRefs = new Set(manifest.files.map((file) => file.object_ref));
202
+ const evidenceKeys = /* @__PURE__ */ new Set();
203
+ for (const checksum of context.materialization.verification?.physical_checksums ?? []) {
204
+ const key = `${checksum.object_ref}\0${checksum.algorithm}`;
205
+ if (!manifestObjectRefs.has(checksum.object_ref) || evidenceKeys.has(key)) {
206
+ throw inspectionError(
207
+ "OBJECT_DIGEST_MISMATCH",
208
+ "Producer checksum evidence is not uniquely bound to a manifest object"
209
+ );
210
+ }
211
+ evidenceKeys.add(key);
212
+ }
213
+ const rows = [];
214
+ let totalBytes = 0;
215
+ let totalDecodedBytes = 0;
216
+ for (const file of manifest.files) {
217
+ if (seen.has(file.object_ref))
218
+ throw inspectionError("OBJECT_DUPLICATE", "Manifest object_ref values must be unique");
219
+ seen.add(file.object_ref);
220
+ totalBytes += file.size_bytes;
221
+ if (totalBytes > maxTotalBytes) throw inspectionError("RESOURCE_TOO_LARGE", "Manifest exceeds total byte budget");
222
+ assertInspectionDeadline(deadline);
223
+ const read = await withinInspectionDeadline(
224
+ options.reader.read({
225
+ role: "object",
226
+ location: resource.location,
227
+ objectRef: file.object_ref,
228
+ expectedSizeBytes: file.size_bytes,
229
+ maxBytes: Math.min(maxObjectBytes, file.size_bytes + 1),
230
+ credential,
231
+ context,
232
+ signal: deadlineSignal(deadline)
233
+ }),
234
+ deadline
235
+ );
236
+ if (read.body.byteLength !== file.size_bytes) {
237
+ throw inspectionError("OBJECT_SIZE_MISMATCH", `Object ${file.object_ref} size does not match the manifest`);
238
+ }
239
+ if (!constantTimeHexEqual(sha256(read.body), file.sha256)) {
240
+ throw inspectionError(
241
+ "OBJECT_DIGEST_MISMATCH",
242
+ `Object ${file.object_ref} checksum does not match the manifest`
243
+ );
244
+ }
245
+ for (const producerChecksum of context.materialization.verification?.physical_checksums?.filter(
246
+ (checksum) => checksum.object_ref === file.object_ref
247
+ ) ?? []) {
248
+ const observed = digestHex(read.body, producerChecksum.algorithm);
249
+ if (!constantTimeHexEqual(producerChecksum.value, observed) || producerChecksum.algorithm === "sha256" && !constantTimeHexEqual(producerChecksum.value, file.sha256)) {
250
+ throw inspectionError(
251
+ "OBJECT_DIGEST_MISMATCH",
252
+ `Producer checksum evidence for ${file.object_ref} does not match the delivered object`
253
+ );
254
+ }
255
+ }
256
+ let decoded;
257
+ try {
258
+ decoded = await withinInspectionDeadline(
259
+ Promise.resolve(decompress(read.body, options.maxDecodedObjectBytes ?? maxObjectBytes)),
260
+ deadline
261
+ );
262
+ } catch (error) {
263
+ if (error instanceof ReportingInspectionError) throw error;
264
+ throw inspectionError(
265
+ "MANIFEST_INVALID",
266
+ `Could not decode ${manifest.compression} object ${file.object_ref}`,
267
+ false,
268
+ error
269
+ );
270
+ }
271
+ if (decoded.byteLength > (options.maxDecodedObjectBytes ?? maxObjectBytes)) {
272
+ throw inspectionError("RESOURCE_TOO_LARGE", `Decoded object ${file.object_ref} exceeds the byte budget`);
273
+ }
274
+ totalDecodedBytes += decoded.byteLength;
275
+ if (totalDecodedBytes > (options.maxDecodedTotalBytes ?? maxTotalBytes)) {
276
+ throw inspectionError("RESOURCE_TOO_LARGE", "Decoded reporting data exceeds the aggregate byte budget");
277
+ }
278
+ let fileRows;
279
+ try {
280
+ fileRows = await withinInspectionDeadline(
281
+ Promise.resolve(
282
+ decoder(decoded, {
283
+ format: manifest.format,
284
+ objectRef: file.object_ref,
285
+ rowSchema: schema,
286
+ inspection: context,
287
+ maxRows: maxRows - rows.length
288
+ })
289
+ ),
290
+ deadline
291
+ );
292
+ } catch (error) {
293
+ if (error instanceof ReportingInspectionError) throw error;
294
+ throw inspectionError("MANIFEST_INVALID", `Could not parse ${manifest.format} object ${file.object_ref}`);
295
+ }
296
+ if (fileRows.length !== file.row_count) {
297
+ throw inspectionError(
298
+ "OBJECT_ROW_COUNT_MISMATCH",
299
+ `Object ${file.object_ref} row count does not match the manifest`
300
+ );
301
+ }
302
+ if (fileRows.length > maxRows - rows.length) {
303
+ throw inspectionError("RESOURCE_TOO_LARGE", "Reporting row count exceeds the configured inspection budget");
304
+ }
305
+ for (const row of fileRows) rows.push(row);
306
+ }
307
+ for (let index = 0; index < rows.length; index += 1) {
308
+ if (index % 256 === 0) assertInspectionDeadline(deadline);
309
+ assertBoundedJsonData(rows[index], "ROW_SCHEMA_VIOLATION");
310
+ assertIJson(rows[index]);
311
+ if (Buffer.byteLength(canonicalize(rows[index])) > (options.maxRowBytes ?? DEFAULT_MAX_ROW_BYTES)) {
312
+ throw inspectionError("RESOURCE_TOO_LARGE", `Row ${index} exceeds the configured byte budget`);
313
+ }
314
+ if (!validateRow(rows[index])) {
315
+ throw new ReportingInspectionError(
316
+ "ROW_SCHEMA_VIOLATION",
317
+ `Row ${index} does not satisfy the pinned schema`,
318
+ false,
319
+ {
320
+ row: index,
321
+ errors: sanitizeAjvErrors(validateRow.errors)
322
+ }
323
+ );
324
+ }
325
+ }
326
+ const observedTotals = options.controlTotalCalculator ? await withinInspectionDeadline(
327
+ Promise.resolve(options.controlTotalCalculator(rows, context.revision.control_totals, context)),
328
+ deadline
329
+ ) : calculateNamedControlTotals(rows, context.revision.control_totals, reportDefinition, deadline);
330
+ const expectedCanonicalDigest = context.revision.canonical_content_digest;
331
+ let canonicalContentDigest;
332
+ if (expectedCanonicalDigest && context.expected.verificationProfile === "canonical_digest") {
333
+ canonicalContentDigest = await recomputeCanonicalDigest(
334
+ rows,
335
+ context,
336
+ resolver,
337
+ options.referenceAllowedOrigins,
338
+ deadline
339
+ );
340
+ }
341
+ const observation = {
342
+ rowCount: rows.length,
343
+ controlTotals: observedTotals,
344
+ manifestSha256: manifestDigest,
345
+ canonicalContentDigest
346
+ };
347
+ if (rows.length !== manifest.row_count || rows.length !== context.revision.row_count) {
348
+ throw inspectionMismatch(
349
+ "ROW_COUNT_MISMATCH",
350
+ "Observed row count does not match manifest and revision",
351
+ observation
352
+ );
353
+ }
354
+ if (!sameControlTotals(observedTotals, manifest.control_totals) || !sameControlTotals(observedTotals, context.revision.control_totals)) {
355
+ throw inspectionMismatch(
356
+ "CONTROL_TOTAL_MISMATCH",
357
+ "Observed control totals do not match manifest and revision",
358
+ observation
359
+ );
360
+ }
361
+ if (canonicalContentDigest && expectedCanonicalDigest) {
362
+ if (!constantTimeHexEqual(canonicalContentDigest.value, expectedCanonicalDigest.value)) {
363
+ throw inspectionMismatch(
364
+ "CANONICAL_DIGEST_MISMATCH",
365
+ "Observed logical rows do not match the canonical content digest",
366
+ { ...observation, canonicalContentDigest }
367
+ );
368
+ }
369
+ }
370
+ const commitRef = typeof options.consumerCommitRef === "function" ? options.consumerCommitRef(context) : options.consumerCommitRef;
371
+ return {
372
+ rowCount: rows.length,
373
+ controlTotals: observedTotals,
374
+ canonicalContentDigest,
375
+ manifestSha256: manifestDigest,
376
+ ...commitRef ? { consumerCommitRef: commitRef } : {}
377
+ };
378
+ };
379
+ }
380
+ async function resolveRowSchema(context, resolver, allowedOrigins, deadline) {
381
+ assertAllowedReferenceOrigin(context.revision.schema_uri, allowedOrigins, "ROW_SCHEMA_FETCH_FAILED");
382
+ const result = await withinInspectionDeadline(
383
+ resolver.resolve({
384
+ uri: context.revision.schema_uri,
385
+ digest: `sha256:${context.revision.schema_sha256.toLowerCase()}`
386
+ }),
387
+ deadline
388
+ );
389
+ if (!result.ok) {
390
+ throw new ReportingInspectionError("ROW_SCHEMA_FETCH_FAILED", result.error.message, result.error.retryable, {
391
+ referenceCode: result.error.code
392
+ });
393
+ }
394
+ requireContentType(result.contentType, ["application/schema+json", "application/json"], "ROW_SCHEMA_INVALID");
395
+ const document = parsePinnedJson(result.body, "ROW_SCHEMA_INVALID");
396
+ if (!isRecord(document)) throw inspectionError("ROW_SCHEMA_INVALID", "Pinned row schema must be a JSON object");
397
+ validateLocalSchema(document);
398
+ return document;
399
+ }
400
+ function compileRowSchema(schema, maxCompileMs, deadline) {
401
+ assertInspectionDeadline(deadline);
402
+ const startedAt = Date.now();
403
+ const ajv = new Ajv2020({
404
+ strictSchema: true,
405
+ strictTypes: false,
406
+ allErrors: true,
407
+ validateSchema: true,
408
+ addUsedSchema: false
409
+ });
410
+ addFormats(ajv);
411
+ try {
412
+ const validate = ajv.compile(schema);
413
+ if (Date.now() - startedAt > maxCompileMs) {
414
+ throw inspectionError("INSPECTION_TIMEOUT", "Pinned row schema exceeded the compilation budget", true);
415
+ }
416
+ assertInspectionDeadline(deadline);
417
+ return validate;
418
+ } catch (error) {
419
+ if (error instanceof ReportingInspectionError) throw error;
420
+ throw inspectionError("ROW_SCHEMA_INVALID", "Pinned row schema could not be compiled", false, error);
421
+ }
422
+ }
423
+ function validateLocalSchema(schema) {
424
+ if (schema.$schema !== REPORTING_SCHEMA_DIALECT) {
425
+ throw inspectionError("ROW_SCHEMA_INVALID", "Row schema must declare the pinned Draft 2020-12 dialect");
426
+ }
427
+ let nodes = 0;
428
+ const refs = /* @__PURE__ */ new Set();
429
+ const walk = (value, depth) => {
430
+ if (depth > 64 || ++nodes > 1e4) throw inspectionError("ROW_SCHEMA_INVALID", "Row schema exceeds safety bounds");
431
+ if (Array.isArray(value)) {
432
+ for (const child of value) walk(child, depth + 1);
433
+ return;
434
+ }
435
+ if (!isRecord(value)) return;
436
+ if ("$dynamicRef" in value || "$dynamicAnchor" in value || "$recursiveRef" in value || "$vocabulary" in value) {
437
+ throw inspectionError("ROW_SCHEMA_INVALID", "Row schema uses an unsupported reference or vocabulary keyword");
438
+ }
439
+ if (typeof value.pattern === "string" && value.pattern.length > 1024) {
440
+ throw inspectionError("ROW_SCHEMA_INVALID", "Row schema regex exceeds the safety bound");
441
+ }
442
+ if ("$ref" in value) {
443
+ if (typeof value.$ref !== "string" || !value.$ref.startsWith("#")) {
444
+ throw inspectionError("ROW_SCHEMA_INVALID", "Row schema references must be local fragments");
445
+ }
446
+ refs.add(value.$ref);
447
+ }
448
+ for (const child of Object.values(value)) walk(child, depth + 1);
449
+ };
450
+ walk(schema, 0);
451
+ if (findUnsafeRegexPattern(schema))
452
+ throw inspectionError("ROW_SCHEMA_INVALID", "Row schema contains an unsafe regex");
453
+ rejectReferenceCycles(schema, refs);
454
+ }
455
+ function rejectReferenceCycles(schema, refs) {
456
+ const anchors = collectAnchors(schema);
457
+ const edges = /* @__PURE__ */ new Map();
458
+ for (const ref of refs) {
459
+ const target = ref === "#" ? schema : ref.startsWith("#/") ? resolveJsonPointer(schema, ref.slice(1)) : anchors.get(ref.slice(1));
460
+ if (target === void 0) throw inspectionError("ROW_SCHEMA_INVALID", `Row schema reference ${ref} is unresolved`);
461
+ const childRefs = /* @__PURE__ */ new Set();
462
+ collectRefs(target, childRefs);
463
+ edges.set(ref, childRefs);
464
+ }
465
+ const visiting = /* @__PURE__ */ new Set();
466
+ const visited = /* @__PURE__ */ new Set();
467
+ const visit = (ref) => {
468
+ if (visiting.has(ref)) throw inspectionError("ROW_SCHEMA_INVALID", "Row schema contains a reference cycle");
469
+ if (visited.has(ref)) return;
470
+ visiting.add(ref);
471
+ for (const child of edges.get(ref) ?? []) if (edges.has(child)) visit(child);
472
+ visiting.delete(ref);
473
+ visited.add(ref);
474
+ };
475
+ for (const ref of refs) visit(ref);
476
+ }
477
+ function collectAnchors(schema) {
478
+ const anchors = /* @__PURE__ */ new Map();
479
+ const walk = (value) => {
480
+ if (Array.isArray(value)) return void value.forEach(walk);
481
+ if (!isRecord(value)) return;
482
+ if ("$anchor" in value) {
483
+ if (typeof value.$anchor !== "string" || !/^[A-Za-z_][-._A-Za-z0-9]*$/.test(value.$anchor)) {
484
+ throw inspectionError("ROW_SCHEMA_INVALID", "Row schema contains an invalid local anchor");
485
+ }
486
+ if (anchors.has(value.$anchor))
487
+ throw inspectionError("ROW_SCHEMA_INVALID", "Row schema contains duplicate anchors");
488
+ anchors.set(value.$anchor, value);
489
+ }
490
+ Object.values(value).forEach(walk);
491
+ };
492
+ walk(schema);
493
+ return anchors;
494
+ }
495
+ function collectRefs(value, output) {
496
+ if (Array.isArray(value)) return void value.forEach((child) => collectRefs(child, output));
497
+ if (!isRecord(value)) return;
498
+ if (typeof value.$ref === "string") output.add(value.$ref);
499
+ for (const child of Object.values(value)) collectRefs(child, output);
500
+ }
501
+ function resolveJsonPointer(root, pointer) {
502
+ if (pointer === "") return root;
503
+ if (!pointer.startsWith("/")) return void 0;
504
+ let cursor = root;
505
+ for (const raw of pointer.slice(1).split("/")) {
506
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
507
+ if (Array.isArray(cursor)) {
508
+ if (!/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= cursor.length) return void 0;
509
+ cursor = cursor[Number(key)];
510
+ continue;
511
+ }
512
+ if (key === "__proto__" || key === "constructor" || key === "prototype" || !isRecord(cursor) || !(key in cursor)) {
513
+ return void 0;
514
+ }
515
+ cursor = cursor[key];
516
+ }
517
+ return cursor;
518
+ }
519
+ async function validateReportDefinition(context, resolver, allowedOrigins, deadline) {
520
+ assertAllowedReferenceOrigin(
521
+ context.revision.report_definition_uri,
522
+ allowedOrigins,
523
+ "REPORT_DEFINITION_FETCH_FAILED"
524
+ );
525
+ const result = await withinInspectionDeadline(
526
+ resolver.resolve({
527
+ uri: context.revision.report_definition_uri,
528
+ digest: `sha256:${context.revision.report_definition_sha256.toLowerCase()}`
529
+ }),
530
+ deadline
531
+ );
532
+ if (!result.ok) {
533
+ throw new ReportingInspectionError("REPORT_DEFINITION_FETCH_FAILED", result.error.message, result.error.retryable, {
534
+ referenceCode: result.error.code
535
+ });
536
+ }
537
+ requireContentType(
538
+ result.contentType,
539
+ ["application/vnd.adcp.reporting-definition+json"],
540
+ "REPORT_DEFINITION_INVALID"
541
+ );
542
+ const definition = parsePinnedJson(result.body, "REPORT_DEFINITION_INVALID");
543
+ if (!isValidReportDefinition(definition)) {
544
+ throw inspectionError(
545
+ "REPORT_DEFINITION_INVALID",
546
+ "Pinned report definition does not satisfy its bundled contract"
547
+ );
548
+ }
549
+ if (definition.report_definition_id !== context.revision.report_definition_id || definition.reporting_profile !== context.revision.reporting_profile) {
550
+ throw inspectionError("REPORT_DEFINITION_INVALID", "Pinned report definition does not match the revision");
551
+ }
552
+ if (context.revision.finality === "official") {
553
+ const policy = definition.finality_policies.find(
554
+ (item) => isRecord(item) && item.finality_policy_id === context.revision.finality_policy_id
555
+ );
556
+ if (!isRecord(policy) || policy.basis !== context.revision.finality_basis) {
557
+ throw inspectionError(
558
+ "REPORT_DEFINITION_INVALID",
559
+ "Revision finality is not bound by the pinned report definition"
560
+ );
561
+ }
562
+ }
563
+ return definition;
564
+ }
565
+ async function recomputeCanonicalDigest(rows, context, resolver, allowedOrigins, deadline) {
566
+ const expected = context.revision.canonical_content_digest;
567
+ assertAllowedReferenceOrigin(expected.canonicalization_uri, allowedOrigins, "CANONICALIZATION_FETCH_FAILED");
568
+ const result = await withinInspectionDeadline(
569
+ resolver.resolve({
570
+ uri: expected.canonicalization_uri,
571
+ digest: `sha256:${expected.canonicalization_sha256.toLowerCase()}`
572
+ }),
573
+ deadline
574
+ );
575
+ if (!result.ok) {
576
+ throw new ReportingInspectionError("CANONICALIZATION_FETCH_FAILED", result.error.message, result.error.retryable, {
577
+ referenceCode: result.error.code
578
+ });
579
+ }
580
+ requireContentType(
581
+ result.contentType,
582
+ ["application/vnd.adcp.reporting-canonicalization+json"],
583
+ "CANONICALIZATION_INVALID"
584
+ );
585
+ const contract = parseCanonicalizationContract(
586
+ parsePinnedJson(result.body, "CANONICALIZATION_INVALID"),
587
+ context.revision.schema_sha256
588
+ );
589
+ if (context.expected.verificationProfile !== "canonical_digest" || !sameStringArray(contract.primary_keys, context.expected.canonicalization.primaryKeys)) {
590
+ throw inspectionError(
591
+ "CANONICALIZATION_INVALID",
592
+ "Canonicalization primary keys do not match consumer expectations"
593
+ );
594
+ }
595
+ for (const vector of contract.golden_vectors) {
596
+ assertInspectionDeadline(deadline);
597
+ const body = canonicalRows(vector.input_rows, contract.primary_keys, deadline);
598
+ if (Buffer.from(body).toString("base64") !== vector.canonical_utf8_base64 || !constantTimeHexEqual(sha256(body), vector.sha256)) {
599
+ throw inspectionError("CANONICALIZATION_INVALID", `Canonicalization golden vector ${vector.name} failed`);
600
+ }
601
+ }
602
+ return { ...expected, value: sha256(canonicalRows(rows, contract.primary_keys, deadline)) };
603
+ }
604
+ function parseCanonicalizationContract(document, schemaSha256) {
605
+ const allowed = ["contract_version", "media_type", "algorithm", "schema_sha256", "primary_keys", "golden_vectors"];
606
+ if (!isRecord(document) || !hasOnlyKeys(document, allowed) || document.contract_version !== "1.0" || document.media_type !== "application/vnd.adcp.reporting-canonicalization+json" || document.algorithm !== "adcp_jcs_rows_v1" || typeof document.schema_sha256 !== "string" || !constantTimeHexEqual(document.schema_sha256, schemaSha256) || !Array.isArray(document.primary_keys) || document.primary_keys.length === 0 || !document.primary_keys.every((item) => typeof item === "string" && item.length > 0 && item.length <= 128) || new Set(document.primary_keys).size !== document.primary_keys.length || !Array.isArray(document.golden_vectors) || document.golden_vectors.length < 2) {
607
+ throw inspectionError(
608
+ "CANONICALIZATION_INVALID",
609
+ "Pinned canonicalization contract is invalid or not bound to the row schema"
610
+ );
611
+ }
612
+ for (const vector of document.golden_vectors) {
613
+ if (!isRecord(vector) || !hasOnlyKeys(vector, ["name", "input_rows", "canonical_utf8_base64", "sha256"]) || typeof vector.name !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(vector.name) || !Array.isArray(vector.input_rows) || !vector.input_rows.every(isRecord) || typeof vector.canonical_utf8_base64 !== "string" || !isCanonicalBase64(vector.canonical_utf8_base64) || typeof vector.sha256 !== "string" || !/^[a-fA-F0-9]{64}$/.test(vector.sha256)) {
614
+ throw inspectionError("CANONICALIZATION_INVALID", "Canonicalization contract contains an invalid golden vector");
615
+ }
616
+ assertBoundedJsonData(vector.input_rows, "CANONICALIZATION_INVALID");
617
+ }
618
+ if (!document.golden_vectors.some((vector) => vector.input_rows.length === 0) || !document.golden_vectors.some((vector) => vector.input_rows.length >= 2)) {
619
+ throw inspectionError(
620
+ "CANONICALIZATION_INVALID",
621
+ "Canonicalization vectors must include empty-report and ordering/encoding cases"
622
+ );
623
+ }
624
+ return document;
625
+ }
626
+ function canonicalRows(rows, primaryKeys, deadline) {
627
+ const encoded = rows.map((row, index) => {
628
+ if (deadline !== void 0 && index % 256 === 0) assertInspectionDeadline(deadline);
629
+ assertIJson(row);
630
+ const keyValues = primaryKeys.map((key) => {
631
+ const value = row[key];
632
+ if (value === void 0 || value === null || typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
633
+ throw inspectionError("CANONICALIZATION_INVALID", `Row primary key ${key} must be a present scalar`);
634
+ }
635
+ return value;
636
+ });
637
+ return { key: Buffer.from(canonicalize(keyValues)), row: canonicalize(row) };
638
+ });
639
+ encoded.sort((left, right) => Buffer.compare(left.key, right.key));
640
+ for (let index = 1; index < encoded.length; index += 1) {
641
+ if (Buffer.compare(encoded[index - 1].key, encoded[index].key) === 0) {
642
+ throw inspectionError("CANONICALIZATION_INVALID", "Reporting rows contain duplicate primary-key tuples");
643
+ }
644
+ }
645
+ return Buffer.from(`[${encoded.map((item) => item.row).join(",")}]`, "utf8");
646
+ }
647
+ function parsePinnedJson(body, code) {
648
+ try {
649
+ return parseStrictJson(new TextDecoder("utf-8", { fatal: true }).decode(body));
650
+ } catch (error) {
651
+ throw inspectionError(code, "Pinned document is not strict UTF-8 JSON", false, error);
652
+ }
653
+ }
654
+ function parseManifest(body) {
655
+ let parsed;
656
+ try {
657
+ parsed = parseStrictJson(new TextDecoder("utf-8", { fatal: true }).decode(body));
658
+ } catch (error) {
659
+ throw inspectionError("MANIFEST_INVALID", "Manifest is not strict UTF-8 JSON", false, error);
660
+ }
661
+ if (!isRecord(parsed)) throw inspectionError("MANIFEST_INVALID", "Manifest must be a JSON object");
662
+ const allowed = /* @__PURE__ */ new Set([
663
+ "manifest_version",
664
+ "complete",
665
+ "reporting_revision_id",
666
+ "reporting_obligation_id",
667
+ "reporting_materialization_id",
668
+ "period",
669
+ "format",
670
+ "compression",
671
+ "files",
672
+ "total_size_bytes",
673
+ "row_count",
674
+ "control_totals",
675
+ "created_at"
676
+ ]);
677
+ if (Object.keys(parsed).some((key) => !allowed.has(key)))
678
+ throw inspectionError("MANIFEST_INVALID", "Manifest has unknown fields");
679
+ if (parsed.manifest_version !== "1.0" || parsed.complete !== true) {
680
+ throw inspectionError("MANIFEST_INCOMPLETE", "Manifest must be a complete v1 manifest-last commit");
681
+ }
682
+ if (typeof parsed.reporting_revision_id !== "string" || !ENTITY_ID_PATTERN.test(parsed.reporting_revision_id) || typeof parsed.reporting_obligation_id !== "string" || !ENTITY_ID_PATTERN.test(parsed.reporting_obligation_id) || typeof parsed.reporting_materialization_id !== "string" || !ENTITY_ID_PATTERN.test(parsed.reporting_materialization_id) || !isPeriod(parsed.period) || !["jsonl", "csv", "parquet", "avro", "orc"].includes(String(parsed.format)) || !["none", "gzip", "zstd", "snappy"].includes(String(parsed.compression)) || !Array.isArray(parsed.files) || parsed.files.length === 0 || !parsed.files.every(isFileEntry) || !isNonNegativeInteger(parsed.total_size_bytes) || !isNonNegativeInteger(parsed.row_count) || !Array.isArray(parsed.control_totals) || !parsed.control_totals.every(isControlTotal) || new Set(parsed.control_totals.map((total) => total.name)).size !== parsed.control_totals.length || typeof parsed.created_at !== "string" || !isRfc3339(parsed.created_at)) {
683
+ throw inspectionError("MANIFEST_INVALID", "Manifest does not satisfy the reporting file manifest contract");
684
+ }
685
+ return parsed;
686
+ }
687
+ function validateManifestBinding(manifest, context) {
688
+ if (manifest.reporting_revision_id !== context.revision.reporting_revision_id || manifest.reporting_obligation_id !== context.obligation.reporting_obligation_id || manifest.reporting_materialization_id !== context.materialization.reporting_materialization_id || canonicalize(manifest.period) !== canonicalize(context.revision.period)) {
689
+ throw inspectionError(
690
+ "MANIFEST_IDENTITY_MISMATCH",
691
+ "Manifest identity or period does not match the reporting ledger"
692
+ );
693
+ }
694
+ const size = manifest.files.reduce((sum, file) => sum + file.size_bytes, 0);
695
+ const rows = manifest.files.reduce((sum, file) => sum + file.row_count, 0);
696
+ if (size !== manifest.total_size_bytes || rows !== manifest.row_count) {
697
+ throw inspectionError("MANIFEST_TOTAL_MISMATCH", "Manifest file totals do not match its declared totals");
698
+ }
699
+ }
700
+ function decodeJsonLines(body, context) {
701
+ let text;
702
+ try {
703
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
704
+ } catch (error) {
705
+ throw inspectionError("MANIFEST_INVALID", "JSONL object is not valid UTF-8", false, error);
706
+ }
707
+ const rows = [];
708
+ let start = 0;
709
+ const append = (line) => {
710
+ const index = rows.length;
711
+ if (index >= context.maxRows) {
712
+ throw inspectionError("RESOURCE_TOO_LARGE", "JSONL object exceeds the configured row budget");
713
+ }
714
+ if (!line.trim()) throw inspectionError("MANIFEST_INVALID", `JSONL row ${index} is empty`);
715
+ try {
716
+ const value = parseStrictJson(line);
717
+ if (!isRecord(value)) throw new Error("row is not an object");
718
+ rows.push(value);
719
+ } catch (error) {
720
+ throw inspectionError("MANIFEST_INVALID", `JSONL row ${index} is invalid`, false, error);
721
+ }
722
+ };
723
+ for (let index = 0; index < text.length; index += 1) {
724
+ if (text[index] !== "\n") continue;
725
+ const end = index > start && text[index - 1] === "\r" ? index - 1 : index;
726
+ append(text.slice(start, end));
727
+ start = index + 1;
728
+ }
729
+ if (start < text.length) append(text.slice(start));
730
+ return rows;
731
+ }
732
+ function decodeCsv(body, context) {
733
+ let text;
734
+ try {
735
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
736
+ } catch (error) {
737
+ throw inspectionError("MANIFEST_INVALID", "CSV object is not valid UTF-8", false, error);
738
+ }
739
+ const records = parseCsvRecords(text, context.maxRows + 1);
740
+ if (records.length === 0) throw inspectionError("MANIFEST_INVALID", "CSV object has no header row");
741
+ const headers = records[0];
742
+ if (headers.some((header) => !header) || new Set(headers).size !== headers.length) {
743
+ throw inspectionError("MANIFEST_INVALID", "CSV header names must be non-empty and unique");
744
+ }
745
+ return records.slice(1).map((record, index) => {
746
+ if (record.length !== headers.length)
747
+ throw inspectionError("MANIFEST_INVALID", `CSV row ${index} has the wrong column count`);
748
+ return Object.fromEntries(headers.map((header, column) => [header, record[column]]));
749
+ });
750
+ }
751
+ function parseCsvRecords(text, maxRecords) {
752
+ const rows = [];
753
+ let row = [];
754
+ let field = "";
755
+ let quoted = false;
756
+ for (let index = 0; index < text.length; index += 1) {
757
+ const char = text[index];
758
+ if (quoted) {
759
+ if (char === '"' && text[index + 1] === '"') {
760
+ field += '"';
761
+ index += 1;
762
+ } else if (char === '"') quoted = false;
763
+ else field += char;
764
+ continue;
765
+ }
766
+ if (char === '"' && field === "") quoted = true;
767
+ else if (char === ",") {
768
+ row.push(field);
769
+ field = "";
770
+ } else if (char === "\n") {
771
+ row.push(field);
772
+ rows.push(row);
773
+ if (rows.length > maxRecords) {
774
+ throw inspectionError("RESOURCE_TOO_LARGE", "CSV object exceeds the configured row budget");
775
+ }
776
+ row = [];
777
+ field = "";
778
+ } else if (char === "\r" && text[index + 1] === "\n") continue;
779
+ else field += char;
780
+ }
781
+ if (quoted) throw inspectionError("MANIFEST_INVALID", "CSV has an unterminated quoted field");
782
+ if (field !== "" || row.length > 0) {
783
+ row.push(field);
784
+ rows.push(row);
785
+ if (rows.length > maxRecords) {
786
+ throw inspectionError("RESOURCE_TOO_LARGE", "CSV object exceeds the configured row budget");
787
+ }
788
+ }
789
+ return rows;
790
+ }
791
+ function calculateNamedControlTotals(rows, expected, reportDefinition, deadline) {
792
+ return expected.map((total) => {
793
+ const metric = reportDefinition.metrics.find(
794
+ (candidate) => isRecord(candidate) && candidate.name === total.name
795
+ );
796
+ if (!isRecord(metric) || metric.aggregation !== "sum" || typeof metric.source_expression !== "string") {
797
+ throw inspectionError(
798
+ "CONTROL_TOTAL_UNSUPPORTED",
799
+ `Control total ${total.name} needs a custom calculator for its declared aggregation`
800
+ );
801
+ }
802
+ let sum = { coefficient: 0n, scale: 0 };
803
+ for (let index = 0; index < rows.length; index += 1) {
804
+ if (index % 256 === 0) assertInspectionDeadline(deadline);
805
+ const row = rows[index];
806
+ const raw = getNamedValue(row, metric.source_expression);
807
+ if (raw === void 0 || typeof raw !== "string" && typeof raw !== "number") {
808
+ throw inspectionError(
809
+ "CONTROL_TOTAL_UNSUPPORTED",
810
+ `Control total ${total.name} needs a custom calculator because it is not a numeric row field`
811
+ );
812
+ }
813
+ sum = addDecimal(sum, parseDecimal(raw));
814
+ }
815
+ const minimumScale = total.value_type === "decimal" ? total.value.split(".")[1]?.length ?? 0 : 0;
816
+ return { ...total, value: formatDecimal(sum, minimumScale) };
817
+ });
818
+ }
819
+ function boundedPositiveInteger(value, name, maximum) {
820
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
821
+ throw new TypeError(`${name} must be an integer from 1 through ${maximum}`);
822
+ }
823
+ return value;
824
+ }
825
+ function assertInspectionDeadline(deadline) {
826
+ if (Date.now() >= deadline) {
827
+ throw inspectionError("INSPECTION_TIMEOUT", "Reporting inspection exceeded its time budget", true);
828
+ }
829
+ }
830
+ function deadlineSignal(deadline) {
831
+ assertInspectionDeadline(deadline);
832
+ return AbortSignal.timeout(Math.max(1, deadline - Date.now()));
833
+ }
834
+ async function withinInspectionDeadline(operation, deadline) {
835
+ assertInspectionDeadline(deadline);
836
+ const remaining = deadline - Date.now();
837
+ let timer;
838
+ try {
839
+ return await Promise.race([
840
+ operation,
841
+ new Promise((_resolve, reject) => {
842
+ timer = setTimeout(
843
+ () => reject(inspectionError("INSPECTION_TIMEOUT", "Reporting inspection exceeded its time budget", true)),
844
+ remaining
845
+ );
846
+ })
847
+ ]);
848
+ } finally {
849
+ if (timer) clearTimeout(timer);
850
+ }
851
+ }
852
+ function parseDecimal(value) {
853
+ const text = String(value);
854
+ if (text.length > 1024) {
855
+ throw inspectionError("CONTROL_TOTAL_UNSUPPORTED", "Control-total decimal exceeds the precision bound");
856
+ }
857
+ const match = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(text);
858
+ if (!match) throw inspectionError("CONTROL_TOTAL_UNSUPPORTED", `Non-decimal control value ${text}`);
859
+ const fraction = match[3] ?? "";
860
+ const exponent = Number(match[4] ?? 0);
861
+ if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 1024 || match[2].length + fraction.length > 1024) {
862
+ throw inspectionError("CONTROL_TOTAL_UNSUPPORTED", "Control-total decimal exceeds the exponent or precision bound");
863
+ }
864
+ let scale = fraction.length - exponent;
865
+ if (Math.abs(scale) > 1024) {
866
+ throw inspectionError("CONTROL_TOTAL_UNSUPPORTED", "Control-total decimal exceeds the scale bound");
867
+ }
868
+ let coefficient = BigInt(`${match[1]}${match[2]}${fraction}`);
869
+ if (scale < 0) {
870
+ coefficient *= 10n ** BigInt(-scale);
871
+ scale = 0;
872
+ }
873
+ return normalizeDecimal({ coefficient, scale });
874
+ }
875
+ function addDecimal(left, right) {
876
+ const scale = Math.max(left.scale, right.scale);
877
+ return normalizeDecimal({
878
+ coefficient: left.coefficient * 10n ** BigInt(scale - left.scale) + right.coefficient * 10n ** BigInt(scale - right.scale),
879
+ scale
880
+ });
881
+ }
882
+ function normalizeDecimal(value) {
883
+ let { coefficient, scale } = value;
884
+ while (scale > 0 && coefficient % 10n === 0n) {
885
+ coefficient /= 10n;
886
+ scale -= 1;
887
+ }
888
+ return { coefficient, scale };
889
+ }
890
+ function formatDecimal(value, minimumScale) {
891
+ const normalized = normalizeDecimal(value);
892
+ const scale = Math.max(normalized.scale, minimumScale);
893
+ const negative = normalized.coefficient < 0n;
894
+ const magnitude = (negative ? -normalized.coefficient : normalized.coefficient) * 10n ** BigInt(scale - normalized.scale);
895
+ const digits = magnitude.toString().padStart(scale + 1, "0");
896
+ const unsigned = scale === 0 ? digits : `${digits.slice(0, -scale)}.${digits.slice(-scale)}`;
897
+ return negative ? `-${unsigned}` : unsigned;
898
+ }
899
+ function getNamedValue(row, name) {
900
+ if (Object.prototype.hasOwnProperty.call(row, name)) return row[name];
901
+ return name.split(".").reduce((value, part) => isRecord(value) ? value[part] : void 0, row);
902
+ }
903
+ function sameControlTotals(left, right) {
904
+ const normalize = (totals) => [...totals].sort((a, b) => a.name.localeCompare(b.name)).map((total) => canonicalize(total));
905
+ return canonicalize(normalize(left)) === canonicalize(normalize(right));
906
+ }
907
+ function sameStringArray(left, right) {
908
+ return left.length === right.length && left.every((value, index) => value === right[index]);
909
+ }
910
+ function assertIJson(value, seen = /* @__PURE__ */ new Set()) {
911
+ if (typeof value === "string") {
912
+ for (let index = 0; index < value.length; index += 1) {
913
+ const code = value.charCodeAt(index);
914
+ if (code >= 55296 && code <= 56319) {
915
+ const next = value.charCodeAt(index + 1);
916
+ if (next < 56320 || next > 57343)
917
+ throw inspectionError("CANONICALIZATION_INVALID", "Lone Unicode surrogate is not I-JSON");
918
+ index += 1;
919
+ } else if (code >= 56320 && code <= 57343) {
920
+ throw inspectionError("CANONICALIZATION_INVALID", "Lone Unicode surrogate is not I-JSON");
921
+ }
922
+ }
923
+ return;
924
+ }
925
+ if (typeof value === "number" && !Number.isFinite(value))
926
+ throw inspectionError("CANONICALIZATION_INVALID", "Non-finite number is not I-JSON");
927
+ if (!value || typeof value !== "object") return;
928
+ if (seen.has(value)) throw inspectionError("CANONICALIZATION_INVALID", "Cyclic row is not JSON");
929
+ seen.add(value);
930
+ if (Array.isArray(value)) value.forEach((child) => assertIJson(child, seen));
931
+ else
932
+ for (const [key, child] of Object.entries(value)) {
933
+ assertIJson(key);
934
+ assertIJson(child, seen);
935
+ }
936
+ seen.delete(value);
937
+ }
938
+ function isValidReportDefinition(value) {
939
+ if (!isRecord(value) || !hasOnlyKeys(value, [
940
+ "contract_version",
941
+ "media_type",
942
+ "report_definition_id",
943
+ "reporting_profile",
944
+ "grain",
945
+ "source",
946
+ "calendar",
947
+ "metrics",
948
+ "dimensions",
949
+ "restatement_policy",
950
+ "finality_policies"
951
+ ]) || value.contract_version !== "1.0" || value.media_type !== "application/vnd.adcp.reporting-definition+json" || typeof value.report_definition_id !== "string" || !ENTITY_ID_PATTERN.test(value.report_definition_id) || typeof value.reporting_profile !== "string" || value.reporting_profile.length === 0 || value.reporting_profile.length > 128 || typeof value.grain !== "string" || value.grain.length === 0 || value.grain.length > 128) {
952
+ return false;
953
+ }
954
+ assertBoundedJsonData(value, "REPORT_DEFINITION_INVALID");
955
+ if (containsReferenceKeyword(value)) return false;
956
+ const source = value.source;
957
+ if (!isRecord(source) || !hasOnlyKeys(source, ["provider", "system", "api_version", "query_semantics"]) || !isRecord(source.provider) || !hasOnlyKeys(source.provider, ["domain"]) || typeof source.provider.domain !== "string" || !DOMAIN_PATTERN.test(source.provider.domain) || typeof source.system !== "string" || source.system.length === 0 || source.system.length > 128 || typeof source.api_version !== "string" || source.api_version.length === 0 || source.api_version.length > 128 || !isRecord(source.query_semantics)) {
958
+ return false;
959
+ }
960
+ const calendar = value.calendar;
961
+ if (!isRecord(calendar) || !hasOnlyKeys(calendar, ["timezone_basis", "timezone"]) || !["utc", "account_timezone", "configured_timezone"].includes(String(calendar.timezone_basis)) || (calendar.timezone_basis === "configured_timezone" ? typeof calendar.timezone !== "string" || calendar.timezone.length === 0 || calendar.timezone.length > 255 : calendar.timezone !== void 0)) {
962
+ return false;
963
+ }
964
+ if (!Array.isArray(value.metrics) || value.metrics.length === 0 || !value.metrics.every(
965
+ (metric) => isRecord(metric) && hasOnlyKeys(metric, ["name", "source_expression", "aggregation", "unit"]) && typeof metric.name === "string" && metric.name.length > 0 && metric.name.length <= 128 && typeof metric.source_expression === "string" && metric.source_expression.length > 0 && metric.source_expression.length <= 2048 && ["sum", "count", "min", "max", "average", "ratio", "last", "custom"].includes(String(metric.aggregation)) && (metric.unit === void 0 || typeof metric.unit === "string" && metric.unit.length > 0 && metric.unit.length <= 64)
966
+ ) || !Array.isArray(value.dimensions) || !value.dimensions.every(
967
+ (dimension) => typeof dimension === "string" && dimension.length > 0 && dimension.length <= 128
968
+ ) || new Set(value.dimensions).size !== value.dimensions.length) {
969
+ return false;
970
+ }
971
+ const restatement = value.restatement_policy;
972
+ if (!isRecord(restatement) || !hasOnlyKeys(restatement, ["source_requery_duration", "emit_only_on_content_change"]) || typeof restatement.source_requery_duration !== "string" || !ISO_DURATION_PATTERN.test(restatement.source_requery_duration) || restatement.emit_only_on_content_change !== true) {
973
+ return false;
974
+ }
975
+ if (!Array.isArray(value.finality_policies) || value.finality_policies.length === 0 || !value.finality_policies.every(isValidFinalityPolicy)) {
976
+ return false;
977
+ }
978
+ const policyIds = value.finality_policies.map((policy) => policy.finality_policy_id);
979
+ return new Set(policyIds).size === policyIds.length;
980
+ }
981
+ function isValidFinalityPolicy(value) {
982
+ if (!isRecord(value) || typeof value.finality_policy_id !== "string" || !ENTITY_ID_PATTERN.test(value.finality_policy_id)) {
983
+ return false;
984
+ }
985
+ if (value.basis === "source_final") {
986
+ return hasOnlyKeys(value, ["finality_policy_id", "basis", "source_signal"]) && typeof value.source_signal === "string" && value.source_signal.length > 0 && value.source_signal.length <= 512;
987
+ }
988
+ if (value.basis === "contractual_cutoff") {
989
+ return hasOnlyKeys(value, ["finality_policy_id", "basis", "duration_after_period_end"]) && typeof value.duration_after_period_end === "string" && ISO_DURATION_PATTERN.test(value.duration_after_period_end);
990
+ }
991
+ if (value.basis === "stabilized") {
992
+ return hasOnlyKeys(value, ["finality_policy_id", "basis", "minimum_age", "unchanged_for"]) && typeof value.minimum_age === "string" && ISO_DURATION_PATTERN.test(value.minimum_age) && typeof value.unchanged_for === "string" && ISO_DURATION_PATTERN.test(value.unchanged_for);
993
+ }
994
+ return false;
995
+ }
996
+ function assertBoundedJsonData(value, code) {
997
+ let nodes = 0;
998
+ const walk = (item, depth) => {
999
+ if (depth > 64 || ++nodes > 1e4) throw inspectionError(code, "Pinned document exceeds safety bounds");
1000
+ if (typeof item === "string" && item.length > 1e6) {
1001
+ throw inspectionError(code, "Pinned document contains an oversized string");
1002
+ }
1003
+ if (Array.isArray(item)) item.forEach((child) => walk(child, depth + 1));
1004
+ else if (isRecord(item))
1005
+ Object.entries(item).forEach(([key, child]) => {
1006
+ walk(key, depth + 1);
1007
+ walk(child, depth + 1);
1008
+ });
1009
+ };
1010
+ walk(value, 0);
1011
+ }
1012
+ function containsReferenceKeyword(value) {
1013
+ if (Array.isArray(value)) return value.some(containsReferenceKeyword);
1014
+ if (!isRecord(value)) return false;
1015
+ if ("$ref" in value || "$dynamicRef" in value || "$recursiveRef" in value) return true;
1016
+ return Object.values(value).some(containsReferenceKeyword);
1017
+ }
1018
+ function hasOnlyKeys(value, allowed) {
1019
+ return Object.keys(value).every((key) => allowed.includes(key));
1020
+ }
1021
+ function isCanonicalBase64(value) {
1022
+ if (!value || value.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
1023
+ return false;
1024
+ }
1025
+ return Buffer.from(value, "base64").toString("base64") === value;
1026
+ }
1027
+ function isPeriod(value) {
1028
+ return isRecord(value) && hasOnlyKeys(value, ["start", "end", "source_timezone"]) && typeof value.start === "string" && typeof value.end === "string" && typeof value.source_timezone === "string" && value.source_timezone.length > 0 && isRfc3339(value.start) && isRfc3339(value.end);
1029
+ }
1030
+ function isFileEntry(value) {
1031
+ return isRecord(value) && Object.keys(value).every((key) => ["object_ref", "size_bytes", "sha256", "row_count", "partition"].includes(key)) && typeof value.object_ref === "string" && value.object_ref.length > 0 && value.object_ref.length <= 1024 && isNonNegativeInteger(value.size_bytes) && typeof value.sha256 === "string" && /^[a-fA-F0-9]{64}$/.test(value.sha256) && isNonNegativeInteger(value.row_count) && (value.partition === void 0 || isStringRecord(value.partition) && Object.keys(value.partition).length <= 32 && Object.values(value.partition).every((item) => item.length <= 512));
1032
+ }
1033
+ function isControlTotal(value) {
1034
+ return isRecord(value) && hasOnlyKeys(value, ["name", "value", "value_type", "unit"]) && typeof value.name === "string" && CONTROL_TOTAL_NAME_PATTERN.test(value.name) && typeof value.value === "string" && CANONICAL_DECIMAL_PATTERN.test(value.value) && (value.value_type === "integer" || value.value_type === "decimal") && (value.unit === void 0 || typeof value.unit === "string" && value.unit.length > 0 && value.unit.length <= 32);
1035
+ }
1036
+ function isStringRecord(value) {
1037
+ return isRecord(value) && Object.values(value).every((child) => typeof child === "string");
1038
+ }
1039
+ function isRecord(value) {
1040
+ return value !== null && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
1041
+ }
1042
+ function isNonNegativeInteger(value) {
1043
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1044
+ }
1045
+ function isRfc3339(value) {
1046
+ return RFC3339_PATTERN.test(value) && Number.isFinite(Date.parse(value));
1047
+ }
1048
+ function sha256(body) {
1049
+ return digestHex(body, "sha256");
1050
+ }
1051
+ function digestHex(body, algorithm) {
1052
+ return createHash(algorithm).update(body).digest("hex");
1053
+ }
1054
+ function constantTimeHexEqual(left, right) {
1055
+ if (left.length !== right.length || !/^(?:[a-fA-F0-9]{64}|[a-fA-F0-9]{128})$/.test(left) || !/^(?:[a-fA-F0-9]{64}|[a-fA-F0-9]{128})$/.test(right))
1056
+ return false;
1057
+ return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
1058
+ }
1059
+ function requireContentType(raw, allowed, code) {
1060
+ const mediaType = raw?.split(";", 1)[0]?.trim().toLowerCase();
1061
+ if (!mediaType || !allowed.includes(mediaType)) {
1062
+ throw inspectionError(code, `Pinned document returned unsupported content type ${mediaType ?? "<missing>"}`);
1063
+ }
1064
+ }
1065
+ function assertAllowedReferenceOrigin(raw, allowedOrigins, code) {
1066
+ let target;
1067
+ try {
1068
+ target = new URL(raw);
1069
+ } catch {
1070
+ throw inspectionError(code, "Pinned document URI is invalid");
1071
+ }
1072
+ if (target.protocol !== "https:" || target.username || target.password || !allowedOrigins.includes(target.origin)) {
1073
+ throw inspectionError(code, "Pinned document URI is not on a consumer-approved origin");
1074
+ }
1075
+ }
1076
+ function sanitizeAjvErrors(errors) {
1077
+ return (errors ?? []).slice(0, 20).map((error) => ({
1078
+ instancePath: error.instancePath,
1079
+ schemaPath: error.schemaPath,
1080
+ keyword: error.keyword,
1081
+ message: error.message
1082
+ }));
1083
+ }
1084
+ function inspectionError(code, message, retryable = false, cause) {
1085
+ return new ReportingInspectionError(code, message, retryable, void 0, cause === void 0 ? void 0 : { cause });
1086
+ }
1087
+ function inspectionMismatch(code, message, observation) {
1088
+ return new ReportingInspectionError(code, message, false, void 0, void 0, observation);
1089
+ }
1090
+ export {
1091
+ ReportingInspectionError,
1092
+ createHttpsReportingResourceReader,
1093
+ createReportingManifestInspector
1094
+ };