@juspay/neurolink 12.19.2 → 12.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/adapters/imageFormatSupport.d.ts +5 -2
  3. package/dist/adapters/imageFormatSupport.js +47 -24
  4. package/dist/browser/neurolink.min.js +394 -394
  5. package/dist/cli/commands/proxy.js +171 -25
  6. package/dist/cli/commands/telemetry.d.ts +4 -1
  7. package/dist/cli/commands/telemetry.js +113 -0
  8. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  9. package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
  10. package/dist/cli/utils/nativeTelemetry.js +322 -0
  11. package/dist/core/modules/MessageBuilder.js +2 -0
  12. package/dist/neurolink.js +1 -0
  13. package/dist/processors/config/fileExtensions.d.ts +8 -1
  14. package/dist/processors/config/fileExtensions.js +8 -1
  15. package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
  16. package/dist/proxy/bodyCapturePolicy.js +132 -0
  17. package/dist/proxy/otelLogSink.d.ts +11 -1
  18. package/dist/proxy/otelLogSink.js +174 -27
  19. package/dist/proxy/otlpLogTransport.d.ts +4 -2
  20. package/dist/proxy/otlpLogTransport.js +22 -7
  21. package/dist/proxy/requestLogger.js +11 -3
  22. package/dist/proxy/restartControl.js +18 -3
  23. package/dist/proxy/rollingProxyServer.js +48 -1
  24. package/dist/proxy/rollingWorkerProcess.js +87 -10
  25. package/dist/proxy/rollingWorkerProtocol.js +4 -2
  26. package/dist/proxy/rollingWorkerSupervisor.js +47 -10
  27. package/dist/proxy/socketWorkerRuntime.js +90 -4
  28. package/dist/proxy/updateChecker.d.ts +3 -3
  29. package/dist/proxy/updateChecker.js +16 -9
  30. package/dist/proxy/updateCoordinator.d.ts +10 -1
  31. package/dist/proxy/updateCoordinator.js +68 -1
  32. package/dist/proxy/updateState.d.ts +2 -0
  33. package/dist/proxy/updateState.js +15 -0
  34. package/dist/rag/pipeline/RAGPipeline.js +29 -0
  35. package/dist/rag/ragIntegration.js +21 -1
  36. package/dist/types/cli.d.ts +18 -0
  37. package/dist/types/file.d.ts +10 -0
  38. package/dist/types/generate.d.ts +27 -1
  39. package/dist/types/proxy.d.ts +97 -3
  40. package/dist/types/stream.d.ts +13 -1
  41. package/dist/utils/imageProcessor.js +151 -6
  42. package/dist/utils/messageBuilder.d.ts +2 -2
  43. package/dist/utils/messageBuilder.js +23 -13
  44. package/dist/utils/multimodalOptionsBuilder.d.ts +3 -0
  45. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  46. package/docs-site/static/search-index.json +1 -1
  47. package/package.json +2 -1
  48. package/scripts/observability/NATIVE-DURABLE.md +68 -0
  49. package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
  50. package/scripts/observability/proxy-telemetry-check.mjs +135 -50
