@juspay/neurolink 12.20.0 → 12.20.2
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/CHANGELOG.md +2 -6
- package/dist/browser/neurolink.min.js +400 -400
- package/dist/cli/commands/proxy.js +171 -25
- package/dist/cli/commands/telemetry.d.ts +4 -1
- package/dist/cli/commands/telemetry.js +113 -0
- package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
- package/dist/cli/utils/nativeTelemetry.js +322 -0
- package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
- package/dist/proxy/bodyCapturePolicy.js +132 -0
- package/dist/proxy/codexFallback.d.ts +27 -9
- package/dist/proxy/codexFallback.js +84 -13
- package/dist/proxy/openaiFormat.js +8 -1
- package/dist/proxy/otelLogSink.d.ts +11 -1
- package/dist/proxy/otelLogSink.js +174 -27
- package/dist/proxy/otlpLogTransport.d.ts +4 -2
- package/dist/proxy/otlpLogTransport.js +22 -7
- package/dist/proxy/requestLogger.js +11 -3
- package/dist/proxy/restartControl.js +18 -3
- package/dist/proxy/rollingProxyServer.js +48 -1
- package/dist/proxy/rollingWorkerProcess.js +87 -10
- package/dist/proxy/rollingWorkerProtocol.js +4 -2
- package/dist/proxy/rollingWorkerSupervisor.js +47 -10
- package/dist/proxy/socketWorkerRuntime.js +90 -4
- package/dist/proxy/updateChecker.d.ts +3 -3
- package/dist/proxy/updateChecker.js +16 -9
- package/dist/proxy/updateCoordinator.d.ts +10 -1
- package/dist/proxy/updateCoordinator.js +68 -1
- package/dist/proxy/updateState.d.ts +2 -0
- package/dist/proxy/updateState.js +15 -0
- package/dist/proxy/vertexAnthropicFallback.d.ts +17 -1
- package/dist/proxy/vertexAnthropicFallback.js +25 -1
- package/dist/server/routes/claudeProxyRoutes.d.ts +17 -0
- package/dist/server/routes/claudeProxyRoutes.js +53 -5
- package/dist/types/cli.d.ts +18 -0
- package/dist/types/providers.d.ts +8 -0
- package/dist/types/proxy.d.ts +117 -7
- package/dist/utils/anthropicCacheBreakpoints.d.ts +22 -1
- package/dist/utils/anthropicCacheBreakpoints.js +113 -0
- package/dist/utils/pricing.js +69 -2
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
- package/scripts/observability/NATIVE-DURABLE.md +68 -0
- package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
- package/scripts/observability/proxy-telemetry-check.mjs +135 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.20.
|
|
3
|
+
"version": "12.20.2",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -5,6 +5,74 @@
|
|
|
5
5
|
Application logs remain OTLP-only. Collector retry storage writes a bounded
|
|
6
6
|
queue to disk so records accepted into that queue survive a collector restart.
|
|
7
7
|
|
|
8
|
+
## Prepare an existing native installation
|
|
9
|
+
|
|
10
|
+
The shipped CLI stages a migration from the existing collector. It preserves
|
|
11
|
+
the metadata stream (for example, `curator`), backend endpoint and authorization,
|
|
12
|
+
metadata listener, health listener and metrics port. Its new body stream defaults
|
|
13
|
+
to `<metadata stream>_bodies`. Other operator customizations need review against
|
|
14
|
+
the staged standard profile, particularly filters, TLS, memory limits and
|
|
15
|
+
exporter settings. The original collector YAML and all service settings stay
|
|
16
|
+
unchanged.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
neurolink telemetry native-prepare \
|
|
20
|
+
--collector-config "$HOME/.neurolink/telemetry-native/config/collector.yaml" \
|
|
21
|
+
--output "$HOME/neurolink-native-migration-review" \
|
|
22
|
+
--queue-directory "$HOME/.neurolink/telemetry-native/queue" \
|
|
23
|
+
--compaction-directory "$HOME/.neurolink/telemetry-native/compact" \
|
|
24
|
+
--disk-quota-mib 1024 \
|
|
25
|
+
--metadata-retention-days 14 \
|
|
26
|
+
--body-retention-days 3
|
|
27
|
+
|
|
28
|
+
neurolink telemetry native-validate \
|
|
29
|
+
--directory "$HOME/neurolink-native-migration-review" \
|
|
30
|
+
--collector-bin "$HOME/.neurolink/telemetry-native/bin/otelcol-contrib"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The destination directory must not exist. It is created with mode `0700`, with
|
|
34
|
+
`0600` files: `collector.yaml`, `collector.env.json`, `proxy.env`, `doctor.env`
|
|
35
|
+
and `manifest.json`. The JSON environment contains the private backend
|
|
36
|
+
authorization; keep it private and do not paste it into an issue. Command output
|
|
37
|
+
and the profile do not contain that credential. The doctor snippet includes
|
|
38
|
+
**both stream names** and the actual collector metrics port; supply backend
|
|
39
|
+
authorization to the doctor's environment privately. Snippets are environment
|
|
40
|
+
configuration to merge, not shell scripts to execute. Never replace an existing
|
|
41
|
+
proxy environment wholesale with a snippet.
|
|
42
|
+
|
|
43
|
+
`native-validate` checks private file permissions, staged file hashes, the source
|
|
44
|
+
configuration hash, and stable collector version `>= 0.160.0`, then runs only
|
|
45
|
+
`otelcol-contrib validate`. It uses temporary queue paths inside the stage and
|
|
46
|
+
removes them afterward. It never starts a collector or connects to the backend.
|
|
47
|
+
Binary diagnostics are suppressed because they can contain credentials. A
|
|
48
|
+
successful validation proves profile acceptance, not delivery, backend retention,
|
|
49
|
+
queue recovery or runtime adoption. Re-prepare into a new directory after any
|
|
50
|
+
source or staged-file edits.
|
|
51
|
+
|
|
52
|
+
The manifest records requested payload limits, planned volume quota, separate
|
|
53
|
+
backend retention periods and remaining operator checks. `--metadata-queue-mib`
|
|
54
|
+
(default 32) applies to each of three metadata signal queues;
|
|
55
|
+
`--body-queue-mib` defaults to 256. These become collector environment byte
|
|
56
|
+
limits. The quota must allow at least twice the sum of payload limits, but that
|
|
57
|
+
is planning headroom, **not a guarantee that compaction fits**. No filesystem
|
|
58
|
+
quota or backend retention setting is applied. The operator must provision both
|
|
59
|
+
private storage directories on a quota-managed volume and apply/verify backend
|
|
60
|
+
retention before activation.
|
|
61
|
+
|
|
62
|
+
There is deliberately no activation command in this workflow. Service changes
|
|
63
|
+
require an explicit operator action: first back up collector config, service
|
|
64
|
+
definition and existing environments; then merge the reviewed settings using
|
|
65
|
+
the service manager. On failed health, export or backend checks, restore those
|
|
66
|
+
backups. Do not substitute `proxy install` for this review: it may regenerate
|
|
67
|
+
existing launch settings. Verify fresh metadata **and** body records, queue
|
|
68
|
+
failure counters and unchanged application-log files afterward. Package release
|
|
69
|
+
and static validation do not establish that the live service adopted the stage.
|
|
70
|
+
|
|
71
|
+
Host scheduling pressure is separate from queue durability. If the machine has
|
|
72
|
+
many concurrent builds, MCP launches or other CPU consumers, review that
|
|
73
|
+
concurrency independently. Preparation and validation do not stop processes,
|
|
74
|
+
disable scanners or change host scheduling policy.
|
|
75
|
+
|
|
8
76
|
Configure these variables in the collector service environment:
|
|
9
77
|
|
|
10
78
|
- `NEUROLINK_OPENOBSERVE_OTLP_ENDPOINT`: backend API endpoint, for example
|
|
@@ -31,7 +31,7 @@ exporters:
|
|
|
31
31
|
enabled: true
|
|
32
32
|
num_consumers: 2
|
|
33
33
|
sizer: bytes
|
|
34
|
-
queue_size: 33554432
|
|
34
|
+
queue_size: ${env:NEUROLINK_OTEL_METADATA_QUEUE_BYTES:-33554432}
|
|
35
35
|
storage: file_storage
|
|
36
36
|
otlphttp/openobserve-bodies:
|
|
37
37
|
endpoint: ${env:NEUROLINK_OPENOBSERVE_OTLP_ENDPOINT}
|
|
@@ -48,7 +48,7 @@ exporters:
|
|
|
48
48
|
enabled: true
|
|
49
49
|
num_consumers: 2
|
|
50
50
|
sizer: bytes
|
|
51
|
-
queue_size: 268435456
|
|
51
|
+
queue_size: ${env:NEUROLINK_OTEL_BODY_QUEUE_BYTES:-268435456}
|
|
52
52
|
storage: file_storage
|
|
53
53
|
extensions:
|
|
54
54
|
file_storage:
|
|
@@ -144,6 +144,89 @@ function normalizeMetadataRows(kind, rows) {
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a single exported source, never a reference chain. References retain
|
|
149
|
+
* the current phase identity and prove only transport acceptance until queried.
|
|
150
|
+
* @param {Record<string, any>} index
|
|
151
|
+
*/
|
|
152
|
+
export function resolveProxyBodyCaptureTarget(index) {
|
|
153
|
+
if (!/^[a-f0-9-]{36}$/i.test(index.captureId ?? "")) {
|
|
154
|
+
throw new Error("Invalid capture identifier");
|
|
155
|
+
}
|
|
156
|
+
if (index.bodyDelivery?.status !== "reference") {
|
|
157
|
+
if (index.bodyReference !== undefined) {
|
|
158
|
+
throw new Error("Unexpected body reference on a non-reference capture");
|
|
159
|
+
}
|
|
160
|
+
return { captureId: index.captureId, exportedAt: undefined };
|
|
161
|
+
}
|
|
162
|
+
const reference = index.bodyReference;
|
|
163
|
+
if (
|
|
164
|
+
!reference ||
|
|
165
|
+
!/^[a-f0-9-]{36}$/i.test(reference.captureId ?? "") ||
|
|
166
|
+
reference.captureId === index.captureId ||
|
|
167
|
+
typeof index.requestId !== "string" ||
|
|
168
|
+
reference.requestId !== index.requestId ||
|
|
169
|
+
!/^[a-f0-9]{64}$/i.test(index.bodySha256 ?? "") ||
|
|
170
|
+
reference.bodySha256 !== index.bodySha256 ||
|
|
171
|
+
reference.redactedBodyBytes !== index.redactedBodyBytes ||
|
|
172
|
+
!Number.isFinite(Date.parse(reference.exportedAt))
|
|
173
|
+
) {
|
|
174
|
+
throw new Error("Invalid or conflicting body reference");
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
captureId: reference.captureId,
|
|
178
|
+
exportedAt: Date.parse(reference.exportedAt),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reconstruct exact retained bytes, including reference captures, using the
|
|
184
|
+
* same event identity and chunk integrity checks as the telemetry doctor.
|
|
185
|
+
* @param {Record<string, any>} index
|
|
186
|
+
* @param {Array<Record<string, any>>} rawChunks
|
|
187
|
+
*/
|
|
188
|
+
export function reconstructProxyBodyCapture(index, rawChunks) {
|
|
189
|
+
const target = resolveProxyBodyCaptureTarget(index);
|
|
190
|
+
const normalized = normalizeBodyChunkRows(rawChunks);
|
|
191
|
+
const chunks = normalized.chunks;
|
|
192
|
+
const bytes = chunks.reduce(
|
|
193
|
+
(sum, row) =>
|
|
194
|
+
sum + (typeof row.body === "string" ? Buffer.byteLength(row.body) : Infinity),
|
|
195
|
+
0,
|
|
196
|
+
);
|
|
197
|
+
if (bytes > 8 * 1024 * 1024) {
|
|
198
|
+
throw new Error("Stored capture exceeds the 8 MiB verification bound");
|
|
199
|
+
}
|
|
200
|
+
const body = chunks.map((row) => row.body).join("");
|
|
201
|
+
const expected = Number(chunks[0]?.body_chunk_count ?? 0);
|
|
202
|
+
const verified =
|
|
203
|
+
expected > 0 &&
|
|
204
|
+
expected === chunks.length &&
|
|
205
|
+
chunks.every(
|
|
206
|
+
(row, i) =>
|
|
207
|
+
Number(row.body_chunk_index) === i &&
|
|
208
|
+
Number(row.body_chunk_count) === expected &&
|
|
209
|
+
row.body_capture_id === target.captureId &&
|
|
210
|
+
row.request_id === index.requestId,
|
|
211
|
+
) &&
|
|
212
|
+
normalized.diagnostics.missingEventIds === 0 &&
|
|
213
|
+
normalized.diagnostics.conflicts.length === 0 &&
|
|
214
|
+
bytes === index.redactedBodyBytes &&
|
|
215
|
+
createHash("sha256").update(body).digest("hex") === index.bodySha256;
|
|
216
|
+
return {
|
|
217
|
+
body,
|
|
218
|
+
captureId: index.captureId,
|
|
219
|
+
sourceCaptureId: target.captureId,
|
|
220
|
+
expectedChunks: expected,
|
|
221
|
+
storedChunks: chunks.length,
|
|
222
|
+
rawStoredChunks: rawChunks.length,
|
|
223
|
+
exactRetryDuplicates: normalized.diagnostics.exactRetryDuplicates,
|
|
224
|
+
missingEventIds: normalized.diagnostics.missingEventIds,
|
|
225
|
+
identityConflicts: normalized.diagnostics.conflicts,
|
|
226
|
+
verified,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
147
230
|
/** @param {Array<Record<string, unknown>>} rows */
|
|
148
231
|
function normalizeBodyChunkRows(rows) {
|
|
149
232
|
const byEvent = new Map();
|
|
@@ -159,6 +242,7 @@ function normalizeBodyChunkRows(rows) {
|
|
|
159
242
|
: undefined;
|
|
160
243
|
const physicalIdentity = JSON.stringify([
|
|
161
244
|
row._timestamp,
|
|
245
|
+
row.request_id,
|
|
162
246
|
row.body_capture_id,
|
|
163
247
|
row.body_chunk_index,
|
|
164
248
|
row.body_chunk_count,
|
|
@@ -576,16 +660,19 @@ export async function checkProxyTelemetry({
|
|
|
576
660
|
reason: "supervisor_telemetry_missing",
|
|
577
661
|
},
|
|
578
662
|
);
|
|
579
|
-
/** @type {Array<{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, recentFailures?: unknown, failureHistoryEvicted?: number}>} */
|
|
663
|
+
/** @type {Array<{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, byteCapacity?: number, outstandingBytes?: number, retryingRecords?: number, recentFailures?: unknown, failureHistoryEvicted?: number}>} */
|
|
580
664
|
const failures = (logs?.otel?.queues ?? []).map(
|
|
581
665
|
(
|
|
582
|
-
/** @type {{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, recentFailures?: unknown, failureHistoryEvicted?: number}} */ q,
|
|
666
|
+
/** @type {{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, byteCapacity?: number, outstandingBytes?: number, retryingRecords?: number, recentFailures?: unknown, failureHistoryEvicted?: number}} */ q,
|
|
583
667
|
) => ({
|
|
584
668
|
kind: q.kind,
|
|
585
669
|
dropped: q.dropped,
|
|
586
670
|
exportUnconfirmed: q.exportUnconfirmed,
|
|
587
671
|
outstanding: q.outstanding,
|
|
588
672
|
capacity: q.capacity,
|
|
673
|
+
byteCapacity: q.byteCapacity,
|
|
674
|
+
outstandingBytes: q.outstandingBytes,
|
|
675
|
+
retryingRecords: q.retryingRecords,
|
|
589
676
|
recentFailures: q.recentFailures,
|
|
590
677
|
failureHistoryEvicted: q.failureHistoryEvicted,
|
|
591
678
|
}),
|
|
@@ -702,7 +789,13 @@ export async function checkProxyTelemetry({
|
|
|
702
789
|
).values(),
|
|
703
790
|
];
|
|
704
791
|
const currentProducerPressure = failures.some(
|
|
705
|
-
(queue) =>
|
|
792
|
+
(queue) =>
|
|
793
|
+
queue.outstanding >= queue.capacity ||
|
|
794
|
+
(typeof queue.byteCapacity === "number" &&
|
|
795
|
+
Number.isFinite(queue.byteCapacity) &&
|
|
796
|
+
queue.byteCapacity > 0 &&
|
|
797
|
+
(queue.outstandingBytes ?? 0) >= queue.byteCapacity) ||
|
|
798
|
+
(queue.retryingRecords ?? 0) > 0,
|
|
706
799
|
);
|
|
707
800
|
const runtimeFailureHistoryIncomplete = failures.some((queue) => {
|
|
708
801
|
if (!(Number(queue.failureHistoryEvicted) > 0)) {
|
|
@@ -767,7 +860,7 @@ export async function checkProxyTelemetry({
|
|
|
767
860
|
const unhealthyCaptures = captureIndexes.filter(
|
|
768
861
|
(row) =>
|
|
769
862
|
row.captureError !== undefined ||
|
|
770
|
-
!["transport_acknowledged", "no_body", "policy_excluded"].includes(
|
|
863
|
+
!["transport_acknowledged", "reference", "no_body", "policy_excluded"].includes(
|
|
771
864
|
row.bodyDelivery?.status ?? "",
|
|
772
865
|
),
|
|
773
866
|
);
|
|
@@ -1014,9 +1107,25 @@ export async function checkProxyTelemetry({
|
|
|
1014
1107
|
(row) =>
|
|
1015
1108
|
row.captureError ||
|
|
1016
1109
|
row.bodyTruncated ||
|
|
1017
|
-
|
|
1110
|
+
(row.bodyDelivery &&
|
|
1111
|
+
"reason" in row.bodyDelivery &&
|
|
1112
|
+
row.bodyDelivery.reason === "body_byte_budget_invalid") ||
|
|
1113
|
+
!["transport_acknowledged", "reference", "policy_excluded", "no_body"].includes(
|
|
1018
1114
|
row.bodyDelivery?.status ?? "",
|
|
1019
|
-
)
|
|
1115
|
+
) ||
|
|
1116
|
+
(() => {
|
|
1117
|
+
try {
|
|
1118
|
+
if (
|
|
1119
|
+
row.bodyDelivery?.status === "reference" ||
|
|
1120
|
+
("bodyReference" in row && row.bodyReference)
|
|
1121
|
+
) {
|
|
1122
|
+
resolveProxyBodyCaptureTarget(row);
|
|
1123
|
+
}
|
|
1124
|
+
return false;
|
|
1125
|
+
} catch {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
})(),
|
|
1020
1129
|
);
|
|
1021
1130
|
add(
|
|
1022
1131
|
"capture_delivery",
|
|
@@ -1039,12 +1148,21 @@ export async function checkProxyTelemetry({
|
|
|
1039
1148
|
const lossyCaptures = indexes.filter(
|
|
1040
1149
|
(row) => row.bodyTruncated || row.redactionLossy,
|
|
1041
1150
|
);
|
|
1151
|
+
const omittedCaptures = indexes.filter(
|
|
1152
|
+
(row) =>
|
|
1153
|
+
row.bodyDelivery?.status === "policy_excluded" &&
|
|
1154
|
+
(!("reason" in row.bodyDelivery) ||
|
|
1155
|
+
row.bodyDelivery.reason !== "borrowed_traffic"),
|
|
1156
|
+
);
|
|
1042
1157
|
add(
|
|
1043
1158
|
"capture_content_completeness",
|
|
1044
|
-
lossyCaptures.length
|
|
1159
|
+
lossyCaptures.length || omittedCaptures.length
|
|
1160
|
+
? "warn"
|
|
1161
|
+
: indexes.length ? "pass" : "unverified",
|
|
1045
1162
|
{
|
|
1046
1163
|
captures: indexes.length,
|
|
1047
1164
|
truncatedOrUnparseable: lossyCaptures.length,
|
|
1165
|
+
policyExcluded: omittedCaptures.length,
|
|
1048
1166
|
captureIds: lossyCaptures.slice(0, 50).map((row) => row.captureId),
|
|
1049
1167
|
boundary:
|
|
1050
1168
|
"Transport acknowledgement does not restore source truncation, processing limits, or invalid frames removed by redaction",
|
|
@@ -1054,13 +1172,14 @@ export async function checkProxyTelemetry({
|
|
|
1054
1172
|
for (const index of indexes
|
|
1055
1173
|
.filter(
|
|
1056
1174
|
(row) =>
|
|
1057
|
-
row.bodySha256 &&
|
|
1175
|
+
row.bodySha256 &&
|
|
1176
|
+
["transport_acknowledged", "reference"].includes(
|
|
1177
|
+
row.bodyDelivery?.status ?? "",
|
|
1178
|
+
),
|
|
1058
1179
|
)
|
|
1059
1180
|
.sort((a, b) => (b.redactedBodyBytes ?? 0) - (a.redactedBodyBytes ?? 0))
|
|
1060
1181
|
.slice(0, 3)) {
|
|
1061
|
-
|
|
1062
|
-
throw new Error("Invalid capture identifier");
|
|
1063
|
-
}
|
|
1182
|
+
const target = resolveProxyBodyCaptureTarget(index);
|
|
1064
1183
|
if (
|
|
1065
1184
|
!Number.isSafeInteger(index.redactedBodyBytes) ||
|
|
1066
1185
|
(index.redactedBodyBytes ?? -1) < 0 ||
|
|
@@ -1069,53 +1188,19 @@ export async function checkProxyTelemetry({
|
|
|
1069
1188
|
throw new Error("Capture verification exceeds the 8 MiB per-body bound");
|
|
1070
1189
|
}
|
|
1071
1190
|
const rawChunks = await query(
|
|
1072
|
-
`SELECT _timestamp, proxy_event_id, body_capture_id, body_chunk_index, body_chunk_count, body FROM "${backend.bodyStream ?? backend.stream}" WHERE proxy_record_kind='body' AND body_capture_id='${
|
|
1191
|
+
`SELECT _timestamp, proxy_event_id, request_id, body_capture_id, body_chunk_index, body_chunk_count, body FROM "${backend.bodyStream ?? backend.stream}" WHERE proxy_record_kind='body' AND body_capture_id='${target.captureId}' ORDER BY body_chunk_index ASC, proxy_event_id ASC`,
|
|
1073
1192
|
"logs",
|
|
1074
1193
|
{
|
|
1075
|
-
startTime: startTime - 120e6,
|
|
1076
|
-
endTime: Math.min(Date.now() * 1000, endTime + 120e6),
|
|
1194
|
+
startTime: (target.exportedAt === undefined ? startTime : target.exportedAt * 1000) - 120e6,
|
|
1195
|
+
endTime: Math.min(Date.now() * 1000, (target.exportedAt === undefined ? endTime : target.exportedAt * 1000) + 120e6),
|
|
1077
1196
|
size: 1000,
|
|
1078
1197
|
},
|
|
1079
1198
|
);
|
|
1080
1199
|
if (rawChunks.length >= 1000) {
|
|
1081
1200
|
throw new Error("Capture verification exceeded its chunk budget");
|
|
1082
1201
|
}
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
const bytes = chunks.reduce(
|
|
1086
|
-
(sum, r) =>
|
|
1087
|
-
sum +
|
|
1088
|
-
(typeof r.body === "string" ? Buffer.byteLength(r.body) : Infinity),
|
|
1089
|
-
0,
|
|
1090
|
-
);
|
|
1091
|
-
if (bytes > 8 * 1024 * 1024) {
|
|
1092
|
-
throw new Error("Stored capture exceeds the 8 MiB verification bound");
|
|
1093
|
-
}
|
|
1094
|
-
const raw = chunks.map((r) => r.body).join(""),
|
|
1095
|
-
expected = Number(chunks[0]?.body_chunk_count ?? 0);
|
|
1096
|
-
const contiguous = chunks.every(
|
|
1097
|
-
(row, i) =>
|
|
1098
|
-
Number(row.body_chunk_index) === i &&
|
|
1099
|
-
Number(row.body_chunk_count) === expected,
|
|
1100
|
-
);
|
|
1101
|
-
const actualSha256 = createHash("sha256").update(raw).digest("hex");
|
|
1102
|
-
bodyChecks.push({
|
|
1103
|
-
captureId: index.captureId,
|
|
1104
|
-
expectedChunks: expected,
|
|
1105
|
-
storedChunks: chunks.length,
|
|
1106
|
-
rawStoredChunks: rawChunks.length,
|
|
1107
|
-
exactRetryDuplicates: normalizedChunks.diagnostics.exactRetryDuplicates,
|
|
1108
|
-
missingEventIds: normalizedChunks.diagnostics.missingEventIds,
|
|
1109
|
-
identityConflicts: normalizedChunks.diagnostics.conflicts,
|
|
1110
|
-
verified:
|
|
1111
|
-
expected > 0 &&
|
|
1112
|
-
expected === chunks.length &&
|
|
1113
|
-
contiguous &&
|
|
1114
|
-
normalizedChunks.diagnostics.missingEventIds === 0 &&
|
|
1115
|
-
normalizedChunks.diagnostics.conflicts.length === 0 &&
|
|
1116
|
-
Buffer.byteLength(raw) === index.redactedBodyBytes &&
|
|
1117
|
-
actualSha256 === index.bodySha256,
|
|
1118
|
-
});
|
|
1202
|
+
const { body: _body, ...checked } = reconstructProxyBodyCapture(index, rawChunks);
|
|
1203
|
+
bodyChecks.push(checked);
|
|
1119
1204
|
}
|
|
1120
1205
|
add(
|
|
1121
1206
|
"sample_body_integrity",
|