@juspay/neurolink 12.12.9 → 12.12.11

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 (40) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/browser/neurolink.min.js +394 -396
  3. package/dist/cli/commands/proxy.d.ts +4 -0
  4. package/dist/cli/commands/proxy.js +60 -18
  5. package/dist/cli/commands/proxyAnalyze.js +8 -1
  6. package/dist/core/baseProvider.d.ts +0 -22
  7. package/dist/core/baseProvider.js +18 -81
  8. package/dist/providers/anthropic/client.js +4 -1
  9. package/dist/providers/openaiChatCompletionsBase.js +4 -1
  10. package/dist/proxy/bodyCaptureProcessing.d.ts +22 -0
  11. package/dist/proxy/bodyCaptureProcessing.js +219 -0
  12. package/dist/proxy/bodyCaptureWorker.d.ts +16 -0
  13. package/dist/proxy/bodyCaptureWorker.js +232 -0
  14. package/dist/proxy/bodyCaptureWorkerEntry.d.ts +1 -0
  15. package/dist/proxy/bodyCaptureWorkerEntry.js +34 -0
  16. package/dist/proxy/proxyAnalysis.js +159 -17
  17. package/dist/proxy/proxyLifecycle.d.ts +25 -0
  18. package/dist/proxy/proxyLifecycle.js +111 -5
  19. package/dist/proxy/proxyRequestKind.d.ts +2 -0
  20. package/dist/proxy/proxyRequestKind.js +6 -0
  21. package/dist/proxy/proxyRuntimeMetrics.d.ts +3 -0
  22. package/dist/proxy/proxyRuntimeMetrics.js +34 -0
  23. package/dist/proxy/requestLogger.d.ts +10 -6
  24. package/dist/proxy/requestLogger.js +99 -231
  25. package/dist/proxy/rollingProxyServer.js +15 -4
  26. package/dist/proxy/rollingWorkerProcess.d.ts +4 -0
  27. package/dist/proxy/rollingWorkerProcess.js +25 -8
  28. package/dist/proxy/rollingWorkerProtocol.d.ts +6 -0
  29. package/dist/proxy/rollingWorkerProtocol.js +12 -1
  30. package/dist/proxy/rollingWorkerSupervisor.d.ts +28 -0
  31. package/dist/proxy/rollingWorkerSupervisor.js +73 -25
  32. package/dist/proxy/socketWorkerRuntime.d.ts +5 -0
  33. package/dist/proxy/socketWorkerRuntime.js +19 -2
  34. package/dist/server/routes/codexProxyRoutes.js +39 -3
  35. package/dist/services/server/ai/observability/instrumentation.js +7 -1
  36. package/dist/types/cli.d.ts +1 -1
  37. package/dist/types/proxy.d.ts +69 -4
  38. package/package.json +1 -1
  39. package/dist/core/modules/GenerationHandler.d.ts +0 -145
  40. package/dist/core/modules/GenerationHandler.js +0 -754