@@ -0,0 +1,322 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { lstat, mkdir, mkdtemp, readFile, rm, writeFile, } from "node:fs/promises";
5
+ import { dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import yaml from "js-yaml";
8
+ const execute = promisify(execFile);
9
+ const profilePath = fileURLToPath(new URL("../../../scripts/observability/otel-collector.proxy-native-durable.yaml", import.meta.url));
10
+ const mib = 1024 * 1024;
11
+ const stagedFiles = [
12
+ "collector.yaml",
13
+ "collector.env.json",
14
+ "proxy.env",
15
+ "doctor.env",
16
+ ];
17
+ const pendingChecks = [
18
+ "Run native-validate with the intended collector executable",
19
+ "Review source collector customizations against the staged standard profile; custom processors, TLS and exporter settings are not automatically copied",
20
+ "Create private queue/compaction directories (0700) on a volume with the declared quota; queue payload bounds do not enforce a disk quota",
21
+ "Apply and verify backend stream retention policies; preparation does not call the backend",
22
+ "Merge proxy and doctor environment snippets into existing settings; supply doctor backend authorization privately",
23
+ "Operator must back up config/service/env, explicitly activate through their service manager, and roll back those backups if health, export, stream or persistence checks fail",
24
+ "Reconcile fresh request, body and backend records after activation; validation alone is not runtime proof",
25
+ ];
26
+ function record(value) {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
28
+ throw new Error("Expected an object in the native collector configuration");
29
+ }
30
+ return value;
31
+ }
32
+ function text(value, field) {
33
+ if (typeof value !== "string" || !value || /[\r\n\0]/.test(value)) {
34
+ throw new Error(`Missing or invalid ${field}`);
35
+ }
36
+ const resolved = value.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_match, key, fallback) => {
37
+ const replacement = process.env[key] ?? fallback;
38
+ if (replacement === undefined) {
39
+ throw new Error(`Set the source collector environment variable ${key} before preparation`);
40
+ }
41
+ return replacement;
42
+ });
43
+ if (!resolved || /\$\{|[\r\n\0]/.test(resolved)) {
44
+ throw new Error(`Unsupported substitution in ${field}`);
45
+ }
46
+ return resolved;
47
+ }
48
+ function positiveInteger(value, name, max) {
49
+ if (!Number.isSafeInteger(value) || value <= 0 || value > max) {
50
+ throw new Error(`${name} must be a positive integer no greater than ${max}`);
51
+ }
52
+ return value;
53
+ }
54
+ function privatePath(value, name) {
55
+ if (!isAbsolute(value) || /[\r\n\0]/.test(value)) {
56
+ throw new Error(`${name} must be an absolute path`);
57
+ }
58
+ const path = resolve(value);
59
+ if (path === dirname(path)) {
60
+ throw new Error(`${name} must not be a filesystem root`);
61
+ }
62
+ return path;
63
+ }
64
+ function loopbackListen(value, name) {
65
+ const endpoint = text(value, name);
66
+ const match = endpoint.match(/^127\.0\.0\.1:(\d+)$/);
67
+ if (!match) {
68
+ throw new Error(`${name} must use an explicit 127.0.0.1 listener`);
69
+ }
70
+ positiveInteger(Number(match[1]), name, 65535);
71
+ return endpoint;
72
+ }
73
+ function digest(value) {
74
+ return createHash("sha256").update(value).digest("hex");
75
+ }
76
+ /** Produce new private files only. Existing config, services and queues are untouched. */
77
+ export async function prepareNativeTelemetry(args) {
78
+ const output = privatePath(args.output, "output");
79
+ const queue = privatePath(args.queueDirectory, "queue-directory");
80
+ const compact = privatePath(args.compactionDirectory, "compaction-directory");
81
+ if ([queue, compact].some((path) => path === output ||
82
+ path.startsWith(`${output}/`) ||
83
+ output.startsWith(`${path}/`))) {
84
+ throw new Error("Staging and collector storage paths must not overlap");
85
+ }
86
+ if (queue === compact ||
87
+ queue.startsWith(`${compact}/`) ||
88
+ compact.startsWith(`${queue}/`)) {
89
+ throw new Error("Queue and compaction directories must be separate, non-nested paths");
90
+ }
91
+ const metadataMib = positiveInteger(args.metadataQueueMib, "metadata-queue-mib", 1024);
92
+ const bodyMib = positiveInteger(args.bodyQueueMib, "body-queue-mib", 4096);
93
+ const quotaMib = positiveInteger(args.diskQuotaMib, "disk-quota-mib", 1048576);
94
+ if (quotaMib < 2 * (3 * metadataMib + bodyMib)) {
95
+ throw new Error("disk-quota-mib must allow at least twice all four queue payload limits for storage and compaction overhead");
96
+ }
97
+ positiveInteger(args.metadataRetentionDays, "metadata-retention-days", 3650);
98
+ positiveInteger(args.bodyRetentionDays, "body-retention-days", 3650);
99
+ positiveInteger(args.bodyPort, "body-port", 65535);
100
+ const sourcePath = resolve(args.collectorConfig);
101
+ const source = await readFile(sourcePath, "utf8");
102
+ let config;
103
+ try {
104
+ config = record(yaml.load(source));
105
+ }
106
+ catch {
107
+ // YAML parser diagnostics can contain complete source lines with credentials.
108
+ throw new Error("Could not parse the source collector YAML; source details are suppressed");
109
+ }
110
+ const exporter = record(record(config.exporters)["otlphttp/openobserve"]);
111
+ const headers = record(exporter.headers);
112
+ const endpoint = text(exporter.endpoint, "OpenObserve endpoint");
113
+ let url;
114
+ try {
115
+ url = new URL(endpoint);
116
+ }
117
+ catch {
118
+ throw new Error("Invalid OpenObserve endpoint");
119
+ }
120
+ if (url.username ||
121
+ url.password ||
122
+ url.search ||
123
+ url.hash ||
124
+ !/^\/api\/[a-zA-Z0-9_-]+\/?$/.test(url.pathname) ||
125
+ !(url.protocol === "https:" ||
126
+ (url.protocol === "http:" &&
127
+ ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))) {
128
+ throw new Error("OpenObserve endpoint must be /api/<organization>, without embedded credentials; require HTTPS outside loopback");
129
+ }
130
+ const authorization = text(headers.Authorization ?? headers.authorization, "OpenObserve authorization");
131
+ const metadataStream = text(headers["stream-name"] ?? "neurolink_proxy", "metadata stream");
132
+ const bodyStream = args.bodyStream ?? `${metadataStream}_bodies`;
133
+ if (![metadataStream, bodyStream].every((value) => /^[a-zA-Z0-9_]+$/.test(value)) ||
134
+ metadataStream === bodyStream) {
135
+ throw new Error("Metadata and body streams must have distinct alphanumeric/underscore names");
136
+ }
137
+ const receivers = record(config.receivers);
138
+ const metadataListen = loopbackListen(record(record(record(receivers.otlp ?? receivers["otlp/metadata"]).protocols)
139
+ .http).endpoint, "metadata listener");
140
+ const extensions = record(config.extensions ?? {});
141
+ const healthListen = loopbackListen(record(extensions.health_check ?? { endpoint: "127.0.0.1:14333" }).endpoint, "health listener");
142
+ const telemetry = record(record(config.service).telemetry ?? {});
143
+ const metrics = record(telemetry.metrics ?? {});
144
+ const readers = Array.isArray(metrics.readers) ? metrics.readers : [];
145
+ const prometheus = readers
146
+ .map((reader) => record(record(record(reader).pull ?? {}).exporter ?? {}).prometheus)
147
+ .find((value) => value !== undefined);
148
+ const metricsPort = prometheus ? Number(record(prometheus).port) : 14388;
149
+ if (prometheus && record(prometheus).host !== "127.0.0.1") {
150
+ throw new Error("Collector metrics must use an explicit 127.0.0.1 listener");
151
+ }
152
+ positiveInteger(metricsPort, "metrics port", 65535);
153
+ const ports = [
154
+ Number(metadataListen.split(":")[1]),
155
+ Number(healthListen.split(":")[1]),
156
+ metricsPort,
157
+ args.bodyPort,
158
+ ];
159
+ if (new Set(ports).size !== ports.length) {
160
+ throw new Error("Metadata, bodies, health and metrics must use distinct ports");
161
+ }
162
+ const profile = await readFile(profilePath, "utf8");
163
+ const env = {
164
+ NEUROLINK_OTEL_METADATA_LISTEN: metadataListen,
165
+ NEUROLINK_OTEL_BODIES_LISTEN: `127.0.0.1:${args.bodyPort}`,
166
+ NEUROLINK_OTEL_HEALTH_LISTEN: healthListen,
167
+ NEUROLINK_OTEL_METRICS_PORT: String(metricsPort),
168
+ NEUROLINK_OTEL_QUEUE_DIRECTORY: queue,
169
+ NEUROLINK_OTEL_COMPACTION_DIRECTORY: compact,
170
+ NEUROLINK_OTEL_METADATA_QUEUE_BYTES: String(metadataMib * mib),
171
+ NEUROLINK_OTEL_BODY_QUEUE_BYTES: String(bodyMib * mib),
172
+ NEUROLINK_OPENOBSERVE_OTLP_ENDPOINT: endpoint,
173
+ NEUROLINK_OPENOBSERVE_BASIC_AUTH: authorization,
174
+ NEUROLINK_PROXY_STREAM_HEADER: metadataStream,
175
+ NEUROLINK_PROXY_BODY_STREAM_HEADER: bodyStream,
176
+ };
177
+ // Every value in these public snippets is independently restricted above.
178
+ const proxyEnv = `NEUROLINK_PROXY_LOG_SINK=otel\nOTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://${metadataListen}/v1/logs\nNEUROLINK_PROXY_OTLP_BODIES_ENDPOINT=http://127.0.0.1:${args.bodyPort}/v1/logs\n`;
179
+ const doctorEnv = `NEUROLINK_OPENOBSERVE_URL=${url.origin}\nNEUROLINK_OPENOBSERVE_ORG=${url.pathname.split("/")[2]}\nNEUROLINK_PROXY_STREAM_HEADER=${metadataStream}\nNEUROLINK_PROXY_BODY_STREAM_HEADER=${bodyStream}\nNEUROLINK_OTEL_COLLECTOR_METRICS_URL=http://127.0.0.1:${metricsPort}/metrics\n`;
180
+ const contents = [
181
+ profile,
182
+ `${JSON.stringify(env, null, 2)}\n`,
183
+ proxyEnv,
184
+ doctorEnv,
185
+ ];
186
+ const manifest = {
187
+ schemaVersion: 1,
188
+ activationImplemented: false,
189
+ source: { path: sourcePath, sha256: digest(source) },
190
+ files: Object.fromEntries(stagedFiles.map((name, index) => [name, digest(contents[index])])),
191
+ metadataStream,
192
+ bodyStream,
193
+ collectorMinimumVersion: "0.160.0",
194
+ queuePayloadBytes: {
195
+ metadataPerSignal: metadataMib * mib,
196
+ bodies: bodyMib * mib,
197
+ total: (3 * metadataMib + bodyMib) * mib,
198
+ },
199
+ operatorPolicy: {
200
+ diskQuotaBytes: quotaMib * mib,
201
+ metadataRetentionDays: args.metadataRetentionDays,
202
+ bodyRetentionDays: args.bodyRetentionDays,
203
+ enforced: false,
204
+ },
205
+ pending: pendingChecks,
206
+ };
207
+ // Exclusive creation refuses an existing destination, including a symlink.
208
+ await mkdir(output, { mode: 0o700 });
209
+ try {
210
+ for (let index = 0; index < stagedFiles.length; index++) {
211
+ await writeFile(join(output, stagedFiles[index]), contents[index], {
212
+ mode: 0o600,
213
+ flag: "wx",
214
+ });
215
+ }
216
+ await writeFile(join(output, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600, flag: "wx" });
217
+ }
218
+ catch {
219
+ await rm(output, { recursive: true, force: true });
220
+ throw new Error("Could not write the private staging directory; incomplete staged files were removed");
221
+ }
222
+ return {
223
+ directory: output,
224
+ prepared: true,
225
+ activated: false,
226
+ metadataStream,
227
+ bodyStream,
228
+ pending: pendingChecks,
229
+ };
230
+ }
231
+ /** Runs only the collector's static validator, with test-owned temporary storage. */
232
+ export async function validateNativeTelemetry(args) {
233
+ const directory = privatePath(args.directory, "directory");
234
+ const binary = privatePath(args.collectorBin, "collector-bin");
235
+ const permissions = await lstat(directory);
236
+ if (!permissions.isDirectory() || (permissions.mode & 0o077) !== 0) {
237
+ throw new Error("Staging directory must be private (0700)");
238
+ }
239
+ for (const name of ["manifest.json", ...stagedFiles]) {
240
+ const permissions = await lstat(join(directory, name));
241
+ if (!permissions.isFile() || (permissions.mode & 0o077) !== 0) {
242
+ throw new Error("Staged files must be private regular files (0600)");
243
+ }
244
+ }
245
+ let manifest;
246
+ let env;
247
+ try {
248
+ manifest = record(JSON.parse(await readFile(join(directory, "manifest.json"), "utf8")));
249
+ env = record(JSON.parse(await readFile(join(directory, "collector.env.json"), "utf8")));
250
+ }
251
+ catch {
252
+ throw new Error("Invalid native telemetry stage; prepare a new directory");
253
+ }
254
+ if (manifest.schemaVersion !== 1 ||
255
+ manifest.activationImplemented !== false) {
256
+ throw new Error("Unsupported native telemetry staging manifest");
257
+ }
258
+ for (const name of stagedFiles) {
259
+ const file = join(directory, name);
260
+ if (digest(await readFile(file, "utf8")) !== record(manifest.files)[name]) {
261
+ throw new Error("Staged files changed or are not private; prepare a new directory before validation");
262
+ }
263
+ }
264
+ const source = record(manifest.source);
265
+ if (typeof source.path !== "string" ||
266
+ digest(await readFile(source.path, "utf8")) !== source.sha256) {
267
+ throw new Error("Source collector configuration changed; prepare again to avoid validating a stale migration");
268
+ }
269
+ const collectorEnv = {};
270
+ for (const [key, value] of Object.entries(env)) {
271
+ if (typeof value !== "string" ||
272
+ !/^(NEUROLINK_OTEL_|NEUROLINK_OPENOBSERVE_|NEUROLINK_PROXY_)/.test(key)) {
273
+ throw new Error("Invalid staged collector environment");
274
+ }
275
+ collectorEnv[key] = value;
276
+ }
277
+ let version;
278
+ try {
279
+ const result = await execute(binary, ["--version"], {
280
+ timeout: 15000,
281
+ maxBuffer: 65536,
282
+ });
283
+ const match = `${result.stdout}\n${result.stderr}`.match(/\b(\d+)\.(\d+)\.(\d+)([-+][\w.-]+)?\b/);
284
+ if (!match ||
285
+ match[4] ||
286
+ (Number(match[1]) === 0 && Number(match[2]) < 160)) {
287
+ throw new Error("unsupported");
288
+ }
289
+ version = `${match[1]}.${match[2]}.${match[3]}`;
290
+ }
291
+ catch {
292
+ throw new Error("Could not verify collector >= 0.160.0 (stable); executable diagnostics are suppressed to protect credentials");
293
+ }
294
+ const temporary = await mkdtemp(join(directory, "validation-"));
295
+ try {
296
+ await execute(binary, ["validate", "--config", join(directory, "collector.yaml")], {
297
+ env: {
298
+ ...process.env,
299
+ ...collectorEnv,
300
+ NEUROLINK_OTEL_QUEUE_DIRECTORY: join(temporary, "queue"),
301
+ NEUROLINK_OTEL_COMPACTION_DIRECTORY: join(temporary, "compact"),
302
+ },
303
+ timeout: 30000,
304
+ maxBuffer: 1048576,
305
+ });
306
+ }
307
+ catch {
308
+ throw new Error("Collector rejected the staged profile or validation timed out; executable diagnostics are suppressed to protect credentials");
309
+ }
310
+ finally {
311
+ await rm(temporary, { recursive: true, force: true });
312
+ }
313
+ return {
314
+ directory,
315
+ valid: true,
316
+ collectorVersion: version,
317
+ activated: false,
318
+ runtimeVerified: false,
319
+ pending: pendingChecks.slice(1),
320
+ };
321
+ }
322
+ //# sourceMappingURL=nativeTelemetry.js.map
@@ -104,6 +104,7 @@ export class MessageBuilder {
104
104
  },
