@adcp/sdk 15.4.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.js +22 -8
- package/dist/lib/reporting/ledger/handler.mjs +22 -8
- package/dist/lib/reporting/ledger/index.d.mts +4 -0
- package/dist/lib/reporting/ledger/index.d.ts +4 -0
- package/dist/lib/reporting/ledger/index.js +9 -1
- package/dist/lib/reporting/ledger/index.mjs +4 -0
- 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 +21 -1
- package/dist/lib/reporting/ledger/managed-postgres.mjs +21 -1
- 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 +175 -0
- package/dist/lib/reporting/ledger/postgres.d.ts +175 -0
- package/dist/lib/reporting/ledger/postgres.js +851 -26
- package/dist/lib/reporting/ledger/postgres.mjs +859 -26
- package/dist/lib/reporting/ledger/producer.js +10 -1
- package/dist/lib/reporting/ledger/producer.mjs +10 -1
- 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 +8 -0
- package/dist/lib/reporting/ledger/types.d.ts +8 -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/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/package.json +1 -1
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "crypto";
|
|
2
|
+
import { gunzipSync, gzipSync } from "zlib";
|
|
3
|
+
import { canonicalize } from "../../utils/jcs.mjs";
|
|
4
|
+
import {
|
|
5
|
+
REPORTING_ROW_CHUNK_MAX_BYTES,
|
|
6
|
+
REPORTING_ROW_ENCODING_V1,
|
|
7
|
+
createReportingRevisionEnvelopeHasherV1,
|
|
8
|
+
createReportingRowsOnlyHasherV1,
|
|
9
|
+
decodeVerifiedReportingRowSegmentsV1,
|
|
10
|
+
encodeReportingRowsV1,
|
|
11
|
+
isReportingRowEncodingError,
|
|
12
|
+
verifyReportingRowChunkV1,
|
|
13
|
+
verifyReportingRowManifestsV1
|
|
14
|
+
} from "./row-encoding.mjs";
|
|
15
|
+
import { probeReportingRowStorageSchemaV1 } from "./row-storage-migration.mjs";
|
|
16
|
+
import { ReportingRowStoreError, isReportingRowStoreError } from "./row-storage-errors.mjs";
|
|
17
|
+
import {
|
|
18
|
+
REPORTING_ROW_DEFAULT_KEY_TEMPLATE,
|
|
19
|
+
assertReportingRowKeyTemplateV1,
|
|
20
|
+
assertReportingRowPrefixV1,
|
|
21
|
+
renderReportingRowObjectKeyV1
|
|
22
|
+
} from "./row-storage-object.mjs";
|
|
23
|
+
import { ReportingRowStoreError as ReportingRowStoreError2, isReportingRowStoreError as isReportingRowStoreError2 } from "./row-storage-errors.mjs";
|
|
24
|
+
const DEFAULT_BINDING_ID = "postgres";
|
|
25
|
+
const BINDING_ID = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
26
|
+
const CREDENTIAL_REF = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
27
|
+
const MAX_ADOPTED_OBJECT_BYTES = 2 * REPORTING_ROW_CHUNK_MAX_BYTES + 1024 * 1024;
|
|
28
|
+
class ReportingRowStorageV1 {
|
|
29
|
+
constructor(pool, options = {}) {
|
|
30
|
+
this.pool = pool;
|
|
31
|
+
this.options = options;
|
|
32
|
+
const bindings = options.bindings ?? { [DEFAULT_BINDING_ID]: { kind: "postgres" } };
|
|
33
|
+
this.providers = Object.freeze({ ...options.providers ?? {} });
|
|
34
|
+
const ids = Object.keys(bindings);
|
|
35
|
+
if (ids.length === 0) throw new ReportingRowStoreError("INVALID_INPUT", "at least one row binding is required");
|
|
36
|
+
for (const id of ids) {
|
|
37
|
+
if (!BINDING_ID.test(id)) throw new ReportingRowStoreError("INVALID_INPUT", `invalid row binding ID ${id}`);
|
|
38
|
+
const binding = bindings[id];
|
|
39
|
+
if (binding.kind === "postgres") continue;
|
|
40
|
+
if (binding.kind !== "object") {
|
|
41
|
+
throw new ReportingRowStoreError("INVALID_INPUT", `row binding ${id} has an unsupported kind`);
|
|
42
|
+
}
|
|
43
|
+
const provider = this.providers[binding.provider];
|
|
44
|
+
if (!provider) throw new ReportingRowStoreError("INVALID_INPUT", `row binding ${id} names an unknown provider`);
|
|
45
|
+
provider.validateLocation(binding.location);
|
|
46
|
+
assertReportingRowPrefixV1(binding.prefix);
|
|
47
|
+
assertReportingRowKeyTemplateV1(binding.keyTemplate ?? REPORTING_ROW_DEFAULT_KEY_TEMPLATE);
|
|
48
|
+
if (binding.credentialRef !== void 0 && !CREDENTIAL_REF.test(binding.credentialRef)) {
|
|
49
|
+
throw new ReportingRowStoreError("INVALID_INPUT", `row binding ${id} has an invalid credentialRef`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.bindings = Object.freeze({ ...bindings });
|
|
53
|
+
this.deploymentNamespace = options.deploymentNamespace ?? "default";
|
|
54
|
+
if (!this.deploymentNamespace || this.deploymentNamespace.length > 256) {
|
|
55
|
+
throw new ReportingRowStoreError("INVALID_INPUT", "deploymentNamespace must be 1-256 characters");
|
|
56
|
+
}
|
|
57
|
+
this.writeDeadline = positiveMilliseconds(options.writeDeadlineMilliseconds ?? 6e4, "writeDeadlineMilliseconds");
|
|
58
|
+
this.readDeadline = positiveMilliseconds(options.readDeadlineMilliseconds ?? 3e4, "readDeadlineMilliseconds");
|
|
59
|
+
}
|
|
60
|
+
pool;
|
|
61
|
+
options;
|
|
62
|
+
bindings;
|
|
63
|
+
providers;
|
|
64
|
+
deploymentNamespace;
|
|
65
|
+
writeDeadline;
|
|
66
|
+
readDeadline;
|
|
67
|
+
readied;
|
|
68
|
+
installation;
|
|
69
|
+
/** Deadline budget a writer must still hold on its lease before uploading. */
|
|
70
|
+
get writeDeadlineMilliseconds() {
|
|
71
|
+
return this.writeDeadline;
|
|
72
|
+
}
|
|
73
|
+
/** Probe the schema and every provider, and register (or verify) every configured binding. Idempotent. */
|
|
74
|
+
ready() {
|
|
75
|
+
this.readied ??= this.register().catch((error) => {
|
|
76
|
+
this.readied = void 0;
|
|
77
|
+
throw error;
|
|
78
|
+
});
|
|
79
|
+
return this.readied;
|
|
80
|
+
}
|
|
81
|
+
/** Encode rows, prove the chunks reproduce the committed binding, select a binding and plan objects. */
|
|
82
|
+
async prepare(input) {
|
|
83
|
+
await this.ready();
|
|
84
|
+
let encoded;
|
|
85
|
+
try {
|
|
86
|
+
encoded = encodeReportingRowsV1(input.rows);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (isReportingRowEncodingError(error)) throw new ReportingRowStoreError("INVALID_INPUT", error.message);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
const digestProfile = input.rowSetKind === "revision" ? "revision_envelope_v1" : "rows_v1";
|
|
92
|
+
const hasher = digestProfile === "revision_envelope_v1" ? createReportingRevisionEnvelopeHasherV1({
|
|
93
|
+
reporting_revision_id: input.rowSetId,
|
|
94
|
+
row_count: input.rows.length,
|
|
95
|
+
control_totals: input.controlTotals ?? []
|
|
96
|
+
}) : createReportingRowsOnlyHasherV1(input.rows.length);
|
|
97
|
+
for (const chunk of encoded.chunks) hasher.update(chunk.bytes);
|
|
98
|
+
const digest = hasher.digest();
|
|
99
|
+
if (digest.sha256 !== input.binding.sha256 || digest.byteCount !== input.binding.byteCount || input.binding.rowCount !== input.rows.length) {
|
|
100
|
+
throw new ReportingRowStoreError("INVALID_INPUT", "rows do not reproduce their committed binding");
|
|
101
|
+
}
|
|
102
|
+
const ids = Object.keys(this.bindings);
|
|
103
|
+
const bindingId = input.bindingId ? input.bindingId : this.options.selectRowBinding ? await this.options.selectRowBinding({
|
|
104
|
+
account_id: input.accountId,
|
|
105
|
+
reporting_obligation_id: input.obligationId,
|
|
106
|
+
row_set_kind: input.rowSetKind,
|
|
107
|
+
...input.finality ? { finality: input.finality } : {},
|
|
108
|
+
row_count: input.rows.length,
|
|
109
|
+
canonical_byte_count: digest.byteCount
|
|
110
|
+
}) : ids.length === 1 ? ids[0] : DEFAULT_BINDING_ID;
|
|
111
|
+
const binding = this.bindings[bindingId];
|
|
112
|
+
if (!binding) throw new ReportingRowStoreError("STATE_UNAVAILABLE", `row binding ${bindingId} is not configured`);
|
|
113
|
+
if (binding.kind === "postgres") return Object.freeze({ input, encoded, bindingId, binding, digestProfile });
|
|
114
|
+
const compression = binding.compression ?? "gzip";
|
|
115
|
+
const namespaceKey = this.installation.namespaceKey;
|
|
116
|
+
const objects = encoded.chunks.map((chunk) => {
|
|
117
|
+
const physicalBytes = compression === "gzip" ? gzipSync(chunk.bytes, { level: 6 }) : chunk.bytes;
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
key: renderReportingRowObjectKeyV1({
|
|
120
|
+
template: binding.keyTemplate ?? REPORTING_ROW_DEFAULT_KEY_TEMPLATE,
|
|
121
|
+
prefix: binding.prefix,
|
|
122
|
+
namespaceKey,
|
|
123
|
+
accountId: input.accountId,
|
|
124
|
+
rowSetId: input.rowSetId,
|
|
125
|
+
...input.periodStart ? { periodStart: input.periodStart } : {},
|
|
126
|
+
...input.finality ? { finality: input.finality } : {},
|
|
127
|
+
contentSha256: input.binding.sha256,
|
|
128
|
+
chunkIndex: chunk.manifest.chunk_index,
|
|
129
|
+
compression
|
|
130
|
+
}),
|
|
131
|
+
physicalBytes,
|
|
132
|
+
physicalSha256: sha256Hex(physicalBytes),
|
|
133
|
+
compression
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
return Object.freeze({ input, encoded, bindingId, binding, digestProfile, objects: Object.freeze(objects) });
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Upload a prepared object-kind row set, create-only. An object already at a
|
|
140
|
+
* planned key is adopted only when its bytes decode to the exact chunk;
|
|
141
|
+
* otherwise `CONTENT_CONFLICT`. `onCreated` is awaited for every object this
|
|
142
|
+
* call created, before the next upload, so the caller can record it in its
|
|
143
|
+
* write intent.
|
|
144
|
+
*/
|
|
145
|
+
async upload(prepared, intentId, onCreated, signal) {
|
|
146
|
+
const binding = prepared.binding;
|
|
147
|
+
if (binding.kind !== "object" || !prepared.objects) {
|
|
148
|
+
throw new ReportingRowStoreError("INVALID_INPUT", "only object-kind row sets are uploaded");
|
|
149
|
+
}
|
|
150
|
+
const provider = this.providers[binding.provider];
|
|
151
|
+
const context = { signal: deadlineSignal(this.writeDeadline, signal) };
|
|
152
|
+
const location = providerLocation(binding);
|
|
153
|
+
const locators = [];
|
|
154
|
+
for (const [index, object] of prepared.objects.entries()) {
|
|
155
|
+
const manifest = prepared.encoded.manifests[index];
|
|
156
|
+
const put = await this.provider(
|
|
157
|
+
() => provider.putIfAbsent(
|
|
158
|
+
{
|
|
159
|
+
...location,
|
|
160
|
+
key: object.key,
|
|
161
|
+
bytes: object.physicalBytes,
|
|
162
|
+
contentType: object.compression === "gzip" ? "application/gzip" : "application/x-ndjson",
|
|
163
|
+
metadata: {
|
|
164
|
+
"adcp-installation": this.installation.installationId,
|
|
165
|
+
"adcp-intent": intentId
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
context
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
if (put.created) {
|
|
172
|
+
await onCreated({ objectKey: object.key, nativeVersion: put.nativeVersion });
|
|
173
|
+
locators.push({
|
|
174
|
+
objectKey: object.key,
|
|
175
|
+
nativeVersion: put.nativeVersion,
|
|
176
|
+
compression: object.compression,
|
|
177
|
+
physicalSha256: object.physicalSha256,
|
|
178
|
+
physicalByteCount: object.physicalBytes.byteLength,
|
|
179
|
+
created: true
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const existing = await this.provider(
|
|
184
|
+
() => provider.get(
|
|
185
|
+
{ ...location, key: object.key, nativeVersion: put.nativeVersion, maxBytes: MAX_ADOPTED_OBJECT_BYTES },
|
|
186
|
+
context
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
const logical = existing ? this.inflate(Buffer.from(existing), object.compression, manifest.byte_count) : null;
|
|
190
|
+
if (!existing || !logical || sha256Hex(logical) !== manifest.sha256) {
|
|
191
|
+
throw new ReportingRowStoreError("CONTENT_CONFLICT", `object for chunk ${index} holds different bytes`);
|
|
192
|
+
}
|
|
193
|
+
locators.push({
|
|
194
|
+
objectKey: object.key,
|
|
195
|
+
nativeVersion: put.nativeVersion,
|
|
196
|
+
compression: object.compression,
|
|
197
|
+
physicalSha256: sha256Hex(existing),
|
|
198
|
+
physicalByteCount: existing.byteLength,
|
|
199
|
+
created: false
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return locators;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Persist a prepared row set inside the caller's ledger transaction:
|
|
206
|
+
* bodies for the `postgres` kind, or the uploaded locators for the object
|
|
207
|
+
* kind. Replays of an identical row set are no-ops; a conflicting row set
|
|
208
|
+
* under the same ID is refused.
|
|
209
|
+
*/
|
|
210
|
+
async writeInTransaction(transaction, prepared, locators, sharedFromRowSetId) {
|
|
211
|
+
const { input, encoded } = prepared;
|
|
212
|
+
if (prepared.binding.kind === "object" && locators?.length !== encoded.chunks.length) {
|
|
213
|
+
throw new ReportingRowStoreError("INVALID_INPUT", "object-kind row sets need one locator per chunk");
|
|
214
|
+
}
|
|
215
|
+
const inserted = await transaction.query(
|
|
216
|
+
`INSERT INTO adcp_reporting_row_sets
|
|
217
|
+
(row_set_id, row_set_kind, account_id, obligation_id, encoding, digest_profile, content_sha256,
|
|
218
|
+
canonical_byte_count, row_count, row_manifest_sha256, chunk_count, row_binding_id,
|
|
219
|
+
rows_shared_from_row_set_id)
|
|
220
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
221
|
+
ON CONFLICT (row_set_id) DO NOTHING`,
|
|
222
|
+
[
|
|
223
|
+
input.rowSetId,
|
|
224
|
+
input.rowSetKind,
|
|
225
|
+
input.accountId,
|
|
226
|
+
input.obligationId,
|
|
227
|
+
REPORTING_ROW_ENCODING_V1,
|
|
228
|
+
prepared.digestProfile,
|
|
229
|
+
input.binding.sha256,
|
|
230
|
+
input.binding.byteCount,
|
|
231
|
+
encoded.rowCount,
|
|
232
|
+
encoded.rowManifestSha256,
|
|
233
|
+
encoded.chunks.length,
|
|
234
|
+
prepared.bindingId,
|
|
235
|
+
sharedFromRowSetId ?? null
|
|
236
|
+
]
|
|
237
|
+
);
|
|
238
|
+
if (inserted.rowCount !== 1) {
|
|
239
|
+
const existing = await transaction.query(
|
|
240
|
+
"SELECT content_sha256, row_manifest_sha256 FROM adcp_reporting_row_sets WHERE row_set_id = $1",
|
|
241
|
+
[input.rowSetId]
|
|
242
|
+
);
|
|
243
|
+
const row = existing.rows[0];
|
|
244
|
+
if (!row || row.content_sha256 !== input.binding.sha256 || row.row_manifest_sha256 !== encoded.rowManifestSha256) {
|
|
245
|
+
throw new ReportingRowStoreError("CONTENT_CONFLICT", "row set identity names different content");
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
for (const [index, chunk] of encoded.chunks.entries()) {
|
|
250
|
+
const manifest = chunk.manifest;
|
|
251
|
+
const locator = locators?.[index];
|
|
252
|
+
if (!locator) {
|
|
253
|
+
await transaction.query(
|
|
254
|
+
`INSERT INTO adcp_reporting_chunk_bodies (account_id, obligation_id, sha256, body)
|
|
255
|
+
VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`,
|
|
256
|
+
[input.accountId, input.obligationId, manifest.sha256, chunk.bytes]
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
await transaction.query(
|
|
260
|
+
`INSERT INTO adcp_reporting_row_chunks
|
|
261
|
+
(row_set_id, chunk_index, account_id, obligation_id, first_ordinal, row_count, byte_count, sha256, segments,
|
|
262
|
+
object_key, native_version, compression, physical_sha256, physical_byte_count)
|
|
263
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12, $13, $14)`,
|
|
264
|
+
[
|
|
265
|
+
input.rowSetId,
|
|
266
|
+
manifest.chunk_index,
|
|
267
|
+
input.accountId,
|
|
268
|
+
input.obligationId,
|
|
269
|
+
manifest.first_ordinal,
|
|
270
|
+
manifest.row_count,
|
|
271
|
+
manifest.byte_count,
|
|
272
|
+
manifest.sha256,
|
|
273
|
+
JSON.stringify(manifest.segments),
|
|
274
|
+
locator?.objectKey ?? null,
|
|
275
|
+
locator?.nativeVersion ?? null,
|
|
276
|
+
locator?.compression ?? "none",
|
|
277
|
+
locator?.physicalSha256 ?? null,
|
|
278
|
+
locator?.physicalByteCount ?? null
|
|
279
|
+
]
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Load a row set and verify its chunk manifest against the committed digest.
|
|
285
|
+
* Returns `null` when no row set exists (legacy inline rows) or it belongs
|
|
286
|
+
* to another account. Throws `ROWS_EXPIRED` / `ROWS_UNAVAILABLE` when the
|
|
287
|
+
* rows were pruned or lost.
|
|
288
|
+
*/
|
|
289
|
+
async find(rowSetId, accountId, queryable = this.pool) {
|
|
290
|
+
const found = await queryable.query(
|
|
291
|
+
`SELECT row_set.row_set_id, row_set.row_set_kind, row_set.account_id, row_set.obligation_id,
|
|
292
|
+
row_set.digest_profile, row_set.content_sha256, row_set.canonical_byte_count, row_set.row_count,
|
|
293
|
+
row_set.row_manifest_sha256, row_set.chunk_count, row_set.row_binding_id,
|
|
294
|
+
row_set.row_location_version, row_set.rows_state, binding.kind, binding.identity_sha256
|
|
295
|
+
FROM adcp_reporting_row_sets row_set
|
|
296
|
+
JOIN adcp_reporting_row_bindings binding ON binding.row_binding_id = row_set.row_binding_id
|
|
297
|
+
WHERE row_set.row_set_id = $1 AND row_set.account_id = $2`,
|
|
298
|
+
[rowSetId, accountId]
|
|
299
|
+
);
|
|
300
|
+
const record = found.rows[0];
|
|
301
|
+
if (!record) return null;
|
|
302
|
+
if (record.rows_state === "pruning" || record.rows_state === "pruned") {
|
|
303
|
+
throw new ReportingRowStoreError("ROWS_EXPIRED");
|
|
304
|
+
}
|
|
305
|
+
if (record.rows_state === "unavailable") throw new ReportingRowStoreError("ROWS_UNAVAILABLE");
|
|
306
|
+
const chunks = await queryable.query(
|
|
307
|
+
`SELECT chunk_index, first_ordinal, row_count, byte_count, sha256, segments,
|
|
308
|
+
object_key, native_version, compression, physical_sha256, physical_byte_count
|
|
309
|
+
FROM adcp_reporting_row_chunks WHERE row_set_id = $1 ORDER BY chunk_index`,
|
|
310
|
+
[rowSetId]
|
|
311
|
+
);
|
|
312
|
+
const manifests = chunks.rows.map((chunk) => ({
|
|
313
|
+
chunk_index: chunk.chunk_index,
|
|
314
|
+
first_ordinal: Number(chunk.first_ordinal),
|
|
315
|
+
row_count: chunk.row_count,
|
|
316
|
+
byte_count: Number(chunk.byte_count),
|
|
317
|
+
sha256: chunk.sha256,
|
|
318
|
+
segments: chunk.segments.map((segment) => ({
|
|
319
|
+
first_ordinal: Number(segment.first_ordinal),
|
|
320
|
+
row_count: Number(segment.row_count),
|
|
321
|
+
byte_offset: Number(segment.byte_offset),
|
|
322
|
+
byte_count: Number(segment.byte_count),
|
|
323
|
+
sha256: segment.sha256
|
|
324
|
+
}))
|
|
325
|
+
}));
|
|
326
|
+
const rowCount = Number(record.row_count);
|
|
327
|
+
try {
|
|
328
|
+
verifyReportingRowManifestsV1(manifests, { rowCount, rowManifestSha256: record.row_manifest_sha256 });
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (isReportingRowEncodingError(error)) throw new ReportingRowStoreError("ROWS_INTEGRITY_FAILED", error.message);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
if (manifests.length !== record.chunk_count) {
|
|
334
|
+
throw new ReportingRowStoreError("ROWS_INTEGRITY_FAILED", "chunk count does not match the row set");
|
|
335
|
+
}
|
|
336
|
+
const locators = chunks.rows.map((chunk) => {
|
|
337
|
+
const hasLocation = chunk.object_key !== null;
|
|
338
|
+
if (hasLocation !== (record.kind === "object")) {
|
|
339
|
+
throw new ReportingRowStoreError("ROWS_INTEGRITY_FAILED", "chunk location does not match its binding kind");
|
|
340
|
+
}
|
|
341
|
+
return hasLocation ? Object.freeze({
|
|
342
|
+
objectKey: chunk.object_key,
|
|
343
|
+
nativeVersion: chunk.native_version,
|
|
344
|
+
compression: chunk.compression,
|
|
345
|
+
physicalSha256: chunk.physical_sha256,
|
|
346
|
+
physicalByteCount: Number(chunk.physical_byte_count)
|
|
347
|
+
}) : void 0;
|
|
348
|
+
});
|
|
349
|
+
return Object.freeze({
|
|
350
|
+
rowSetId: record.row_set_id,
|
|
351
|
+
rowSetKind: record.row_set_kind,
|
|
352
|
+
accountId: record.account_id,
|
|
353
|
+
obligationId: record.obligation_id,
|
|
354
|
+
digestProfile: record.digest_profile,
|
|
355
|
+
contentSha256: record.content_sha256,
|
|
356
|
+
canonicalByteCount: Number(record.canonical_byte_count),
|
|
357
|
+
rowCount,
|
|
358
|
+
rowManifestSha256: record.row_manifest_sha256,
|
|
359
|
+
bindingId: record.row_binding_id,
|
|
360
|
+
bindingIdentitySha256: record.identity_sha256,
|
|
361
|
+
kind: record.kind,
|
|
362
|
+
manifests: Object.freeze(manifests),
|
|
363
|
+
locators: Object.freeze(locators)
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/** Verified rows `[offset, offset + limit)` in ordinal order. */
|
|
367
|
+
async readPage(handle, offset, limit, signal) {
|
|
368
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1) {
|
|
369
|
+
throw new ReportingRowStoreError("INVALID_INPUT", "row page bounds are invalid");
|
|
370
|
+
}
|
|
371
|
+
const end = Math.min(handle.rowCount, offset + limit);
|
|
372
|
+
const rows = [];
|
|
373
|
+
for (const manifest of handle.manifests) {
|
|
374
|
+
if (offset >= end) break;
|
|
375
|
+
const chunkEnd = manifest.first_ordinal + manifest.row_count;
|
|
376
|
+
if (chunkEnd <= offset || manifest.first_ordinal >= end) continue;
|
|
377
|
+
signal?.throwIfAborted();
|
|
378
|
+
const from = manifest.segments.findIndex(
|
|
379
|
+
(segment) => segment.first_ordinal + segment.row_count > Math.max(offset, manifest.first_ordinal)
|
|
380
|
+
);
|
|
381
|
+
let to = from;
|
|
382
|
+
while (to + 1 < manifest.segments.length && manifest.segments[to + 1].first_ordinal < end) to += 1;
|
|
383
|
+
const firstSegment = manifest.segments[from];
|
|
384
|
+
const lastSegment = manifest.segments[to];
|
|
385
|
+
const bytes = await this.readChunkRange(
|
|
386
|
+
handle,
|
|
387
|
+
manifest,
|
|
388
|
+
firstSegment.byte_offset,
|
|
389
|
+
lastSegment.byte_offset + lastSegment.byte_count - firstSegment.byte_offset,
|
|
390
|
+
signal
|
|
391
|
+
);
|
|
392
|
+
const decoded = this.decode(() => decodeVerifiedReportingRowSegmentsV1(manifest, [from, to], bytes));
|
|
393
|
+
const startIndex = Math.max(offset, firstSegment.first_ordinal) - firstSegment.first_ordinal;
|
|
394
|
+
const endIndex = Math.min(end, lastSegment.first_ordinal + lastSegment.row_count) - firstSegment.first_ordinal;
|
|
395
|
+
rows.push(...decoded.slice(startIndex, endIndex));
|
|
396
|
+
}
|
|
397
|
+
return rows;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Every row, verified chunk by chunk and against the committed binding.
|
|
401
|
+
* `controlTotals` are required for revision row sets.
|
|
402
|
+
*/
|
|
403
|
+
async readAll(handle, controlTotals, options = {}) {
|
|
404
|
+
if (options.maxBytes !== void 0 && handle.canonicalByteCount > options.maxBytes) {
|
|
405
|
+
throw new ReportingRowStoreError("INVALID_INPUT", "row set exceeds the hydration byte limit");
|
|
406
|
+
}
|
|
407
|
+
const hasher = handle.digestProfile === "revision_envelope_v1" ? createReportingRevisionEnvelopeHasherV1({
|
|
408
|
+
reporting_revision_id: handle.rowSetId,
|
|
409
|
+
row_count: handle.rowCount,
|
|
410
|
+
control_totals: controlTotals ?? []
|
|
411
|
+
}) : createReportingRowsOnlyHasherV1(handle.rowCount);
|
|
412
|
+
const rows = [];
|
|
413
|
+
for (const manifest of handle.manifests) {
|
|
414
|
+
options.signal?.throwIfAborted();
|
|
415
|
+
const bytes = await this.readChunkRange(handle, manifest, 0, manifest.byte_count, options.signal);
|
|
416
|
+
this.decode(() => verifyReportingRowChunkV1(manifest, bytes));
|
|
417
|
+
hasher.update(bytes);
|
|
418
|
+
rows.push(
|
|
419
|
+
...this.decode(() => decodeVerifiedReportingRowSegmentsV1(manifest, [0, manifest.segments.length - 1], bytes))
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const digest = this.decode(() => hasher.digest());
|
|
423
|
+
if (digest.sha256 !== handle.contentSha256 || digest.byteCount !== handle.canonicalByteCount) {
|
|
424
|
+
throw new ReportingRowStoreError("ROWS_INTEGRITY_FAILED", "rows do not reproduce the committed binding");
|
|
425
|
+
}
|
|
426
|
+
return rows;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Delete exact object versions through the configured binding's provider.
|
|
430
|
+
* `absent` counts as success. Only object-kind bindings are accepted.
|
|
431
|
+
*/
|
|
432
|
+
async deleteObjects(bindingId, objects, signal) {
|
|
433
|
+
await this.ready();
|
|
434
|
+
const binding = this.bindings[bindingId];
|
|
435
|
+
if (!binding || binding.kind !== "object") {
|
|
436
|
+
throw new ReportingRowStoreError("STATE_UNAVAILABLE", `row binding ${bindingId} is not configured for objects`);
|
|
437
|
+
}
|
|
438
|
+
const provider = this.providers[binding.provider];
|
|
439
|
+
const ownedPrefix = `${binding.prefix}/${this.installation.namespaceKey}/`;
|
|
440
|
+
const context = { signal: deadlineSignal(this.writeDeadline, signal) };
|
|
441
|
+
for (const object of objects) {
|
|
442
|
+
if (!object.objectKey.startsWith(ownedPrefix)) {
|
|
443
|
+
throw new ReportingRowStoreError("UNSAFE_BINDING", "refusing to delete an object outside the owned prefix");
|
|
444
|
+
}
|
|
445
|
+
await this.provider(
|
|
446
|
+
() => provider.delete(
|
|
447
|
+
{ ...providerLocation(binding), key: object.objectKey, nativeVersion: object.nativeVersion },
|
|
448
|
+
context
|
|
449
|
+
)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* A live row set in the same obligation and binding whose rows are
|
|
455
|
+
* byte-identical (same manifest digest), with its chunk locations. Sharing
|
|
456
|
+
* never crosses obligations, so it never crosses consumers.
|
|
457
|
+
*/
|
|
458
|
+
async findShareable(queryable, prepared) {
|
|
459
|
+
if (prepared.encoded.chunks.length === 0) return void 0;
|
|
460
|
+
const found = await queryable.query(
|
|
461
|
+
`SELECT row_set_id FROM adcp_reporting_row_sets
|
|
462
|
+
WHERE obligation_id = $1 AND account_id = $2 AND row_binding_id = $3 AND row_manifest_sha256 = $4
|
|
463
|
+
AND rows_state = 'live' AND row_set_id <> $5
|
|
464
|
+
ORDER BY recorded_at DESC, row_set_id LIMIT 1
|
|
465
|
+
FOR SHARE`,
|
|
466
|
+
[
|
|
467
|
+
prepared.input.obligationId,
|
|
468
|
+
prepared.input.accountId,
|
|
469
|
+
prepared.bindingId,
|
|
470
|
+
prepared.encoded.rowManifestSha256,
|
|
471
|
+
prepared.input.rowSetId
|
|
472
|
+
]
|
|
473
|
+
);
|
|
474
|
+
const rowSetId = found.rows[0]?.row_set_id;
|
|
475
|
+
if (!rowSetId) return void 0;
|
|
476
|
+
const chunks = await queryable.query(
|
|
477
|
+
`SELECT object_key, native_version, compression, physical_sha256, physical_byte_count
|
|
478
|
+
FROM adcp_reporting_row_chunks WHERE row_set_id = $1 ORDER BY chunk_index`,
|
|
479
|
+
[rowSetId]
|
|
480
|
+
);
|
|
481
|
+
if (chunks.rows.length !== prepared.encoded.chunks.length) return void 0;
|
|
482
|
+
if (prepared.binding.kind === "postgres") return { rowSetId, locators: [] };
|
|
483
|
+
if (chunks.rows.some((chunk) => chunk.object_key === null)) return void 0;
|
|
484
|
+
return {
|
|
485
|
+
rowSetId,
|
|
486
|
+
locators: chunks.rows.map((chunk) => ({
|
|
487
|
+
objectKey: chunk.object_key,
|
|
488
|
+
nativeVersion: chunk.native_version,
|
|
489
|
+
compression: chunk.compression,
|
|
490
|
+
physicalSha256: chunk.physical_sha256,
|
|
491
|
+
physicalByteCount: Number(chunk.physical_byte_count),
|
|
492
|
+
created: false
|
|
493
|
+
}))
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/** Installation identity and namespace key; available after `ready()`. */
|
|
497
|
+
get installationIdentity() {
|
|
498
|
+
if (!this.installation) throw new ReportingRowStoreError("STATE_UNAVAILABLE", "row storage is not ready");
|
|
499
|
+
return this.installation;
|
|
500
|
+
}
|
|
501
|
+
async readChunkRange(handle, manifest, byteOffset, byteCount, signal) {
|
|
502
|
+
if (handle.kind === "postgres") {
|
|
503
|
+
const result = await this.pool.query(
|
|
504
|
+
`SELECT substring(body FROM $4::integer + 1 FOR $5::integer) AS bytes
|
|
505
|
+
FROM adcp_reporting_chunk_bodies
|
|
506
|
+
WHERE account_id = $1 AND obligation_id = $2 AND sha256 = $3`,
|
|
507
|
+
[handle.accountId, handle.obligationId, manifest.sha256, byteOffset, byteCount]
|
|
508
|
+
);
|
|
509
|
+
const bytes = result.rows[0]?.bytes;
|
|
510
|
+
if (!bytes) throw new ReportingRowStoreError("ROWS_UNAVAILABLE", `chunk ${manifest.chunk_index} body is missing`);
|
|
511
|
+
return bytes;
|
|
512
|
+
}
|
|
513
|
+
const binding = this.bindings[handle.bindingId];
|
|
514
|
+
if (!binding || binding.kind !== "object" || bindingIdentitySha256(binding) !== handle.bindingIdentitySha256) {
|
|
515
|
+
throw new ReportingRowStoreError("STATE_UNAVAILABLE", `row binding ${handle.bindingId} is not configured`);
|
|
516
|
+
}
|
|
517
|
+
const locator = handle.locators[manifest.chunk_index];
|
|
518
|
+
const provider = this.providers[binding.provider];
|
|
519
|
+
const context = { signal: deadlineSignal(this.readDeadline, signal) };
|
|
520
|
+
const location = providerLocation(binding);
|
|
521
|
+
if (locator.compression === "none" && provider.rangedReads) {
|
|
522
|
+
const bytes = await this.provider(
|
|
523
|
+
() => provider.get(
|
|
524
|
+
{
|
|
525
|
+
...location,
|
|
526
|
+
key: locator.objectKey,
|
|
527
|
+
nativeVersion: locator.nativeVersion,
|
|
528
|
+
range: { offset: byteOffset, length: byteCount },
|
|
529
|
+
maxBytes: byteCount
|
|
530
|
+
},
|
|
531
|
+
context
|
|
532
|
+
)
|
|
533
|
+
);
|
|
534
|
+
if (!bytes)
|
|
535
|
+
throw new ReportingRowStoreError("ROWS_UNAVAILABLE", `chunk ${manifest.chunk_index} object is missing`);
|
|
536
|
+
if (bytes.byteLength !== byteCount) {
|
|
537
|
+
throw new ReportingRowStoreError("ROWS_INTEGRITY_FAILED", `chunk ${manifest.chunk_index} range is truncated`);
|
|
538
|
+
}
|
|
539
|
+
return Buffer.from(bytes);
|
|
540
|
+
}
|
|
541
|
+
const stored = await this.provider(
|
|
542
|
+
() => provider.get(
|
|
543
|
+
{
|
|
544
|
+
...location,
|
|
545
|
+
key: locator.objectKey,
|
|
546
|
+
nativeVersion: locator.nativeVersion,
|
|
547
|
+
maxBytes: locator.physicalByteCount
|
|
548
|
+
},
|
|
549
|
+
context
|
|
550
|
+
)
|
|
551
|
+
);
|
|
552
|
+
if (!stored)
|
|
553
|
+
throw new ReportingRowStoreError("ROWS_UNAVAILABLE", `chunk ${manifest.chunk_index} object is missing`);
|
|
554
|
+
const physical = Buffer.from(stored);
|
|
555
|
+
if (physical.byteLength !== locator.physicalByteCount || sha256Hex(physical) !== locator.physicalSha256) {
|
|
556
|
+
throw new ReportingRowStoreError("ROWS_INTEGRITY_FAILED", `chunk ${manifest.chunk_index} stored bytes differ`);
|
|
557
|
+
}
|
|
558
|
+
const logical = this.inflate(physical, locator.compression, manifest.byte_count);
|
|
559
|
+
if (!logical) {
|
|
560
|
+
throw new ReportingRowStoreError("ROWS_INTEGRITY_FAILED", `chunk ${manifest.chunk_index} does not decompress`);
|
|
561
|
+
}
|
|
562
|
+
return logical.subarray(byteOffset, byteOffset + byteCount);
|
|
563
|
+
}
|
|
564
|
+
inflate(physical, compression, byteCount) {
|
|
565
|
+
if (compression === "none") return physical.byteLength === byteCount ? physical : null;
|
|
566
|
+
try {
|
|
567
|
+
const logical = gunzipSync(physical, { maxOutputLength: byteCount });
|
|
568
|
+
return logical.byteLength === byteCount ? logical : null;
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
decode(operation) {
|
|
574
|
+
try {
|
|
575
|
+
return operation();
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (isReportingRowEncodingError(error)) throw new ReportingRowStoreError("ROWS_INTEGRITY_FAILED", error.message);
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/** Collapse provider failures to stable, secret-free codes. */
|
|
582
|
+
async provider(operation) {
|
|
583
|
+
try {
|
|
584
|
+
return await operation();
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (isReportingRowStoreError(error)) throw error;
|
|
587
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
588
|
+
throw new ReportingRowStoreError(error.name === "TimeoutError" ? "DEADLINE_EXCEEDED" : "ABORTED");
|
|
589
|
+
}
|
|
590
|
+
throw new ReportingRowStoreError("PROVIDER_UNAVAILABLE");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async register() {
|
|
594
|
+
const { installationId } = await probeReportingRowStorageSchemaV1(this.pool);
|
|
595
|
+
const namespaceKey = sha256Hex(canonicalize(["adcp.rows.v1", this.deploymentNamespace, installationId]));
|
|
596
|
+
this.installation = { installationId, namespaceKey };
|
|
597
|
+
for (const [bindingId, binding] of Object.entries(this.bindings)) {
|
|
598
|
+
if (binding.kind === "object") {
|
|
599
|
+
const provider = this.providers[binding.provider];
|
|
600
|
+
await this.provider(
|
|
601
|
+
() => provider.probe(
|
|
602
|
+
{ ...providerLocation(binding), prefix: binding.prefix },
|
|
603
|
+
{ signal: AbortSignal.timeout(this.readDeadline) }
|
|
604
|
+
)
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const identityConfig = bindingIdentityConfig(binding);
|
|
608
|
+
const identitySha256 = bindingIdentitySha256(binding);
|
|
609
|
+
const operationalConfig = binding.kind === "object" ? {
|
|
610
|
+
compression: binding.compression ?? "gzip",
|
|
611
|
+
...binding.credentialRef ? { credentialRef: binding.credentialRef } : {}
|
|
612
|
+
} : {};
|
|
613
|
+
await this.pool.query(
|
|
614
|
+
`INSERT INTO adcp_reporting_row_bindings
|
|
615
|
+
(row_binding_id, kind, provider, identity_config, identity_sha256, operational_config, namespace_key)
|
|
616
|
+
VALUES ($1, $2, $3, $4::jsonb, $5, $6::jsonb, $7)
|
|
617
|
+
ON CONFLICT (row_binding_id) DO NOTHING`,
|
|
618
|
+
[
|
|
619
|
+
bindingId,
|
|
620
|
+
binding.kind,
|
|
621
|
+
binding.kind === "object" ? binding.provider : "postgres",
|
|
622
|
+
JSON.stringify(identityConfig),
|
|
623
|
+
identitySha256,
|
|
624
|
+
JSON.stringify(operationalConfig),
|
|
625
|
+
namespaceKey
|
|
626
|
+
]
|
|
627
|
+
);
|
|
628
|
+
const stored = await this.pool.query(
|
|
629
|
+
"SELECT identity_sha256, namespace_key, state FROM adcp_reporting_row_bindings WHERE row_binding_id = $1",
|
|
630
|
+
[bindingId]
|
|
631
|
+
);
|
|
632
|
+
const row = stored.rows[0];
|
|
633
|
+
if (!row || row.identity_sha256 !== identitySha256) {
|
|
634
|
+
throw new ReportingRowStoreError(
|
|
635
|
+
"UNSAFE_BINDING",
|
|
636
|
+
`row binding ${bindingId} identity differs from its definition`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (row.namespace_key !== namespaceKey) {
|
|
640
|
+
throw new ReportingRowStoreError("UNSAFE_BINDING", `row binding ${bindingId} belongs to another installation`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function bindingIdentityConfig(binding) {
|
|
646
|
+
return binding.kind === "object" ? {
|
|
647
|
+
location: { ...binding.location },
|
|
648
|
+
prefix: binding.prefix,
|
|
649
|
+
keyTemplate: binding.keyTemplate ?? REPORTING_ROW_DEFAULT_KEY_TEMPLATE
|
|
650
|
+
} : {};
|
|
651
|
+
}
|
|
652
|
+
function bindingIdentitySha256(binding) {
|
|
653
|
+
const provider = binding.kind === "object" ? binding.provider : binding.kind;
|
|
654
|
+
return sha256Hex(canonicalize([binding.kind, provider, bindingIdentityConfig(binding)]));
|
|
655
|
+
}
|
|
656
|
+
function providerLocation(binding) {
|
|
657
|
+
return { location: binding.location, ...binding.credentialRef ? { credentialRef: binding.credentialRef } : {} };
|
|
658
|
+
}
|
|
659
|
+
function deadlineSignal(milliseconds, signal) {
|
|
660
|
+
const timeout = AbortSignal.timeout(milliseconds);
|
|
661
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
662
|
+
}
|
|
663
|
+
function positiveMilliseconds(value, name) {
|
|
664
|
+
if (!Number.isSafeInteger(value) || value < 1e3 || value > 6e5) {
|
|
665
|
+
throw new ReportingRowStoreError("INVALID_INPUT", `${name} must be between 1000 and 600000`);
|
|
666
|
+
}
|
|
667
|
+
return value;
|
|
668
|
+
}
|
|
669
|
+
function sha256Hex(value) {
|
|
670
|
+
return createHash("sha256").update(value).digest("hex");
|
|
671
|
+
}
|
|
672
|
+
function reportingRowWriteIntentIdV1() {
|
|
673
|
+
return randomUUID();
|
|
674
|
+
}
|
|
675
|
+
export {
|
|
676
|
+
ReportingRowStorageV1,
|
|
677
|
+
ReportingRowStoreError2 as ReportingRowStoreError,
|
|
678
|
+
isReportingRowStoreError2 as isReportingRowStoreError,
|
|
679
|
+
reportingRowWriteIntentIdV1
|
|
680
|
+
};
|
|
@@ -756,6 +756,14 @@ export interface ReportingLedgerStore {
|
|
|
756
756
|
}>;
|
|
757
757
|
getObligation(reporting_obligation_id: string, account_id?: string): Promise<ReportingLedgerObligationV1 | null>;
|
|
758
758
|
listObligations(account_id?: string): Promise<ReportingLedgerObligationV1[]>;
|
|
759
|
+
/**
|
|
760
|
+
* Optional: periods whose ledger records were retired by retention. The
|
|
761
|
+
* planner treats them as already covered so it never re-creates them.
|
|
762
|
+
*/
|
|
763
|
+
listRetiredObligationOrdinals?(account_id?: string): Promise<{
|
|
764
|
+
configurationId: string;
|
|
765
|
+
periodOrdinal: number;
|
|
766
|
+
}[]>;
|
|
759
767
|
listLifecycleDueObligations(input: {
|
|
760
768
|
ledgerAsOf: string;
|
|
761
769
|
account_id?: string;
|