@adcp/sdk 15.3.0 → 15.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/advanced.d.mts +2 -2
- package/dist/lib/advanced.d.ts +2 -2
- package/dist/lib/advanced.js +8 -2
- package/dist/lib/advanced.mjs +8 -2
- package/dist/lib/core/AgentClient.d.mts +4 -0
- package/dist/lib/core/AgentClient.d.ts +4 -0
- package/dist/lib/core/AgentClient.js +8 -0
- package/dist/lib/core/AgentClient.mjs +8 -0
- package/dist/lib/core/SingleAgentClient.d.mts +154 -0
- package/dist/lib/core/SingleAgentClient.d.ts +154 -0
- package/dist/lib/core/SingleAgentClient.js +661 -23
- package/dist/lib/core/SingleAgentClient.mjs +666 -23
- package/dist/lib/core/agent-discovery-cache.d.mts +103 -0
- package/dist/lib/core/agent-discovery-cache.d.ts +103 -0
- package/dist/lib/core/agent-discovery-cache.js +121 -0
- package/dist/lib/core/agent-discovery-cache.mjs +92 -0
- package/dist/lib/index.d.mts +5 -3
- package/dist/lib/index.d.ts +5 -3
- package/dist/lib/index.js +7 -0
- package/dist/lib/index.mjs +6 -0
- package/dist/lib/protocols/index.d.mts +2 -2
- package/dist/lib/protocols/index.d.ts +2 -2
- package/dist/lib/protocols/index.js +1 -1
- package/dist/lib/protocols/index.mjs +1 -1
- package/dist/lib/protocols/mcp-call-context.d.mts +109 -0
- package/dist/lib/protocols/mcp-call-context.d.ts +109 -0
- package/dist/lib/protocols/mcp-call-context.js +221 -0
- package/dist/lib/protocols/mcp-call-context.mjs +186 -0
- package/dist/lib/protocols/mcp-modern.d.mts +9 -1
- package/dist/lib/protocols/mcp-modern.d.ts +9 -1
- package/dist/lib/protocols/mcp-modern.js +180 -104
- package/dist/lib/protocols/mcp-modern.mjs +193 -104
- package/dist/lib/protocols/mcp-scope.d.mts +15 -0
- package/dist/lib/protocols/mcp-scope.d.ts +15 -0
- package/dist/lib/protocols/mcp.d.mts +4 -2
- package/dist/lib/protocols/mcp.d.ts +4 -2
- package/dist/lib/protocols/mcp.js +178 -154
- package/dist/lib/protocols/mcp.mjs +189 -153
- package/dist/lib/protocols/transportDiagnostics.d.mts +33 -1
- package/dist/lib/protocols/transportDiagnostics.d.ts +33 -1
- package/dist/lib/protocols/transportDiagnostics.js +128 -12
- package/dist/lib/protocols/transportDiagnostics.mjs +128 -12
- package/dist/lib/reporting/gcs/index.d.mts +2 -0
- package/dist/lib/reporting/gcs/index.d.ts +2 -0
- package/dist/lib/reporting/gcs/index.js +3 -0
- package/dist/lib/reporting/gcs/index.mjs +2 -0
- package/dist/lib/reporting/gcs/row-provider.d.mts +32 -0
- package/dist/lib/reporting/gcs/row-provider.d.ts +32 -0
- package/dist/lib/reporting/gcs/row-provider.js +205 -0
- package/dist/lib/reporting/gcs/row-provider.mjs +188 -0
- package/dist/lib/reporting/ledger/change-feed.d.mts +41 -0
- package/dist/lib/reporting/ledger/change-feed.d.ts +41 -0
- package/dist/lib/reporting/ledger/change-feed.js +101 -0
- package/dist/lib/reporting/ledger/change-feed.mjs +74 -0
- package/dist/lib/reporting/ledger/handler.d.mts +6 -0
- package/dist/lib/reporting/ledger/handler.d.ts +6 -0
- package/dist/lib/reporting/ledger/handler.js +51 -8
- package/dist/lib/reporting/ledger/handler.mjs +50 -8
- package/dist/lib/reporting/ledger/index.d.mts +5 -0
- package/dist/lib/reporting/ledger/index.d.ts +5 -0
- package/dist/lib/reporting/ledger/index.js +11 -1
- package/dist/lib/reporting/ledger/index.mjs +5 -0
- package/dist/lib/reporting/ledger/lifecycle.js +3 -2
- package/dist/lib/reporting/ledger/lifecycle.mjs +3 -2
- package/dist/lib/reporting/ledger/managed-postgres.d.mts +2 -0
- package/dist/lib/reporting/ledger/managed-postgres.d.ts +2 -0
- package/dist/lib/reporting/ledger/managed-postgres.js +26 -6
- package/dist/lib/reporting/ledger/managed-postgres.mjs +26 -6
- package/dist/lib/reporting/ledger/managed.d.mts +9 -0
- package/dist/lib/reporting/ledger/managed.d.ts +9 -0
- package/dist/lib/reporting/ledger/managed.js +18 -0
- package/dist/lib/reporting/ledger/managed.mjs +18 -0
- package/dist/lib/reporting/ledger/postgres.d.mts +179 -1
- package/dist/lib/reporting/ledger/postgres.d.ts +179 -1
- package/dist/lib/reporting/ledger/postgres.js +882 -19
- package/dist/lib/reporting/ledger/postgres.mjs +890 -19
- package/dist/lib/reporting/ledger/producer.js +13 -3
- package/dist/lib/reporting/ledger/producer.mjs +13 -3
- package/dist/lib/reporting/ledger/revision-metadata.d.mts +17 -0
- package/dist/lib/reporting/ledger/revision-metadata.d.ts +17 -0
- package/dist/lib/reporting/ledger/revision-metadata.js +71 -0
- package/dist/lib/reporting/ledger/revision-metadata.mjs +44 -0
- package/dist/lib/reporting/ledger/row-encoding.d.mts +128 -0
- package/dist/lib/reporting/ledger/row-encoding.d.ts +128 -0
- package/dist/lib/reporting/ledger/row-encoding.js +355 -0
- package/dist/lib/reporting/ledger/row-encoding.mjs +315 -0
- package/dist/lib/reporting/ledger/row-storage-errors.d.mts +7 -0
- package/dist/lib/reporting/ledger/row-storage-errors.d.ts +7 -0
- package/dist/lib/reporting/ledger/row-storage-errors.js +41 -0
- package/dist/lib/reporting/ledger/row-storage-errors.mjs +16 -0
- package/dist/lib/reporting/ledger/row-storage-migration.d.mts +41 -0
- package/dist/lib/reporting/ledger/row-storage-migration.d.ts +41 -0
- package/dist/lib/reporting/ledger/row-storage-migration.js +234 -0
- package/dist/lib/reporting/ledger/row-storage-migration.mjs +208 -0
- package/dist/lib/reporting/ledger/row-storage-object.d.mts +112 -0
- package/dist/lib/reporting/ledger/row-storage-object.d.ts +112 -0
- package/dist/lib/reporting/ledger/row-storage-object.js +281 -0
- package/dist/lib/reporting/ledger/row-storage-object.mjs +239 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.d.mts +51 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.d.ts +51 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.js +152 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.mjs +119 -0
- package/dist/lib/reporting/ledger/row-storage.d.mts +224 -0
- package/dist/lib/reporting/ledger/row-storage.d.ts +224 -0
- package/dist/lib/reporting/ledger/row-storage.js +692 -0
- package/dist/lib/reporting/ledger/row-storage.mjs +680 -0
- package/dist/lib/reporting/ledger/types.d.mts +45 -0
- package/dist/lib/reporting/ledger/types.d.ts +45 -0
- package/dist/lib/reporting/service/index.d.mts +41 -1
- package/dist/lib/reporting/service/index.d.ts +41 -1
- package/dist/lib/reporting/service/index.js +68 -1
- package/dist/lib/reporting/service/index.mjs +70 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/signing/agent-resolver/fetch-helpers.js +1 -1
- package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +1 -1
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/README.md +1 -1
- package/docs/TYPE-SUMMARY.md +35 -2
- package/docs/guides/DISCOVERY-CACHE-AND-SESSION-REUSE.md +142 -0
- package/docs/guides/EXISTING-PLATFORM.md +43 -12
- package/docs/guides/REPORTING-LEDGER.md +209 -0
- package/docs/llms.txt +4 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/docs/recipes/verifying-inbound-webhooks.md +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var row_encoding_exports = {};
|
|
20
|
+
__export(row_encoding_exports, {
|
|
21
|
+
REPORTING_ROW_CHUNK_MAX_BYTES: () => REPORTING_ROW_CHUNK_MAX_BYTES,
|
|
22
|
+
REPORTING_ROW_CHUNK_MAX_ROWS: () => REPORTING_ROW_CHUNK_MAX_ROWS,
|
|
23
|
+
REPORTING_ROW_CHUNK_MAX_SEGMENTS: () => REPORTING_ROW_CHUNK_MAX_SEGMENTS,
|
|
24
|
+
REPORTING_ROW_ENCODING_V1: () => REPORTING_ROW_ENCODING_V1,
|
|
25
|
+
REPORTING_ROW_SEGMENT_MAX_ROWS: () => REPORTING_ROW_SEGMENT_MAX_ROWS,
|
|
26
|
+
ReportingRowEncodingError: () => ReportingRowEncodingError,
|
|
27
|
+
assertReportingRevisionEnvelopeV1: () => assertReportingRevisionEnvelopeV1,
|
|
28
|
+
canonicalReportingRowV1: () => canonicalReportingRowV1,
|
|
29
|
+
createReportingRevisionEnvelopeHasherV1: () => createReportingRevisionEnvelopeHasherV1,
|
|
30
|
+
createReportingRowsOnlyHasherV1: () => createReportingRowsOnlyHasherV1,
|
|
31
|
+
decodeVerifiedReportingRowSegmentsV1: () => decodeVerifiedReportingRowSegmentsV1,
|
|
32
|
+
encodeReportingRowsV1: () => encodeReportingRowsV1,
|
|
33
|
+
isReportingRowEncodingError: () => isReportingRowEncodingError,
|
|
34
|
+
reportingRevisionEnvelopeDigestV1: () => reportingRevisionEnvelopeDigestV1,
|
|
35
|
+
reportingRowManifestSha256V1: () => reportingRowManifestSha256V1,
|
|
36
|
+
verifyReportingRowChunkV1: () => verifyReportingRowChunkV1,
|
|
37
|
+
verifyReportingRowManifestsV1: () => verifyReportingRowManifestsV1
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(row_encoding_exports);
|
|
40
|
+
var import_crypto = require("crypto");
|
|
41
|
+
var import_jcs = require('../../utils/jcs.js');
|
|
42
|
+
const REPORTING_ROW_ENCODING_V1 = "adcp_canonical_jsonl_v1";
|
|
43
|
+
const REPORTING_ROW_SEGMENT_MAX_ROWS = 500;
|
|
44
|
+
const REPORTING_ROW_CHUNK_MAX_SEGMENTS = 20;
|
|
45
|
+
const REPORTING_ROW_CHUNK_MAX_ROWS = REPORTING_ROW_SEGMENT_MAX_ROWS * REPORTING_ROW_CHUNK_MAX_SEGMENTS;
|
|
46
|
+
const REPORTING_ROW_CHUNK_MAX_BYTES = 8 * 1024 * 1024;
|
|
47
|
+
const NEWLINE = 10;
|
|
48
|
+
const COMMA = Buffer.from(",", "utf8");
|
|
49
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
50
|
+
class ReportingRowEncodingError extends Error {
|
|
51
|
+
constructor(code, detail) {
|
|
52
|
+
super(`Reporting row encoding: ${code}: ${detail}`);
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.name = "ReportingRowEncodingError";
|
|
55
|
+
Object.defineProperty(this, /* @__PURE__ */ Symbol.for("adcp.reportingRowEncodingError"), { value: true });
|
|
56
|
+
}
|
|
57
|
+
code;
|
|
58
|
+
}
|
|
59
|
+
function isReportingRowEncodingError(error) {
|
|
60
|
+
return !!error && typeof error === "object" && error[/* @__PURE__ */ Symbol.for("adcp.reportingRowEncodingError")] === true;
|
|
61
|
+
}
|
|
62
|
+
function canonicalReportingRowV1(row, ordinal = 0) {
|
|
63
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
64
|
+
throw new ReportingRowEncodingError("INVALID_ROW", `row ${ordinal} is not a JSON object`);
|
|
65
|
+
}
|
|
66
|
+
assertPortableNumbers(row, ordinal);
|
|
67
|
+
let canonical;
|
|
68
|
+
try {
|
|
69
|
+
canonical = (0, import_jcs.canonicalize)(row);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new ReportingRowEncodingError(
|
|
72
|
+
"INVALID_ROW",
|
|
73
|
+
`row ${ordinal} is not canonical JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (LONE_SURROGATE.test(canonical)) {
|
|
77
|
+
throw new ReportingRowEncodingError("INVALID_ROW", `row ${ordinal} contains a lone surrogate`);
|
|
78
|
+
}
|
|
79
|
+
return canonical;
|
|
80
|
+
}
|
|
81
|
+
function encodeReportingRowsV1(rows) {
|
|
82
|
+
const chunks = [];
|
|
83
|
+
let chunkLines = [];
|
|
84
|
+
let chunkBytes = 0;
|
|
85
|
+
let chunkFirstOrdinal = 0;
|
|
86
|
+
const closeChunk = () => {
|
|
87
|
+
if (chunkLines.length === 0) return;
|
|
88
|
+
chunks.push(buildChunk(chunks.length, chunkFirstOrdinal, chunkLines));
|
|
89
|
+
chunkLines = [];
|
|
90
|
+
chunkBytes = 0;
|
|
91
|
+
};
|
|
92
|
+
rows.forEach((row, ordinal) => {
|
|
93
|
+
const line = Buffer.from(canonicalReportingRowV1(row, ordinal) + "\n", "utf8");
|
|
94
|
+
if (chunkLines.length === REPORTING_ROW_CHUNK_MAX_ROWS || chunkLines.length > 0 && chunkBytes + line.byteLength > REPORTING_ROW_CHUNK_MAX_BYTES) {
|
|
95
|
+
closeChunk();
|
|
96
|
+
}
|
|
97
|
+
if (chunkLines.length === 0) chunkFirstOrdinal = ordinal;
|
|
98
|
+
chunkLines.push(line);
|
|
99
|
+
chunkBytes += line.byteLength;
|
|
100
|
+
});
|
|
101
|
+
closeChunk();
|
|
102
|
+
const manifests = chunks.map((chunk) => chunk.manifest);
|
|
103
|
+
return {
|
|
104
|
+
encoding: REPORTING_ROW_ENCODING_V1,
|
|
105
|
+
rowCount: rows.length,
|
|
106
|
+
byteCount: chunks.reduce((total, chunk) => total + chunk.manifest.byte_count, 0),
|
|
107
|
+
chunks,
|
|
108
|
+
manifests,
|
|
109
|
+
rowManifestSha256: reportingRowManifestSha256V1(manifests)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function reportingRowManifestSha256V1(manifests) {
|
|
113
|
+
return sha256Hex(
|
|
114
|
+
(0, import_jcs.canonicalize)(
|
|
115
|
+
manifests.map((manifest) => ({
|
|
116
|
+
chunk_index: manifest.chunk_index,
|
|
117
|
+
first_ordinal: manifest.first_ordinal,
|
|
118
|
+
row_count: manifest.row_count,
|
|
119
|
+
byte_count: manifest.byte_count,
|
|
120
|
+
sha256: manifest.sha256,
|
|
121
|
+
segments: manifest.segments.map((segment) => ({
|
|
122
|
+
first_ordinal: segment.first_ordinal,
|
|
123
|
+
row_count: segment.row_count,
|
|
124
|
+
byte_offset: segment.byte_offset,
|
|
125
|
+
byte_count: segment.byte_count,
|
|
126
|
+
sha256: segment.sha256
|
|
127
|
+
}))
|
|
128
|
+
}))
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function verifyReportingRowManifestsV1(manifests, expected) {
|
|
133
|
+
if (reportingRowManifestSha256V1(manifests) !== expected.rowManifestSha256.toLowerCase()) {
|
|
134
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", "chunk manifest digest does not match the header");
|
|
135
|
+
}
|
|
136
|
+
let nextOrdinal = 0;
|
|
137
|
+
manifests.forEach((manifest, index) => {
|
|
138
|
+
if (manifest.chunk_index !== index || manifest.first_ordinal !== nextOrdinal) {
|
|
139
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} is out of order`);
|
|
140
|
+
}
|
|
141
|
+
if (manifest.row_count < 1 || manifest.row_count > REPORTING_ROW_CHUNK_MAX_ROWS || manifest.segments.length < 1 || manifest.segments.length > REPORTING_ROW_CHUNK_MAX_SEGMENTS) {
|
|
142
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} exceeds contract limits`);
|
|
143
|
+
}
|
|
144
|
+
let segmentOrdinal = manifest.first_ordinal;
|
|
145
|
+
let segmentOffset = 0;
|
|
146
|
+
manifest.segments.forEach((segment, segmentIndex) => {
|
|
147
|
+
const last = segmentIndex === manifest.segments.length - 1;
|
|
148
|
+
if (segment.first_ordinal !== segmentOrdinal || segment.byte_offset !== segmentOffset || segment.row_count < 1 || segment.byte_count < 1 || segment.row_count > REPORTING_ROW_SEGMENT_MAX_ROWS || !last && segment.row_count !== REPORTING_ROW_SEGMENT_MAX_ROWS) {
|
|
149
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} segment ${segmentIndex} is invalid`);
|
|
150
|
+
}
|
|
151
|
+
segmentOrdinal += segment.row_count;
|
|
152
|
+
segmentOffset += segment.byte_count;
|
|
153
|
+
});
|
|
154
|
+
if (segmentOrdinal !== manifest.first_ordinal + manifest.row_count || segmentOffset !== manifest.byte_count) {
|
|
155
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} segments do not tile the chunk`);
|
|
156
|
+
}
|
|
157
|
+
if (manifest.row_count > 1 && manifest.byte_count > REPORTING_ROW_CHUNK_MAX_BYTES) {
|
|
158
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} exceeds the byte limit`);
|
|
159
|
+
}
|
|
160
|
+
nextOrdinal += manifest.row_count;
|
|
161
|
+
});
|
|
162
|
+
if (nextOrdinal !== expected.rowCount) {
|
|
163
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", "chunk manifests do not cover row_count");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function verifyReportingRowChunkV1(manifest, bytes) {
|
|
167
|
+
const buffer = toBuffer(bytes);
|
|
168
|
+
if (buffer.byteLength !== manifest.byte_count || sha256Hex(buffer) !== manifest.sha256) {
|
|
169
|
+
throw new ReportingRowEncodingError("CHUNK_INTEGRITY_FAILED", `chunk ${manifest.chunk_index} digest mismatch`);
|
|
170
|
+
}
|
|
171
|
+
manifest.segments.forEach((segment, index) => {
|
|
172
|
+
verifySegment(manifest, index, buffer.subarray(segment.byte_offset, segment.byte_offset + segment.byte_count));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function decodeVerifiedReportingRowSegmentsV1(manifest, segmentRange, bytes) {
|
|
176
|
+
const [from, to] = segmentRange;
|
|
177
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < from || to >= manifest.segments.length) {
|
|
178
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", "segment range is outside the chunk");
|
|
179
|
+
}
|
|
180
|
+
const buffer = toBuffer(bytes);
|
|
181
|
+
const base = manifest.segments[from].byte_offset;
|
|
182
|
+
const end = manifest.segments[to].byte_offset + manifest.segments[to].byte_count;
|
|
183
|
+
if (buffer.byteLength !== end - base) {
|
|
184
|
+
throw new ReportingRowEncodingError("CHUNK_INTEGRITY_FAILED", "segment range byte length mismatch");
|
|
185
|
+
}
|
|
186
|
+
for (let index = from; index <= to; index += 1) {
|
|
187
|
+
const segment = manifest.segments[index];
|
|
188
|
+
verifySegment(
|
|
189
|
+
manifest,
|
|
190
|
+
index,
|
|
191
|
+
buffer.subarray(segment.byte_offset - base, segment.byte_offset - base + segment.byte_count)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return splitLines(buffer).map((line) => JSON.parse(line.toString("utf8")));
|
|
195
|
+
}
|
|
196
|
+
function createReportingRevisionEnvelopeHasherV1(envelope) {
|
|
197
|
+
const prefix = Buffer.from(
|
|
198
|
+
`{"control_totals":${(0, import_jcs.canonicalize)(envelope.control_totals)},"reporting_revision_id":${(0, import_jcs.canonicalize)(
|
|
199
|
+
envelope.reporting_revision_id
|
|
200
|
+
)},"reporting_rows":[`,
|
|
201
|
+
"utf8"
|
|
202
|
+
);
|
|
203
|
+
const suffix = Buffer.from(`],"row_count":${(0, import_jcs.canonicalize)(envelope.row_count)}}`, "utf8");
|
|
204
|
+
return createJoinedRowsHasher(prefix, suffix, envelope.row_count);
|
|
205
|
+
}
|
|
206
|
+
function reportingRevisionEnvelopeDigestV1(envelope, chunks) {
|
|
207
|
+
const hasher = createReportingRevisionEnvelopeHasherV1(envelope);
|
|
208
|
+
for (const chunk of chunks) hasher.update(chunk);
|
|
209
|
+
return hasher.digest();
|
|
210
|
+
}
|
|
211
|
+
function createReportingRowsOnlyHasherV1(rowCount) {
|
|
212
|
+
return createJoinedRowsHasher(Buffer.from("[", "utf8"), Buffer.from("]", "utf8"), rowCount);
|
|
213
|
+
}
|
|
214
|
+
function assertReportingRevisionEnvelopeV1(envelope, chunks, expected) {
|
|
215
|
+
const actual = reportingRevisionEnvelopeDigestV1(envelope, chunks);
|
|
216
|
+
if (actual.sha256 !== expected.sha256.toLowerCase() || expected.byteCount !== void 0 && actual.byteCount !== expected.byteCount) {
|
|
217
|
+
throw new ReportingRowEncodingError("ENVELOPE_INTEGRITY_FAILED", "revision content binding mismatch");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function buildChunk(chunkIndex, firstOrdinal, lines) {
|
|
221
|
+
const segments = [];
|
|
222
|
+
let offset = 0;
|
|
223
|
+
for (let start = 0; start < lines.length; start += REPORTING_ROW_SEGMENT_MAX_ROWS) {
|
|
224
|
+
const segmentLines = lines.slice(start, start + REPORTING_ROW_SEGMENT_MAX_ROWS);
|
|
225
|
+
const segmentBytes = Buffer.concat(segmentLines);
|
|
226
|
+
segments.push({
|
|
227
|
+
first_ordinal: firstOrdinal + start,
|
|
228
|
+
row_count: segmentLines.length,
|
|
229
|
+
byte_offset: offset,
|
|
230
|
+
byte_count: segmentBytes.byteLength,
|
|
231
|
+
sha256: sha256Hex(segmentBytes)
|
|
232
|
+
});
|
|
233
|
+
offset += segmentBytes.byteLength;
|
|
234
|
+
}
|
|
235
|
+
const bytes = Buffer.concat(lines);
|
|
236
|
+
return {
|
|
237
|
+
manifest: {
|
|
238
|
+
chunk_index: chunkIndex,
|
|
239
|
+
first_ordinal: firstOrdinal,
|
|
240
|
+
row_count: lines.length,
|
|
241
|
+
byte_count: bytes.byteLength,
|
|
242
|
+
sha256: sha256Hex(bytes),
|
|
243
|
+
segments
|
|
244
|
+
},
|
|
245
|
+
bytes
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function verifySegment(manifest, index, bytes) {
|
|
249
|
+
const segment = manifest.segments[index];
|
|
250
|
+
if (bytes.byteLength !== segment.byte_count || sha256Hex(bytes) !== segment.sha256) {
|
|
251
|
+
throw new ReportingRowEncodingError(
|
|
252
|
+
"CHUNK_INTEGRITY_FAILED",
|
|
253
|
+
`chunk ${manifest.chunk_index} segment ${index} digest mismatch`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
let lines = 0;
|
|
257
|
+
for (const byte of bytes) if (byte === NEWLINE) lines += 1;
|
|
258
|
+
if (lines !== segment.row_count || bytes[bytes.byteLength - 1] !== NEWLINE) {
|
|
259
|
+
throw new ReportingRowEncodingError(
|
|
260
|
+
"CHUNK_INTEGRITY_FAILED",
|
|
261
|
+
`chunk ${manifest.chunk_index} segment ${index} line count mismatch`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function createJoinedRowsHasher(prefix, suffix, expectedRows) {
|
|
266
|
+
const hash = (0, import_crypto.createHash)("sha256").update(prefix);
|
|
267
|
+
let byteCount = prefix.byteLength;
|
|
268
|
+
let rows = 0;
|
|
269
|
+
let finished = false;
|
|
270
|
+
return {
|
|
271
|
+
update(chunkBytes) {
|
|
272
|
+
if (finished) throw new Error("Reporting row hasher already finished");
|
|
273
|
+
const buffer = toBuffer(chunkBytes);
|
|
274
|
+
if (buffer.byteLength > 0 && buffer[buffer.byteLength - 1] !== NEWLINE) {
|
|
275
|
+
throw new ReportingRowEncodingError("CHUNK_INTEGRITY_FAILED", "chunk does not end at a row boundary");
|
|
276
|
+
}
|
|
277
|
+
for (const line of splitLines(buffer)) {
|
|
278
|
+
if (rows > 0) {
|
|
279
|
+
hash.update(COMMA);
|
|
280
|
+
byteCount += 1;
|
|
281
|
+
}
|
|
282
|
+
hash.update(line);
|
|
283
|
+
byteCount += line.byteLength;
|
|
284
|
+
rows += 1;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
digest() {
|
|
288
|
+
if (finished) throw new Error("Reporting row hasher already finished");
|
|
289
|
+
finished = true;
|
|
290
|
+
if (rows !== expectedRows) {
|
|
291
|
+
throw new ReportingRowEncodingError("ENVELOPE_INTEGRITY_FAILED", "row count does not match the header");
|
|
292
|
+
}
|
|
293
|
+
hash.update(suffix);
|
|
294
|
+
return { sha256: hash.digest("hex"), byteCount: byteCount + suffix.byteLength };
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function splitLines(buffer) {
|
|
299
|
+
const lines = [];
|
|
300
|
+
let start = 0;
|
|
301
|
+
for (let index = 0; index < buffer.byteLength; index += 1) {
|
|
302
|
+
if (buffer[index] === NEWLINE) {
|
|
303
|
+
lines.push(buffer.subarray(start, index));
|
|
304
|
+
start = index + 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (start !== buffer.byteLength) {
|
|
308
|
+
throw new ReportingRowEncodingError("CHUNK_INTEGRITY_FAILED", "trailing bytes after the last row");
|
|
309
|
+
}
|
|
310
|
+
return lines;
|
|
311
|
+
}
|
|
312
|
+
function assertPortableNumbers(value, ordinal) {
|
|
313
|
+
if (typeof value === "number") {
|
|
314
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
315
|
+
throw new ReportingRowEncodingError(
|
|
316
|
+
"INVALID_ROW",
|
|
317
|
+
`row ${ordinal} contains a number outside the portable range; encode it as a string`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
for (const item of value) assertPortableNumbers(item, ordinal);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (value && typeof value === "object") {
|
|
327
|
+
for (const item of Object.values(value)) assertPortableNumbers(item, ordinal);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function toBuffer(bytes) {
|
|
331
|
+
return Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
332
|
+
}
|
|
333
|
+
function sha256Hex(value) {
|
|
334
|
+
return (0, import_crypto.createHash)("sha256").update(value).digest("hex");
|
|
335
|
+
}
|
|
336
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
337
|
+
0 && (module.exports = {
|
|
338
|
+
REPORTING_ROW_CHUNK_MAX_BYTES,
|
|
339
|
+
REPORTING_ROW_CHUNK_MAX_ROWS,
|
|
340
|
+
REPORTING_ROW_CHUNK_MAX_SEGMENTS,
|
|
341
|
+
REPORTING_ROW_ENCODING_V1,
|
|
342
|
+
REPORTING_ROW_SEGMENT_MAX_ROWS,
|
|
343
|
+
ReportingRowEncodingError,
|
|
344
|
+
assertReportingRevisionEnvelopeV1,
|
|
345
|
+
canonicalReportingRowV1,
|
|
346
|
+
createReportingRevisionEnvelopeHasherV1,
|
|
347
|
+
createReportingRowsOnlyHasherV1,
|
|
348
|
+
decodeVerifiedReportingRowSegmentsV1,
|
|
349
|
+
encodeReportingRowsV1,
|
|
350
|
+
isReportingRowEncodingError,
|
|
351
|
+
reportingRevisionEnvelopeDigestV1,
|
|
352
|
+
reportingRowManifestSha256V1,
|
|
353
|
+
verifyReportingRowChunkV1,
|
|
354
|
+
verifyReportingRowManifestsV1
|
|
355
|
+
});
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { canonicalize } from "../../utils/jcs.mjs";
|
|
3
|
+
const REPORTING_ROW_ENCODING_V1 = "adcp_canonical_jsonl_v1";
|
|
4
|
+
const REPORTING_ROW_SEGMENT_MAX_ROWS = 500;
|
|
5
|
+
const REPORTING_ROW_CHUNK_MAX_SEGMENTS = 20;
|
|
6
|
+
const REPORTING_ROW_CHUNK_MAX_ROWS = REPORTING_ROW_SEGMENT_MAX_ROWS * REPORTING_ROW_CHUNK_MAX_SEGMENTS;
|
|
7
|
+
const REPORTING_ROW_CHUNK_MAX_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
const NEWLINE = 10;
|
|
9
|
+
const COMMA = Buffer.from(",", "utf8");
|
|
10
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
11
|
+
class ReportingRowEncodingError extends Error {
|
|
12
|
+
constructor(code, detail) {
|
|
13
|
+
super(`Reporting row encoding: ${code}: ${detail}`);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "ReportingRowEncodingError";
|
|
16
|
+
Object.defineProperty(this, /* @__PURE__ */ Symbol.for("adcp.reportingRowEncodingError"), { value: true });
|
|
17
|
+
}
|
|
18
|
+
code;
|
|
19
|
+
}
|
|
20
|
+
function isReportingRowEncodingError(error) {
|
|
21
|
+
return !!error && typeof error === "object" && error[/* @__PURE__ */ Symbol.for("adcp.reportingRowEncodingError")] === true;
|
|
22
|
+
}
|
|
23
|
+
function canonicalReportingRowV1(row, ordinal = 0) {
|
|
24
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
25
|
+
throw new ReportingRowEncodingError("INVALID_ROW", `row ${ordinal} is not a JSON object`);
|
|
26
|
+
}
|
|
27
|
+
assertPortableNumbers(row, ordinal);
|
|
28
|
+
let canonical;
|
|
29
|
+
try {
|
|
30
|
+
canonical = canonicalize(row);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new ReportingRowEncodingError(
|
|
33
|
+
"INVALID_ROW",
|
|
34
|
+
`row ${ordinal} is not canonical JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (LONE_SURROGATE.test(canonical)) {
|
|
38
|
+
throw new ReportingRowEncodingError("INVALID_ROW", `row ${ordinal} contains a lone surrogate`);
|
|
39
|
+
}
|
|
40
|
+
return canonical;
|
|
41
|
+
}
|
|
42
|
+
function encodeReportingRowsV1(rows) {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
let chunkLines = [];
|
|
45
|
+
let chunkBytes = 0;
|
|
46
|
+
let chunkFirstOrdinal = 0;
|
|
47
|
+
const closeChunk = () => {
|
|
48
|
+
if (chunkLines.length === 0) return;
|
|
49
|
+
chunks.push(buildChunk(chunks.length, chunkFirstOrdinal, chunkLines));
|
|
50
|
+
chunkLines = [];
|
|
51
|
+
chunkBytes = 0;
|
|
52
|
+
};
|
|
53
|
+
rows.forEach((row, ordinal) => {
|
|
54
|
+
const line = Buffer.from(canonicalReportingRowV1(row, ordinal) + "\n", "utf8");
|
|
55
|
+
if (chunkLines.length === REPORTING_ROW_CHUNK_MAX_ROWS || chunkLines.length > 0 && chunkBytes + line.byteLength > REPORTING_ROW_CHUNK_MAX_BYTES) {
|
|
56
|
+
closeChunk();
|
|
57
|
+
}
|
|
58
|
+
if (chunkLines.length === 0) chunkFirstOrdinal = ordinal;
|
|
59
|
+
chunkLines.push(line);
|
|
60
|
+
chunkBytes += line.byteLength;
|
|
61
|
+
});
|
|
62
|
+
closeChunk();
|
|
63
|
+
const manifests = chunks.map((chunk) => chunk.manifest);
|
|
64
|
+
return {
|
|
65
|
+
encoding: REPORTING_ROW_ENCODING_V1,
|
|
66
|
+
rowCount: rows.length,
|
|
67
|
+
byteCount: chunks.reduce((total, chunk) => total + chunk.manifest.byte_count, 0),
|
|
68
|
+
chunks,
|
|
69
|
+
manifests,
|
|
70
|
+
rowManifestSha256: reportingRowManifestSha256V1(manifests)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function reportingRowManifestSha256V1(manifests) {
|
|
74
|
+
return sha256Hex(
|
|
75
|
+
canonicalize(
|
|
76
|
+
manifests.map((manifest) => ({
|
|
77
|
+
chunk_index: manifest.chunk_index,
|
|
78
|
+
first_ordinal: manifest.first_ordinal,
|
|
79
|
+
row_count: manifest.row_count,
|
|
80
|
+
byte_count: manifest.byte_count,
|
|
81
|
+
sha256: manifest.sha256,
|
|
82
|
+
segments: manifest.segments.map((segment) => ({
|
|
83
|
+
first_ordinal: segment.first_ordinal,
|
|
84
|
+
row_count: segment.row_count,
|
|
85
|
+
byte_offset: segment.byte_offset,
|
|
86
|
+
byte_count: segment.byte_count,
|
|
87
|
+
sha256: segment.sha256
|
|
88
|
+
}))
|
|
89
|
+
}))
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
function verifyReportingRowManifestsV1(manifests, expected) {
|
|
94
|
+
if (reportingRowManifestSha256V1(manifests) !== expected.rowManifestSha256.toLowerCase()) {
|
|
95
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", "chunk manifest digest does not match the header");
|
|
96
|
+
}
|
|
97
|
+
let nextOrdinal = 0;
|
|
98
|
+
manifests.forEach((manifest, index) => {
|
|
99
|
+
if (manifest.chunk_index !== index || manifest.first_ordinal !== nextOrdinal) {
|
|
100
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} is out of order`);
|
|
101
|
+
}
|
|
102
|
+
if (manifest.row_count < 1 || manifest.row_count > REPORTING_ROW_CHUNK_MAX_ROWS || manifest.segments.length < 1 || manifest.segments.length > REPORTING_ROW_CHUNK_MAX_SEGMENTS) {
|
|
103
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} exceeds contract limits`);
|
|
104
|
+
}
|
|
105
|
+
let segmentOrdinal = manifest.first_ordinal;
|
|
106
|
+
let segmentOffset = 0;
|
|
107
|
+
manifest.segments.forEach((segment, segmentIndex) => {
|
|
108
|
+
const last = segmentIndex === manifest.segments.length - 1;
|
|
109
|
+
if (segment.first_ordinal !== segmentOrdinal || segment.byte_offset !== segmentOffset || segment.row_count < 1 || segment.byte_count < 1 || segment.row_count > REPORTING_ROW_SEGMENT_MAX_ROWS || !last && segment.row_count !== REPORTING_ROW_SEGMENT_MAX_ROWS) {
|
|
110
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} segment ${segmentIndex} is invalid`);
|
|
111
|
+
}
|
|
112
|
+
segmentOrdinal += segment.row_count;
|
|
113
|
+
segmentOffset += segment.byte_count;
|
|
114
|
+
});
|
|
115
|
+
if (segmentOrdinal !== manifest.first_ordinal + manifest.row_count || segmentOffset !== manifest.byte_count) {
|
|
116
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} segments do not tile the chunk`);
|
|
117
|
+
}
|
|
118
|
+
if (manifest.row_count > 1 && manifest.byte_count > REPORTING_ROW_CHUNK_MAX_BYTES) {
|
|
119
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", `chunk ${index} exceeds the byte limit`);
|
|
120
|
+
}
|
|
121
|
+
nextOrdinal += manifest.row_count;
|
|
122
|
+
});
|
|
123
|
+
if (nextOrdinal !== expected.rowCount) {
|
|
124
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", "chunk manifests do not cover row_count");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function verifyReportingRowChunkV1(manifest, bytes) {
|
|
128
|
+
const buffer = toBuffer(bytes);
|
|
129
|
+
if (buffer.byteLength !== manifest.byte_count || sha256Hex(buffer) !== manifest.sha256) {
|
|
130
|
+
throw new ReportingRowEncodingError("CHUNK_INTEGRITY_FAILED", `chunk ${manifest.chunk_index} digest mismatch`);
|
|
131
|
+
}
|
|
132
|
+
manifest.segments.forEach((segment, index) => {
|
|
133
|
+
verifySegment(manifest, index, buffer.subarray(segment.byte_offset, segment.byte_offset + segment.byte_count));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function decodeVerifiedReportingRowSegmentsV1(manifest, segmentRange, bytes) {
|
|
137
|
+
const [from, to] = segmentRange;
|
|
138
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < from || to >= manifest.segments.length) {
|
|
139
|
+
throw new ReportingRowEncodingError("MANIFEST_MISMATCH", "segment range is outside the chunk");
|
|
140
|
+
}
|
|
141
|
+
const buffer = toBuffer(bytes);
|
|
142
|
+
const base = manifest.segments[from].byte_offset;
|
|
143
|
+
const end = manifest.segments[to].byte_offset + manifest.segments[to].byte_count;
|
|
144
|
+
if (buffer.byteLength !== end - base) {
|
|
145
|
+
throw new ReportingRowEncodingError("CHUNK_INTEGRITY_FAILED", "segment range byte length mismatch");
|
|
146
|
+
}
|
|
147
|
+
for (let index = from; index <= to; index += 1) {
|
|
148
|
+
const segment = manifest.segments[index];
|
|
149
|
+
verifySegment(
|
|
150
|
+
manifest,
|
|
151
|
+
index,
|
|
152
|
+
buffer.subarray(segment.byte_offset - base, segment.byte_offset - base + segment.byte_count)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return splitLines(buffer).map((line) => JSON.parse(line.toString("utf8")));
|
|
156
|
+
}
|
|
157
|
+
function createReportingRevisionEnvelopeHasherV1(envelope) {
|
|
158
|
+
const prefix = Buffer.from(
|
|
159
|
+
`{"control_totals":${canonicalize(envelope.control_totals)},"reporting_revision_id":${canonicalize(
|
|
160
|
+
envelope.reporting_revision_id
|
|
161
|
+
)},"reporting_rows":[`,
|
|
162
|
+
"utf8"
|
|
163
|
+
);
|
|
164
|
+
const suffix = Buffer.from(`],"row_count":${canonicalize(envelope.row_count)}}`, "utf8");
|
|
165
|
+
return createJoinedRowsHasher(prefix, suffix, envelope.row_count);
|
|
166
|
+
}
|
|
167
|
+
function reportingRevisionEnvelopeDigestV1(envelope, chunks) {
|
|
168
|
+
const hasher = createReportingRevisionEnvelopeHasherV1(envelope);
|
|
169
|
+
for (const chunk of chunks) hasher.update(chunk);
|
|
170
|
+
return hasher.digest();
|
|
171
|
+
}
|
|
172
|
+
function createReportingRowsOnlyHasherV1(rowCount) {
|
|
173
|
+
return createJoinedRowsHasher(Buffer.from("[", "utf8"), Buffer.from("]", "utf8"), rowCount);
|
|
174
|
+
}
|
|
175
|
+
function assertReportingRevisionEnvelopeV1(envelope, chunks, expected) {
|
|
176
|
+
const actual = reportingRevisionEnvelopeDigestV1(envelope, chunks);
|
|
177
|
+
if (actual.sha256 !== expected.sha256.toLowerCase() || expected.byteCount !== void 0 && actual.byteCount !== expected.byteCount) {
|
|
178
|
+
throw new ReportingRowEncodingError("ENVELOPE_INTEGRITY_FAILED", "revision content binding mismatch");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function buildChunk(chunkIndex, firstOrdinal, lines) {
|
|
182
|
+
const segments = [];
|
|
183
|
+
let offset = 0;
|
|
184
|
+
for (let start = 0; start < lines.length; start += REPORTING_ROW_SEGMENT_MAX_ROWS) {
|
|
185
|
+
const segmentLines = lines.slice(start, start + REPORTING_ROW_SEGMENT_MAX_ROWS);
|
|
186
|
+
const segmentBytes = Buffer.concat(segmentLines);
|
|
187
|
+
segments.push({
|
|
188
|
+
first_ordinal: firstOrdinal + start,
|
|
189
|
+
row_count: segmentLines.length,
|
|
190
|
+
byte_offset: offset,
|
|
191
|
+
byte_count: segmentBytes.byteLength,
|
|
192
|
+
sha256: sha256Hex(segmentBytes)
|
|
193
|
+
});
|
|
194
|
+
offset += segmentBytes.byteLength;
|
|
195
|
+
}
|
|
196
|
+
const bytes = Buffer.concat(lines);
|
|
197
|
+
return {
|
|
198
|
+
manifest: {
|
|
199
|
+
chunk_index: chunkIndex,
|
|
200
|
+
first_ordinal: firstOrdinal,
|
|
201
|
+
row_count: lines.length,
|
|
202
|
+
byte_count: bytes.byteLength,
|
|
203
|
+
sha256: sha256Hex(bytes),
|
|
204
|
+
segments
|
|
205
|
+
},
|
|
206
|
+
bytes
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function verifySegment(manifest, index, bytes) {
|
|
210
|
+
const segment = manifest.segments[index];
|
|
211
|
+
if (bytes.byteLength !== segment.byte_count || sha256Hex(bytes) !== segment.sha256) {
|
|
212
|
+
throw new ReportingRowEncodingError(
|
|
213
|
+
"CHUNK_INTEGRITY_FAILED",
|
|
214
|
+
`chunk ${manifest.chunk_index} segment ${index} digest mismatch`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
let lines = 0;
|
|
218
|
+
for (const byte of bytes) if (byte === NEWLINE) lines += 1;
|
|
219
|
+
if (lines !== segment.row_count || bytes[bytes.byteLength - 1] !== NEWLINE) {
|
|
220
|
+
throw new ReportingRowEncodingError(
|
|
221
|
+
"CHUNK_INTEGRITY_FAILED",
|
|
222
|
+
`chunk ${manifest.chunk_index} segment ${index} line count mismatch`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function createJoinedRowsHasher(prefix, suffix, expectedRows) {
|
|
227
|
+
const hash = createHash("sha256").update(prefix);
|
|
228
|
+
let byteCount = prefix.byteLength;
|
|
229
|
+
let rows = 0;
|
|
230
|
+
let finished = false;
|
|
231
|
+
return {
|
|
232
|
+
update(chunkBytes) {
|
|
233
|
+
if (finished) throw new Error("Reporting row hasher already finished");
|
|
234
|
+
const buffer = toBuffer(chunkBytes);
|
|
235
|
+
if (buffer.byteLength > 0 && buffer[buffer.byteLength - 1] !== NEWLINE) {
|
|
236
|
+
throw new ReportingRowEncodingError("CHUNK_INTEGRITY_FAILED", "chunk does not end at a row boundary");
|
|
237
|
+
}
|
|
238
|
+
for (const line of splitLines(buffer)) {
|
|
239
|
+
if (rows > 0) {
|
|
240
|
+
hash.update(COMMA);
|
|
241
|
+
byteCount += 1;
|
|
242
|
+
}
|
|
243
|
+
hash.update(line);
|
|
244
|
+
byteCount += line.byteLength;
|
|
245
|
+
rows += 1;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
digest() {
|
|
249
|
+
if (finished) throw new Error("Reporting row hasher already finished");
|
|
250
|
+
finished = true;
|
|
251
|
+
if (rows !== expectedRows) {
|
|
252
|
+
throw new ReportingRowEncodingError("ENVELOPE_INTEGRITY_FAILED", "row count does not match the header");
|
|
253
|
+
}
|
|
254
|
+
hash.update(suffix);
|
|
255
|
+
return { sha256: hash.digest("hex"), byteCount: byteCount + suffix.byteLength };
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function splitLines(buffer) {
|
|
260
|
+
const lines = [];
|
|
261
|
+
let start = 0;
|
|
262
|
+
for (let index = 0; index < buffer.byteLength; index += 1) {
|
|
263
|
+
if (buffer[index] === NEWLINE) {
|
|
264
|
+
lines.push(buffer.subarray(start, index));
|
|
265
|
+
start = index + 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (start !== buffer.byteLength) {
|
|
269
|
+
throw new ReportingRowEncodingError("CHUNK_INTEGRITY_FAILED", "trailing bytes after the last row");
|
|
270
|
+
}
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
function assertPortableNumbers(value, ordinal) {
|
|
274
|
+
if (typeof value === "number") {
|
|
275
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
276
|
+
throw new ReportingRowEncodingError(
|
|
277
|
+
"INVALID_ROW",
|
|
278
|
+
`row ${ordinal} contains a number outside the portable range; encode it as a string`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (Array.isArray(value)) {
|
|
284
|
+
for (const item of value) assertPortableNumbers(item, ordinal);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (value && typeof value === "object") {
|
|
288
|
+
for (const item of Object.values(value)) assertPortableNumbers(item, ordinal);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function toBuffer(bytes) {
|
|
292
|
+
return Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
293
|
+
}
|
|
294
|
+
function sha256Hex(value) {
|
|
295
|
+
return createHash("sha256").update(value).digest("hex");
|
|
296
|
+
}
|
|
297
|
+
export {
|
|
298
|
+
REPORTING_ROW_CHUNK_MAX_BYTES,
|
|
299
|
+
REPORTING_ROW_CHUNK_MAX_ROWS,
|
|
300
|
+
REPORTING_ROW_CHUNK_MAX_SEGMENTS,
|
|
301
|
+
REPORTING_ROW_ENCODING_V1,
|
|
302
|
+
REPORTING_ROW_SEGMENT_MAX_ROWS,
|
|
303
|
+
ReportingRowEncodingError,
|
|
304
|
+
assertReportingRevisionEnvelopeV1,
|
|
305
|
+
canonicalReportingRowV1,
|
|
306
|
+
createReportingRevisionEnvelopeHasherV1,
|
|
307
|
+
createReportingRowsOnlyHasherV1,
|
|
308
|
+
decodeVerifiedReportingRowSegmentsV1,
|
|
309
|
+
encodeReportingRowsV1,
|
|
310
|
+
isReportingRowEncodingError,
|
|
311
|
+
reportingRevisionEnvelopeDigestV1,
|
|
312
|
+
reportingRowManifestSha256V1,
|
|
313
|
+
verifyReportingRowChunkV1,
|
|
314
|
+
verifyReportingRowManifestsV1
|
|
315
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ReportingRowStoreErrorCode = 'INVALID_INPUT' | 'CONTENT_CONFLICT' | 'ROWS_INTEGRITY_FAILED' | 'ROWS_UNAVAILABLE' | 'ROWS_EXPIRED' | 'PROVIDER_UNAVAILABLE' | 'DEADLINE_EXCEEDED' | 'ABORTED' | 'UNSAFE_BINDING' | 'STATE_UNAVAILABLE';
|
|
2
|
+
/** Stable, secret-free row-storage failure. Switch on `code`. */
|
|
3
|
+
export declare class ReportingRowStoreError extends Error {
|
|
4
|
+
readonly code: ReportingRowStoreErrorCode;
|
|
5
|
+
constructor(code: ReportingRowStoreErrorCode, detail?: string);
|
|
6
|
+
}
|
|
7
|
+
export declare function isReportingRowStoreError(error: unknown): error is ReportingRowStoreError;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ReportingRowStoreErrorCode = 'INVALID_INPUT' | 'CONTENT_CONFLICT' | 'ROWS_INTEGRITY_FAILED' | 'ROWS_UNAVAILABLE' | 'ROWS_EXPIRED' | 'PROVIDER_UNAVAILABLE' | 'DEADLINE_EXCEEDED' | 'ABORTED' | 'UNSAFE_BINDING' | 'STATE_UNAVAILABLE';
|
|
2
|
+
/** Stable, secret-free row-storage failure. Switch on `code`. */
|
|
3
|
+
export declare class ReportingRowStoreError extends Error {
|
|
4
|
+
readonly code: ReportingRowStoreErrorCode;
|
|
5
|
+
constructor(code: ReportingRowStoreErrorCode, detail?: string);
|
|
6
|
+
}
|
|
7
|
+
export declare function isReportingRowStoreError(error: unknown): error is ReportingRowStoreError;
|