105
105
  csvOptions: options.csvOptions,
106
106
  pdfOptions: options.pdfOptions,
107
+ imageOptions: options.imageOptions,
107
108
  provider: options.provider,
108
109
  model: options.model,
109
110
  temperature: options.temperature,
@@ -226,6 +227,7 @@ export class MessageBuilder {
226
227
  },
227
228
  csvOptions: options.csvOptions,
228
229
  pdfOptions: options.pdfOptions,
230
+ imageOptions: options.imageOptions,
229
231
  provider: options.provider,
230
232
  model: options.model,
231
233
  temperature: options.temperature,
package/dist/neurolink.js CHANGED
@@ -4210,6 +4210,7 @@ Current user's request: ${currentInput}`;
4210
4210
  // reached convertToImages.
4211
4211
  csvOptions: options.csvOptions,
4212
4212
  pdfOptions: options.pdfOptions,
4213
+ imageOptions: options.imageOptions,
4213
4214
  region: options.region,
4214
4215
  tts: options.tts,
4215
4216
  stt: options.stt,
@@ -13,7 +13,14 @@
13
13
  */
14
14
  /**
15
15
  * Image file extensions supported by the platform.
16
- * Includes `.svg`, which is processed as sanitized markup rather than raster.
16
+ *
17
+ * Includes `.svg`. On the processor path (`SvgProcessor`) that guarantees
18
+ * sanitized markup, never raster bytes — but that guarantee is scoped to
19
+ * that one consumer. This constant is also consumed by code that treats
20
+ * every image extension as raster (captioning, multimodal embedding, vision
21
+ * adapters); `.svg` is markup there too, just without the sanitization, so
22
+ * each such consumer must explicitly handle or reject `.svg` itself rather
23
+ * than assume this list implies raster-safety.
17
24
  */
