@adcp/sdk 15.2.2 → 15.4.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/core/ConversationTypes.d.mts +5 -0
- package/dist/lib/core/ConversationTypes.d.ts +5 -0
- package/dist/lib/core/SingleAgentClient.d.mts +19 -1
- package/dist/lib/core/SingleAgentClient.d.ts +19 -1
- package/dist/lib/core/SingleAgentClient.js +38 -7
- package/dist/lib/core/SingleAgentClient.mjs +38 -7
- package/dist/lib/index.d.mts +1 -1
- package/dist/lib/index.d.ts +1 -1
- 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 +36 -7
- package/dist/lib/reporting/ledger/handler.mjs +35 -7
- package/dist/lib/reporting/ledger/index.d.mts +1 -0
- package/dist/lib/reporting/ledger/index.d.ts +1 -0
- package/dist/lib/reporting/ledger/index.js +3 -1
- package/dist/lib/reporting/ledger/index.mjs +1 -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.js +5 -5
- package/dist/lib/reporting/ledger/managed-postgres.mjs +5 -5
- package/dist/lib/reporting/ledger/postgres.d.mts +4 -1
- package/dist/lib/reporting/ledger/postgres.d.ts +4 -1
- package/dist/lib/reporting/ledger/postgres.js +38 -0
- package/dist/lib/reporting/ledger/postgres.mjs +38 -0
- package/dist/lib/reporting/ledger/producer.js +3 -2
- package/dist/lib/reporting/ledger/producer.mjs +3 -2
- 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/types.d.mts +37 -0
- package/dist/lib/reporting/ledger/types.d.ts +37 -0
- package/dist/lib/schemas-data/v2.5/_provenance.json +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/TYPE-SUMMARY.md +2 -1
- package/docs/guides/PUSH-NOTIFICATION-CONFIG.md +39 -0
- package/docs/llms.txt +1 -1
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSONL row encoding for reporting revisions (shared SDK
|
|
3
|
+
* persistence contract, `reporting-row-storage` v1).
|
|
4
|
+
*
|
|
5
|
+
* Rows are stored as `JCS(row) + "\n"` in ordinal order. JCS escapes newline
|
|
6
|
+
* characters inside strings, so the `0x0A` byte only ever separates rows.
|
|
7
|
+
* Rows are grouped into segments (the unit of verification and paged reads)
|
|
8
|
+
* and chunks (the unit of storage). Boundaries are fixed by the contract so
|
|
9
|
+
* every SDK produces byte-identical chunks and manifests for the same rows:
|
|
10
|
+
*
|
|
11
|
+
* - a new chunk starts when the current chunk holds
|
|
12
|
+
* {@link REPORTING_ROW_CHUNK_MAX_ROWS} rows, or when appending the next row
|
|
13
|
+
* would exceed {@link REPORTING_ROW_CHUNK_MAX_BYTES} canonical bytes;
|
|
14
|
+
* - segments restart at each chunk boundary and close every
|
|
15
|
+
* {@link REPORTING_ROW_SEGMENT_MAX_ROWS} rows.
|
|
16
|
+
*
|
|
17
|
+
* A row larger than the chunk byte limit forms its own segment and chunk.
|
|
18
|
+
* Digests always cover uncompressed canonical bytes.
|
|
19
|
+
*/
|
|
20
|
+
export declare const REPORTING_ROW_ENCODING_V1: "adcp_canonical_jsonl_v1";
|
|
21
|
+
export declare const REPORTING_ROW_SEGMENT_MAX_ROWS = 500;
|
|
22
|
+
export declare const REPORTING_ROW_CHUNK_MAX_SEGMENTS = 20;
|
|
23
|
+
export declare const REPORTING_ROW_CHUNK_MAX_ROWS: number;
|
|
24
|
+
export declare const REPORTING_ROW_CHUNK_MAX_BYTES: number;
|
|
25
|
+
export type ReportingRowEncodingErrorCode = 'INVALID_ROW' | 'MANIFEST_MISMATCH' | 'CHUNK_INTEGRITY_FAILED' | 'ENVELOPE_INTEGRITY_FAILED';
|
|
26
|
+
export declare class ReportingRowEncodingError extends Error {
|
|
27
|
+
readonly code: ReportingRowEncodingErrorCode;
|
|
28
|
+
constructor(code: ReportingRowEncodingErrorCode, detail: string);
|
|
29
|
+
}
|
|
30
|
+
export declare function isReportingRowEncodingError(error: unknown): error is ReportingRowEncodingError;
|
|
31
|
+
export interface ReportingRowSegmentManifestV1 {
|
|
32
|
+
first_ordinal: number;
|
|
33
|
+
row_count: number;
|
|
34
|
+
/** Offset of the segment within its chunk's uncompressed canonical bytes. */
|
|
35
|
+
byte_offset: number;
|
|
36
|
+
byte_count: number;
|
|
37
|
+
sha256: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ReportingRowChunkManifestV1 {
|
|
40
|
+
chunk_index: number;
|
|
41
|
+
first_ordinal: number;
|
|
42
|
+
row_count: number;
|
|
43
|
+
byte_count: number;
|
|
44
|
+
sha256: string;
|
|
45
|
+
segments: ReportingRowSegmentManifestV1[];
|
|
46
|
+
}
|
|
47
|
+
export interface ReportingRowChunkV1 {
|
|
48
|
+
manifest: ReportingRowChunkManifestV1;
|
|
49
|
+
/** Uncompressed canonical JSONL bytes. */
|
|
50
|
+
bytes: Buffer;
|
|
51
|
+
}
|
|
52
|
+
export interface ReportingEncodedRowsV1 {
|
|
53
|
+
encoding: typeof REPORTING_ROW_ENCODING_V1;
|
|
54
|
+
rowCount: number;
|
|
55
|
+
/** Total canonical JSONL bytes across all chunks. */
|
|
56
|
+
byteCount: number;
|
|
57
|
+
chunks: ReportingRowChunkV1[];
|
|
58
|
+
manifests: ReportingRowChunkManifestV1[];
|
|
59
|
+
/** `sha256(JCS(manifests))`; bound immutably on the revision header. */
|
|
60
|
+
rowManifestSha256: string;
|
|
61
|
+
}
|
|
62
|
+
export interface ReportingRevisionEnvelopeInputV1 {
|
|
63
|
+
reporting_revision_id: string;
|
|
64
|
+
row_count: number;
|
|
65
|
+
control_totals: readonly unknown[];
|
|
66
|
+
}
|
|
67
|
+
export interface ReportingRevisionEnvelopeDigestV1 {
|
|
68
|
+
sha256: string;
|
|
69
|
+
/** Byte length of the canonical envelope; equals `binding.byteCount`. */
|
|
70
|
+
byteCount: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Canonicalize one row for storage. Refuses values whose canonical bytes are
|
|
74
|
+
* not portable across conforming JCS implementations: non-plain objects,
|
|
75
|
+
* non-finite numbers, integers outside ±(2^53 − 1), and lone surrogates.
|
|
76
|
+
*/
|
|
77
|
+
export declare function canonicalReportingRowV1(row: unknown, ordinal?: number): string;
|
|
78
|
+
/** Encode rows into contract-fixed canonical JSONL chunks and manifests. */
|
|
79
|
+
export declare function encodeReportingRowsV1(rows: readonly unknown[]): ReportingEncodedRowsV1;
|
|
80
|
+
/** `sha256(JCS(manifests))` over the ordered chunk manifests, locators excluded. */
|
|
81
|
+
export declare function reportingRowManifestSha256V1(manifests: readonly ReportingRowChunkManifestV1[]): string;
|
|
82
|
+
/**
|
|
83
|
+
* Verify that a manifest set is internally consistent and matches the
|
|
84
|
+
* header's committed `row_manifest_sha256` and `row_count`. Ordinals must be
|
|
85
|
+
* contiguous from zero, chunk and segment boundaries must follow the contract
|
|
86
|
+
* limits, and segment byte ranges must tile their chunk exactly.
|
|
87
|
+
*/
|
|
88
|
+
export declare function verifyReportingRowManifestsV1(manifests: readonly ReportingRowChunkManifestV1[], expected: {
|
|
89
|
+
rowCount: number;
|
|
90
|
+
rowManifestSha256: string;
|
|
91
|
+
}): void;
|
|
92
|
+
/**
|
|
93
|
+
* Verify a whole chunk's canonical bytes against its manifest: total digest,
|
|
94
|
+
* byte count, and every segment's digest and line count.
|
|
95
|
+
*/
|
|
96
|
+
export declare function verifyReportingRowChunkV1(manifest: ReportingRowChunkManifestV1, bytes: Uint8Array): void;
|
|
97
|
+
/**
|
|
98
|
+
* Verify and decode a contiguous range of segments `[fromSegment, toSegment]`
|
|
99
|
+
* (inclusive). `bytes` must hold exactly those segments' canonical bytes,
|
|
100
|
+
* starting at the first segment's `byte_offset` — the shape returned by a
|
|
101
|
+
* ranged object read. Rows are released only after every segment verifies.
|
|
102
|
+
*/
|
|
103
|
+
export declare function decodeVerifiedReportingRowSegmentsV1(manifest: ReportingRowChunkManifestV1, segmentRange: readonly [fromSegment: number, toSegment: number], bytes: Uint8Array): Record<string, unknown>[];
|
|
104
|
+
/**
|
|
105
|
+
* Streaming hasher for the protocol revision binding
|
|
106
|
+
* `sha256(JCS({reporting_revision_id, row_count, control_totals, reporting_rows}))`.
|
|
107
|
+
* Feed verified chunk bytes in order; rows are never parsed.
|
|
108
|
+
*/
|
|
109
|
+
export declare function createReportingRevisionEnvelopeHasherV1(envelope: ReportingRevisionEnvelopeInputV1): {
|
|
110
|
+
update(chunkBytes: Uint8Array): void;
|
|
111
|
+
digest(): ReportingRevisionEnvelopeDigestV1;
|
|
112
|
+
};
|
|
113
|
+
/** Envelope digest over already-encoded chunk bytes. */
|
|
114
|
+
export declare function reportingRevisionEnvelopeDigestV1(envelope: ReportingRevisionEnvelopeInputV1, chunks: Iterable<Uint8Array>): ReportingRevisionEnvelopeDigestV1;
|
|
115
|
+
/** Streaming hasher for digest profile `rows_v1`: `sha256(JCS(rows))`. */
|
|
116
|
+
export declare function createReportingRowsOnlyHasherV1(rowCount: number): {
|
|
117
|
+
update(chunkBytes: Uint8Array): void;
|
|
118
|
+
digest(): ReportingRevisionEnvelopeDigestV1;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Throw unless the canonical chunks reproduce the header's committed revision
|
|
122
|
+
* binding. Used at write time (proof that chunks and envelope agree) and for
|
|
123
|
+
* full-revision reads.
|
|
124
|
+
*/
|
|
125
|
+
export declare function assertReportingRevisionEnvelopeV1(envelope: ReportingRevisionEnvelopeInputV1, chunks: Iterable<Uint8Array>, expected: {
|
|
126
|
+
sha256: string;
|
|
127
|
+
byteCount?: number;
|
|
128
|
+
}): void;
|
|
@@ -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
|
+
});
|