@@ -0,0 +1,219 @@
1
+ import { join } from "node:path";
2
+ import { mkdir, chmod, writeFile } from "node:fs/promises";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { promisify } from "node:util";
5
+ import { gzip as gzipCallback } from "node:zlib";
6
+ const REQUEST_LOG_IO_TIMEOUT_MS = 5_000;
7
+ /** Maximum redacted body bytes persisted per capture entry. */
8
+ const MAX_CAPTURED_BODY_BYTES = 1024 * 1024;
9
+ const BODY_TRUNCATION_MARKER = "\n...[TRUNCATED]";
10
+ const gzip = promisify(gzipCallback);
11
+ /** Headers whose values must always be redacted. */
12
+ const SENSITIVE_HEADER_NAMES = new Set([
13
+ "authorization",
14
+ "proxy-authorization",
15
+ "x-api-key",
16
+ "cookie",
17
+ "set-cookie",
18
+ ]);
19
+ /** Pattern that matches header names likely to contain secrets. */
20
+ const SENSITIVE_HEADER_PATTERN = /token|secret|key|password|credential/i;
21
+ /** JSON keys whose values should be redacted in request/response bodies. */
22
+ const SENSITIVE_BODY_KEYS = /("(?:password|access_token|refresh_token|api_key|apiKey|secret|authorization|token|credential|x-api-key)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
23
+ /**
24
+ * Copy headers while removing credential values, including custom
25
+ * secret-bearing header names.
26
+ */
27
+ function redactHeaders(headers) {
28
+ if (!headers) {
29
+ return headers;
30
+ }
31
+ const redacted = {};
32
+ for (const [key, value] of Object.entries(headers)) {
33
+ const lower = key.toLowerCase();
34
+ if (SENSITIVE_HEADER_NAMES.has(lower) ||
35
+ SENSITIVE_HEADER_PATTERN.test(lower)) {
36
+ redacted[key] = "[REDACTED]";
37
+ }
38
+ else {
39
+ redacted[key] = value;
40
+ }
41
+ }
42
+ return redacted;
43
+ }
44
+ const SENSITIVE_BODY_KEY = /^(?:password|access_token|refresh_token|api_key|apiKey|secret|authorization|token|credential|x-api-key)$/i;
45
+ /** Redact every value under a sensitive key, including objects and arrays. */
46
+ function redactBody(body) {
47
+ if (body === undefined || body === null) {
48
+ return undefined;
49
+ }
50
+ let value = body;
51
+ if (typeof body === "string") {
52
+ try {
53
+ value = JSON.parse(body);
54
+ }
55
+ catch {
56
+ // Non-JSON bodies (including SSE transcripts) keep the legacy fallback.
57
+ return body.replace(SENSITIVE_BODY_KEYS, '$1"[REDACTED]"');
58
+ }
59
+ }
60
+ return JSON.stringify(value, (key, nested) => SENSITIVE_BODY_KEY.test(key) ? "[REDACTED]" : nested);
61
+ }
62
+ /**
63
+ * Restrict request and phase identifiers to characters safe for artifact
64
+ * path components.
65
+ */
66
+ function sanitizePhase(phase) {
67
+ return phase.replace(/[^a-zA-Z0-9._-]+/g, "_");
68
+ }
69
+ /**
70
+ * Hash the redacted artifact body so offline reconstruction can verify its
71
+ * contents.
72
+ */
73
+ function sha256(value) {
74
+ return createHash("sha256").update(value).digest("hex");
75
+ }
76
+ /**
77
+ * Measure persisted UTF-8 bytes rather than JavaScript UTF-16 character
78
+ * counts.
79
+ */
80
+ function utf8ByteLength(value) {
81
+ return Buffer.byteLength(value, "utf8");
82
+ }
83
+ /**
84
+ * Reserve space for the truncation marker and cut only at a UTF-8 code
85
+ * point boundary.
86
+ */
87
+ function truncateUtf8String(input, maxBytes, marker = BODY_TRUNCATION_MARKER) {
88
+ const inputBytes = utf8ByteLength(input);
89
+ if (inputBytes <= maxBytes) {
90
+ return { value: input, bytes: inputBytes, truncated: false };
91
+ }
92
+ const markerBytes = utf8ByteLength(marker);
93
+ if (maxBytes <= markerBytes) {
94
+ return { value: marker, bytes: markerBytes, truncated: true };
95
+ }
96
+ const buffer = Buffer.from(input, "utf8");
97
+ let end = maxBytes - markerBytes;
98
+ while (end > 0 && (buffer[end] & 0xc0) === 0x80) {
99
+ end -= 1;
100
+ }
101
+ const value = buffer.subarray(0, end).toString("utf8");
102
+ const truncatedValue = `${value}${marker}`;
103
+ return {
104
+ value: truncatedValue,
105
+ bytes: utf8ByteLength(truncatedValue),
106
+ truncated: true,
107
+ };
108
+ }
109
+ /**
110
+ * Split redacted text into byte-bounded OTLP chunks without splitting
111
+ * encoded characters.
112
+ */
113
+ export function splitUtf8StringByBytes(input, maxBytes) {
114
+ if (!input) {
115
+ return [""];
116
+ }
117
+ const chunks = [];
118
+ const buffer = Buffer.from(input, "utf8");
119
+ for (let start = 0; start < buffer.length;) {
120
+ let end = Math.min(start + Math.max(4, maxBytes), buffer.length);
121
+ while (end < buffer.length && (buffer[end] & 0xc0) === 0x80) {
122
+ end -= 1;
123
+ }
124
+ chunks.push(buffer.subarray(start, end).toString("utf8"));
125
+ start = end;
126
+ }
127
+ return chunks;
128
+ }
129
+ /**
130
+ * Apply structural redaction before enforcing the per-artifact byte
131
+ * ceiling.
132
+ */
133
+ function prepareRedactedBody(body) {
134
+ const redacted = redactBody(body);
135
+ if (redacted === undefined) {
136
+ return { truncated: false };
137
+ }
138
+ return truncateUtf8String(redacted, MAX_CAPTURED_BODY_BYTES);
139
+ }
140
+ /**
141
+ * Write a private gzip artifact with a unique name and return its
142
+ * redacted-body digest.
143
+ */
144
+ async function writeBodyArtifact(logDir, entry, redactedHeaders, redactedBody, bodyTruncated) {
145
+ if (redactedBody === undefined) {
146
+ return {};
147
+ }
148
+ const dateStr = new Date(entry.timestamp).toISOString().split("T")[0];
149
+ const bodyDir = join(logDir, "bodies", dateStr, sanitizePhase(entry.requestId));
150
+ await mkdir(bodyDir, { recursive: true, mode: 0o700 });
151
+ await chmod(bodyDir, 0o700);
152
+ const fileName = `${randomUUID()}-${sanitizePhase(entry.phase)}` +
153
+ (entry.attempt !== undefined ? `-attempt-${entry.attempt}` : "") +
154
+ `.json.gz`;
155
+ const bodyPath = join(bodyDir, fileName);
156
+ const payload = JSON.stringify({
157
+ timestamp: entry.timestamp,
158
+ requestId: entry.requestId,
159
+ phase: entry.phase,
160
+ model: entry.model,
161
+ stream: entry.stream,
162
+ account: entry.account,
163
+ accountType: entry.accountType,
164
+ attempt: entry.attempt,
165
+ responseStatus: entry.responseStatus,
166
+ durationMs: entry.durationMs,
167
+ contentType: entry.contentType,
168
+ headers: redactedHeaders,
169
+ body: redactedBody,
170
+ traceId: entry.traceId,
171
+ spanId: entry.spanId,
172
+ metadata: entry.metadata,
173
+ });
174
+ const compressed = await gzip(payload);
175
+ await writeFile(bodyPath, compressed, {
176
+ mode: 0o600,
177
+ signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
178
+ });
179
+ return {
180
+ bodyPath,
181
+ bodySha256: sha256(redactedBody),
182
+ redactedBodyBytes: utf8ByteLength(redactedBody),
183
+ storedFileBytes: compressed.byteLength,
184
+ redactedBody,
185
+ bodyTruncated,
186
+ };
187
+ }
188
+ /** Shared pure redaction for replay; serving paths invoke it in the worker. */
189
+ export function prepareProxyBodyForLogging(body) {
190
+ return prepareRedactedBody(body);
191
+ }
192
+ /**
193
+ * Expose the same header-redaction policy to replay and metadata
194
+ * consumers.
195
+ */
196
+ export function redactProxyHeadersForLogging(headers) {
197
+ return redactHeaders(headers);
198
+ }
199
+ /**
200
+ * Process a capture in the worker and retain redacted output when artifact
201
+ * persistence fails.
202
+ */
203
+ export async function processProxyBodyCapture(entry, logDir) {
204
+ const headers = redactHeaders(entry.headers);
205
+ const prepared = prepareRedactedBody(entry.body);
206
+ let stored;
207
+ try {
208
+ stored = await writeBodyArtifact(logDir, entry, headers, prepared.value, prepared.truncated);
209
+ }
210
+ catch {
211
+ stored = {
212
+ redactedBody: prepared.value,
213
+ redactedBodyBytes: prepared.bytes,
214
+ bodyTruncated: prepared.truncated,
215
+ bodyWriteFailed: true,
216
+ };
217
+ }
218
+ return { headers, stored };
219
+ }
@@ -0,0 +1,16 @@
1
+ import type { ProcessedProxyBodyCapture, ProxyBodyCaptureEntry, ProxyBodyCaptureWorkerSnapshot } from "../types/index.js";
2
+ export declare const PROXY_BODY_CAPTURE_DEADLINE_MS = 20000;
3
+ /** Bounded bulk capture. Failures are indexed; never fall back to blocking work. */
4
+ export declare function captureProxyBody(entry: ProxyBodyCaptureEntry, logDir: string, consume: (result: ProcessedProxyBodyCapture) => Promise<void>): Promise<void>;
5
+ /**
6
+ * Return independent counters for processing, rejection, failure, and
7
+ * retained publication work.
8
+ */
9
+ export declare function getBodyCaptureWorkerSnapshot(): ProxyBodyCaptureWorkerSnapshot;
10
+ /** Isolated tests point at a separately executed built worker. */
11
+ export declare const __bodyCaptureWorkerTestHooks: {
12
+ /**
13
+ * Reset an isolated worker after capture publications drain, optionally selecting a fixture entry.
14
+ */
15
+ reset(url?: URL): Promise<void>;
16
+ };
@@ -0,0 +1,232 @@
1
+ import { Worker } from "node:worker_threads";
2
+ const MAX_PENDING = 16;
3
+ const MAX_PENDING_BYTES = 32 * 1024 * 1024;
4
+ const MAX_ENTRY_BYTES = 8 * 1024 * 1024;
5
+ export const PROXY_BODY_CAPTURE_DEADLINE_MS = 20_000;
6
+ let worker;
7
+ let workerUrl;
8
+ let retryAfter = 0;
9
+ let nextId = 0;
10
+ const snapshot = {
11
+ attempted: 0,
12
+ completed: 0,
13
+ rejected: 0,
14
+ failed: 0,
15
+ pending: 0,
16
+ pendingBytes: 0,
17
+ maxPending: MAX_PENDING,
18
+ maxPendingBytes: MAX_PENDING_BYTES,
19
+ };
20
+ const pending = new Map();
21
+ // Bound traversal as well as the structured clone sent to the worker. Never
22
+ // invoke getters/toJSON or stringify a large body on the serving event loop.
23
+ /**
24
+ * Conservatively bound clone size and traversal work without invoking
25
+ * getters or serializers.
26
+ */
27
+ function estimateCloneBytes(value) {
28
+ const stack = [value];
29
+ const seen = new Set();
30
+ let bytes = 0, nodes = 0;
31
+ while (stack.length) {
32
+ if (++nodes > 100_000 || bytes > MAX_ENTRY_BYTES) {
33
+ return Infinity;
34
+ }
35
+ const item = stack.pop();
36
+ if (typeof item === "string") {
37
+ bytes += item.length * 3;
38
+ continue;
39
+ }
40
+ bytes += 16;
41
+ if (!item || typeof item !== "object") {
42
+ continue;
43
+ }
44
+ if (seen.has(item)) {
45
+ return Infinity;
46
+ }
47
+ seen.add(item);
48
+ if (!Array.isArray(item) &&
49
+ Object.getPrototypeOf(item) !== Object.prototype &&
50
+ Object.getPrototypeOf(item) !== null) {
51
+ return Infinity;
52
+ }
53
+ for (const key of Object.keys(item)) {
54
+ bytes += key.length * 3;
55
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
56
+ if (!descriptor || descriptor.get || descriptor.set) {
57
+ return Infinity;
58
+ }
59
+ stack.push(descriptor.value);
60
+ if (stack.length > 100_000 || bytes > MAX_ENTRY_BYTES) {
61
+ return Infinity;
62
+ }
63
+ }
64
+ }
65
+ return bytes;
66
+ }
67
+ /**
68
+ * Settle IPC ownership once while publication retains the capture memory
69
+ * lease.
70
+ */
71
+ function settle(id, result) {
72
+ const task = pending.get(id);
73
+ if (!task) {
74
+ return;
75
+ }
76
+ pending.delete(id);
77
+ clearTimeout(task.timer);
78
+ task.resolve(result);
79
+ if (!pending.size) {
80
+ worker?.unref();
81
+ }
82
+ }
83
+ /**
84
+ * Fail every pending capture explicitly and back off without moving bulk
85
+ * work to the caller.
86
+ */
87
+ function failWorker(current, reason) {
88
+ if (worker !== current) {
89
+ return;
90
+ }
91
+ worker = undefined;
92
+ retryAfter = Date.now() + 5_000;
93
+ for (const id of pending.keys()) {
94
+ settle(id, {
95
+ error: reason,
96
+ stored: { bodyWriteFailed: true },
97
+ });
98
+ }
99
+ void current.terminate().catch(() => undefined);
100
+ }
101
+ /**
102
+ * Lazily create the bounded worker; only admitted processing keeps it
103
+ * referenced.
104
+ */
105
+ function getWorker() {
106
+ if (worker) {
107
+ return worker;
108
+ }
109
+ const current = new Worker(workerUrl ?? new URL("./bodyCaptureWorkerEntry.js", import.meta.url), {
110
+ execArgv: process.execArgv.filter((arg) => !arg.startsWith("--input-type")),
111
+ resourceLimits: { maxOldGenerationSizeMb: 128 },
112
+ });
113
+ worker = current;
114
+ current.on("message", (message) => {
115
+ if (worker === current) {
116
+ settle(message.id, message.result);
117
+ }
118
+ });
119
+ current.on("error", () => failWorker(current, "body_worker_error"));
120
+ current.on("exit", () => failWorker(current, "body_worker_exit"));
121
+ current.unref();
122
+ return current;
123
+ }
124
+ /** Bounded bulk capture. Failures are indexed; never fall back to blocking work. */
125
+ export async function captureProxyBody(entry, logDir, consume) {
126
+ snapshot.attempted += 1;
127
+ let bytes;
128
+ try {
129
+ bytes = estimateCloneBytes(entry);
130
+ }
131
+ catch {
132
+ bytes = Infinity;
133
+ }
134
+ if (bytes > MAX_ENTRY_BYTES ||
135
+ snapshot.pending >= MAX_PENDING ||
136
+ snapshot.pendingBytes + bytes > MAX_PENDING_BYTES ||
137
+ Date.now() < retryAfter) {
138
+ snapshot.rejected += 1;
139
+ const error = bytes > MAX_ENTRY_BYTES
140
+ ? "body_capture_too_large_or_non_json"
141
+ : Date.now() < retryAfter
142
+ ? "body_worker_backoff"
143
+ : "body_capture_queue_full";
144
+ snapshot.lastError = error;
145
+ return consume({ error, stored: { bodyWriteFailed: true } });
146
+ }
147
+ let current;
148
+ try {
149
+ current = getWorker();
150
+ }
151
+ catch {
152
+ snapshot.failed += 1;
153
+ retryAfter = Date.now() + 5_000;
154
+ return consume({
155
+ error: "body_worker_start_failed",
156
+ stored: { bodyWriteFailed: true },
157
+ });
158
+ }
159
+ const id = ++nextId;
160
+ return new Promise((resolve) => {
161
+ const timer = setTimeout(() => failWorker(current, "body_worker_timeout"), PROXY_BODY_CAPTURE_DEADLINE_MS);
162
+ timer.unref();
163
+ pending.set(id, {
164
+ timer,
165
+ resolve: (result) => {
166
+ // Keep the byte/count lease through index writes and OTLP publication,
167
+ // so completed worker results cannot form an unbounded parent backlog.
168
+ void consume(result)
169
+ .catch(() => {
170
+ result.error ??= "body_capture_publication_failed";
171
+ })
172
+ .finally(() => {
173
+ snapshot.pending -= 1;
174
+ snapshot.pendingBytes -= bytes;
175
+ if (result.error || result.stored.bodyWriteFailed) {
176
+ snapshot.failed += 1;
177
+ }
178
+ else {
179
+ snapshot.completed += 1;
180
+ }
181
+ if (result.error) {
182
+ snapshot.lastError = result.error;
183
+ }
184
+ resolve();
185
+ });
186
+ },
187
+ });
188
+ snapshot.pending += 1;
189
+ snapshot.pendingBytes += bytes;
190
+ current.ref();
191
+ try {
192
+ current.postMessage({ id, entry, logDir, queuedAt: Date.now() });
193
+ }
194
+ catch {
195
+ settle(id, {
196
+ error: "body_capture_clone_failed",
197
+ stored: { bodyWriteFailed: true },
198
+ });
199
+ }
200
+ });
201
+ }
202
+ /**
203
+ * Return independent counters for processing, rejection, failure, and
204
+ * retained publication work.
205
+ */
206
+ export function getBodyCaptureWorkerSnapshot() {
207
+ return { ...snapshot };
208
+ }
209
+ /** Isolated tests point at a separately executed built worker. */
210
+ export const __bodyCaptureWorkerTestHooks = {
211
+ /**
212
+ * Reset an isolated worker after capture publications drain, optionally selecting a fixture entry.
213
+ */
214
+ async reset(url) {
215
+ if (worker) {
216
+ const current = worker;
217
+ failWorker(current, "body_worker_test_reset");
218
+ await current.terminate();
219
+ }
220
+ workerUrl = url;
221
+ retryAfter = 0;
222
+ Object.assign(snapshot, {
223
+ attempted: 0,
224
+ completed: 0,
225
+ rejected: 0,
226
+ failed: 0,
227
+ pending: 0,
228
+ pendingBytes: 0,
229
+ lastError: undefined,
230
+ });
231
+ },
232
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { performance } from "node:perf_hooks";
3
+ import { processProxyBodyCapture } from "./bodyCaptureProcessing.js";
4
+ // Sequential processing bounds serialization/compression memory. The parent
5
+ // bounds both the queued record count and clone bytes, including in-flight work.
6
+ let tail = Promise.resolve();
7
+ parentPort?.on("message", (message) => {
8
+ tail = tail.then(async () => {
9
+ const started = performance.now();
10
+ const queueWaitMs = Math.max(0, Date.now() - message.queuedAt);
11
+ try {
12
+ const result = await processProxyBodyCapture(message.entry, message.logDir);
13
+ parentPort?.postMessage({
14
+ id: message.id,
15
+ result: {
16
+ ...result,
17
+ queueWaitMs,
18
+ processingMs: performance.now() - started,
19
+ },
20
+ });
21
+ }
22
+ catch {
23
+ parentPort?.postMessage({
24
+ id: message.id,
25
+ result: {
26
+ error: "body_capture_processing_failed",
27
+ stored: { bodyWriteFailed: true },
28
+ queueWaitMs,
29
+ processingMs: performance.now() - started,
30
+ },
31
+ });
32
+ }
33
+ });
34
+ });