18
25
  export declare const IMAGE_EXTENSIONS: readonly string[];
19
26
  /**
@@ -17,7 +17,14 @@ import { extensionsForModality } from "./fileTypeRegistry.js";
17
17
  // =============================================================================
18
18
  /**
19
19
  * Image file extensions supported by the platform.
20
- * Includes `.svg`, which is processed as sanitized markup rather than raster.
20
+ *
21
+ * Includes `.svg`. On the processor path (`SvgProcessor`) that guarantees
22
+ * sanitized markup, never raster bytes — but that guarantee is scoped to
23
+ * that one consumer. This constant is also consumed by code that treats
24
+ * every image extension as raster (captioning, multimodal embedding, vision
25
+ * adapters); `.svg` is markup there too, just without the sanitization, so
26
+ * each such consumer must explicitly handle or reject `.svg` itself rather
27
+ * than assume this list implies raster-safety.
21
28
  */
22
29
  export const IMAGE_EXTENSIONS = extensionsForModality("image");
23
30
  /**
@@ -0,0 +1,6 @@
1
+ import type { ProxyBodyCaptureEntry, ProxyBodyCapturePolicyOptions, ProxyBodyCapturePolicySnapshot, ProxyBodyCapturePublication, ProxyBodyDeliveryResult, StoredBodyArtifact } from "../types/index.js";
2
+ /** Diagnostic-only policy. Never receives or changes an inference body. */
3
+ export declare function createProxyBodyCapturePolicy(options?: ProxyBodyCapturePolicyOptions): {
4
+ snapshot(): ProxyBodyCapturePolicySnapshot;
5
+ publish(entry: ProxyBodyCaptureEntry, stored: StoredBodyArtifact, publish: () => Promise<ProxyBodyDeliveryResult | undefined>): Promise<ProxyBodyCapturePublication>;
6
+ };
@@ -0,0 +1,132 @@
1
+ const MAX_REFERENCES = 1024;
2
+ const REFERENCE_TTL_MS = 5 * 60_000;
3
+ /** Diagnostic-only policy. Never receives or changes an inference body. */
4
+ export function createProxyBodyCapturePolicy(options = {}) {
5
+ const now = options.now ?? (() => performance.now());
6
+ const configured = options.bytesPerMinute?.trim();
7
+ const budget = configured ? Number(configured) : null;
8
+ const invalidBudget = budget !== null && (!Number.isSafeInteger(budget) || budget <= 0);
9
+ const references = new Map();
10
+ let availableBytes = budget ?? Infinity;
11
+ let lastRefill = now();
12
+ let submittedBytes = 0;
13
+ let deduplicatedCaptures = 0;
14
+ let deduplicatedBytes = 0;
15
+ let policyExcludedCaptures = 0;
16
+ let policyExcludedBytes = 0;
17
+ const maintain = () => {
18
+ const at = now();
19
+ if (budget !== null && !invalidBudget) {
20
+ availableBytes = Math.min(budget, availableBytes + (Math.max(0, at - lastRefill) * budget) / 60_000);
21
+ }
22
+ lastRefill = at;
23
+ for (const [key, value] of references) {
24
+ if (value.expiresAt <= at) {
25
+ references.delete(key);
26
+ }
27
+ }
28
+ };
29
+ return {
30
+ snapshot() {
31
+ maintain();
32
+ return {
33
+ deduplicationEnabled: options.deduplicate !== false,
34
+ maxReferences: MAX_REFERENCES,
35
+ referenceTtlMs: REFERENCE_TTL_MS,
36
+ references: references.size,
37
+ byteBudgetPerMinute: invalidBudget ? null : budget,
38
+ invalidByteBudget: invalidBudget,
39
+ availableBytes: budget === null || invalidBudget ? null : Math.floor(availableBytes),
40
+ submittedBytes,
41
+ deduplicatedCaptures,
42
+ deduplicatedBytes,
43
+ policyExcludedCaptures,
44
+ policyExcludedBytes,
45
+ };
46
+ },
47
+ async publish(entry, stored, publish) {
48
+ maintain();
49
+ if (stored.redactedBody === undefined) {
50
+ return {};
51
+ }
52
+ const bytes = Buffer.byteLength(stored.redactedBody, "utf8");
53
+ const key = options.deduplicate !== false && stored.bodySha256 && entry.captureId
54
+ ? JSON.stringify([entry.requestId, stored.bodySha256, bytes])
55
+ : undefined;
56
+ const prior = key ? references.get(key) : undefined;
57
+ // Only detached diagnostic work waits. An uncertain export never becomes
58
+ // a reference, and references always point directly to an exported body.
59
+ if (prior &&
60
+ prior.reference.captureId !== entry.captureId &&
61
+ (await prior.acknowledged) &&
62
+ prior.expiresAt > now()) {
63
+ deduplicatedCaptures++;
64
+ deduplicatedBytes += bytes;
65
+ return {
66
+ delivery: {
67
+ status: "reference",
68
+ reason: "identical_redacted_payload",
69
+ },
70
+ reference: { ...prior.reference },
71
+ };
72
+ }
73
+ maintain();
74
+ if (invalidBudget || bytes > availableBytes) {
75
+ policyExcludedCaptures++;
76
+ policyExcludedBytes += bytes;
77
+ return {
78
+ delivery: {
79
+ status: "policy_excluded",
80
+ reason: invalidBudget
81
+ ? "body_byte_budget_invalid"
82
+ : "body_byte_budget_exhausted",
83
+ },
84
+ };
85
+ }
86
+ availableBytes -= bytes;
87
+ submittedBytes += bytes;
88
+ let acknowledge = () => { };
89
+ const candidate = key && entry.captureId && stored.bodySha256
90
+ ? {
91
+ reference: {
92
+ captureId: entry.captureId,
93
+ requestId: entry.requestId,
94
+ bodySha256: stored.bodySha256,
95
+ redactedBodyBytes: bytes,
96
+ exportedAt: "",
97
+ },
98
+ expiresAt: now() + REFERENCE_TTL_MS,
99
+ acknowledged: new Promise((resolve) => {
100
+ acknowledge = resolve;
101
+ }),
102
+ }
103
+ : undefined;
104
+ if (key && candidate) {
105
+ // Eviction only loses an optimization. It cannot invalidate an emitted
106
+ // reference, whose source remains an independently exported body.
107
+ if (references.size >= MAX_REFERENCES) {
108
+ const oldest = references.keys().next().value;
109
+ if (oldest !== undefined) {
110
+ references.delete(oldest);
111
+ }
112
+ }
113
+ references.set(key, candidate);
114
+ }
115
+ let accepted = false;
116
+ try {
117
+ const delivery = await publish();
118
+ accepted = delivery?.status === "transport_acknowledged";
119
+ if (candidate && accepted) {
120
+ candidate.reference.exportedAt = new Date().toISOString();
121
+ }
122
+ return { delivery };
123
+ }
124
+ finally {
125
+ acknowledge(accepted);
126
+ if (key && !accepted && references.get(key) === candidate) {
127
+ references.delete(key);
128
+ }
129
+ }
130
+ },
131
+ };
132
+ }
@@ -52,12 +52,22 @@ export declare function getProxyOtelLogSnapshot(): {
52
52
  exportUnconfirmed: number;
53
53
  dropped: number;
54
54
  outstanding: number;
55
+ outstandingBytes: number;
56
+ highWaterBytes: number;
57
+ byteLimitDrops: number;
58
+ retryingRecords: number;
59
+ retriedBatches: number;
60
+ retentionExpired: number;
61
+ lastTransientFailureAt: string | undefined;
55
62
  lastAcknowledgedAt: string | undefined;
56
63
  lastFailureAt: string | undefined;
57
64
  highWaterOutstanding: number;
58
65
  failureHistoryEvicted: number;
59
- kind: "metadata" | "bodies";
66
+ kind: "metadata" | "bodies" | "indexes" | "diagnostics";
60
67
  capacity: number;
68
+ byteCapacity: number;
69
+ retentionMs: number;
70
+ oldestOutstandingAgeMs: number;
61
71
  }[];
62
72
  };
63
73
  /** Bounded provider flush belongs after final request and lifecycle publication. */