@juspay/neurolink
Version:
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
308 lines (307 loc) • 11.2 kB
JavaScript
import { join } from "node:path";
import { mkdir, chmod, writeFile } from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
import { promisify } from "node:util";
import { sanitizeForLog } from "../utils/logSanitize.js";
import { gzip as gzipCallback } from "node:zlib";
const REQUEST_LOG_IO_TIMEOUT_MS = 5_000;
/** Maximum redacted body bytes persisted per capture entry. */
const MAX_CAPTURED_BODY_BYTES = 1024 * 1024;
const MAX_OTEL_CAPTURED_BODY_BYTES = 8 * 1024 * 1024;
const BODY_TRUNCATION_MARKER = "\n...[TRUNCATED]";
const gzip = promisify(gzipCallback);
/** Headers whose values must always be redacted. */
const SENSITIVE_HEADER_NAMES = new Set([
"authorization",
"proxy-authorization",
"x-api-key",
"cookie",
"set-cookie",
]);
/** Pattern that matches header names likely to contain secrets. */
const SENSITIVE_HEADER_PATTERN = /token|secret|key|password|credential/i;
/** JSON keys whose values should be redacted in request/response bodies. */
const SENSITIVE_BODY_KEYS = /("(?:password|access_token|refresh_token|api_key|apiKey|secret|authorization|token|credential|x-api-key)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
/**
* Copy headers while removing credential values, including custom
* secret-bearing header names.
*/
function redactHeaders(headers) {
if (!headers) {
return headers;
}
const redacted = {};
for (const [key, value] of Object.entries(headers)) {
const lower = key.toLowerCase();
if (SENSITIVE_HEADER_NAMES.has(lower) ||
SENSITIVE_HEADER_PATTERN.test(lower)) {
redacted[key] = "[REDACTED]";
}
else {
redacted[key] = value;
}
}
return redacted;
}
const SENSITIVE_BODY_KEY = /^(?:password|access_token|refresh_token|api_key|apiKey|secret|authorization|token|credential|x-api-key)$/i;
/** Error messages may contain nested JSON encoded as a string. */
function redactNestedValue(key, value) {
if (SENSITIVE_BODY_KEY.test(key)) {
return "[REDACTED]";
}
return typeof value === "string"
? sanitizeForLog(value.replace(SENSITIVE_BODY_KEYS, '$1"[REDACTED]"'), value.length)
: value;
}
/** Redact every value under a sensitive key, including objects and arrays. */
function redactBody(body, redaction) {
if (body === undefined || body === null) {
return undefined;
}
let value = body;
if (typeof body === "string") {
try {
value = JSON.parse(body);
}
catch {
if (/^(?:event|data):/m.test(body)) {
// Redact whole SSE data values, including nested credentials. Never
// retain a truncated/invalid JSON tail whose secret cannot be parsed.
return body
.split(/\r\n\r\n|\n\n|\r\r/)
.map((frame) => {
const lines = frame.split(/\r\n|\n|\r/);
const data = lines
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trimStart())
.join("\n");
if (!data || data === "[DONE]") {
return frame;
}
let redacted;
try {
redacted = JSON.stringify(JSON.parse(data), redactNestedValue);
}
catch {
redaction.unparseableFrames++;
redacted = "[UNPARSEABLE DATA REDACTED]";
}
return [
...lines.filter((line) => !line.startsWith("data:")),
`data: ${redacted}`,
].join("\n");
})
.join("\n\n");
}
// Invalid or truncated structured JSON must not expose an unterminated
// credential value that a quoted-string regex cannot safely redact.
if (/^\s*[[{]/.test(body)) {
redaction.unparseableFrames++;
return "[UNPARSEABLE DATA REDACTED]";
}
// Plain-text bodies retain the existing credential-field redaction.
return sanitizeForLog(body.replace(SENSITIVE_BODY_KEYS, '$1"[REDACTED]"'), body.length);
}
}
return JSON.stringify(value, redactNestedValue);
}
/**
* Restrict request and phase identifiers to characters safe for artifact
* path components.
*/
function sanitizePhase(phase) {
return phase.replace(/[^a-zA-Z0-9._-]+/g, "_");
}
/**
* Hash the redacted artifact body so offline reconstruction can verify its
* contents.
*/
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
/**
* Measure persisted UTF-8 bytes rather than JavaScript UTF-16 character
* counts.
*/
function utf8ByteLength(value) {
return Buffer.byteLength(value, "utf8");
}
/**
* Reserve space for the truncation marker and cut only at a UTF-8 code
* point boundary.
*/
function truncateUtf8String(input, maxBytes, marker = BODY_TRUNCATION_MARKER) {
const inputBytes = utf8ByteLength(input);
if (inputBytes <= maxBytes) {
return { value: input, bytes: inputBytes, truncated: false };
}
const markerBytes = utf8ByteLength(marker);
if (maxBytes <= markerBytes) {
return { value: marker, bytes: markerBytes, truncated: true };
}
const buffer = Buffer.from(input, "utf8");
let end = maxBytes - markerBytes;
while (end > 0 && (buffer[end] & 0xc0) === 0x80) {
end -= 1;
}
const value = buffer.subarray(0, end).toString("utf8");
const truncatedValue = `${value}${marker}`;
return {
value: truncatedValue,
bytes: utf8ByteLength(truncatedValue),
truncated: true,
};
}
/**
* Split redacted text into byte-bounded OTLP chunks without splitting
* encoded characters.
*/
export function splitUtf8StringByBytes(input, maxBytes) {
if (!input) {
return [""];
}
const chunks = [];
const buffer = Buffer.from(input, "utf8");
for (let start = 0; start < buffer.length;) {
let end = Math.min(start + Math.max(4, maxBytes), buffer.length);
while (end < buffer.length && (buffer[end] & 0xc0) === 0x80) {
end -= 1;
}
chunks.push(buffer.subarray(start, end).toString("utf8"));
start = end;
}
return chunks;
}
/**
* Apply structural redaction before enforcing the per-artifact byte
* ceiling.
*/
function prepareRedactedBody(body, maxBytes = MAX_CAPTURED_BODY_BYTES) {
const redaction = { unparseableFrames: 0 };
const redacted = redactBody(body, redaction);
if (redacted === undefined) {
return { truncated: false, ...redaction };
}
return {
...truncateUtf8String(redacted, maxBytes),
originalBytes: utf8ByteLength(redacted),
...redaction,
};
}
/**
* Write a private gzip artifact with a unique name and return its
* redacted-body digest.
*/
async function writeBodyArtifact(logDir, entry, redactedHeaders, redactedBody, bodyTruncated) {
if (redactedBody === undefined) {
return {};
}
const dateStr = new Date(entry.timestamp).toISOString().split("T")[0];
const bodyDir = join(logDir, "bodies", dateStr, sanitizePhase(entry.requestId));
await mkdir(bodyDir, { recursive: true, mode: 0o700 });
await chmod(bodyDir, 0o700);
const fileName = `${randomUUID()}-${sanitizePhase(entry.phase)}` +
(entry.attempt !== undefined ? `-attempt-${entry.attempt}` : "") +
`.json.gz`;
const bodyPath = join(bodyDir, fileName);
const payload = JSON.stringify({
timestamp: entry.timestamp,
requestId: entry.requestId,
phase: entry.phase,
model: entry.model,
stream: entry.stream,
account: entry.account,
accountType: entry.accountType,
attempt: entry.attempt,
responseStatus: entry.responseStatus,
durationMs: entry.durationMs,
contentType: entry.contentType,
headers: redactedHeaders,
body: redactedBody,
traceId: entry.traceId,
spanId: entry.spanId,
metadata: entry.metadata,
});
const compressed = await gzip(payload);
await writeFile(bodyPath, compressed, {
mode: 0o600,
signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
});
return {
bodyPath,
bodySha256: sha256(redactedBody),
redactedBodyBytes: utf8ByteLength(redactedBody),
storedFileBytes: compressed.byteLength,
redactedBody,
bodyTruncated,
};
}
/** Shared pure redaction for replay; serving paths invoke it in the worker. */
export function prepareProxyBodyForLogging(body) {
return prepareRedactedBody(body);
}
/**
* Expose the same header-redaction policy to replay and metadata
* consumers.
*/
export function redactProxyHeadersForLogging(headers) {
return redactHeaders(headers);
}
/**
* Process a capture in the worker and retain redacted output when artifact
* persistence fails.
*/
export async function processProxyBodyCapture(entry, logDir) {
const headers = redactHeaders(entry.headers);
const limit = logDir === null ? MAX_OTEL_CAPTURED_BODY_BYTES : MAX_CAPTURED_BODY_BYTES;
const prepared = prepareRedactedBody(entry.body, limit);
// Structured objects no longer carry their wire byte representation. Do not
// serialize a second unredacted copy merely to manufacture a byte count.
const inputRetainedBytes = typeof entry.body === "string" ? utf8ByteLength(entry.body) : undefined;
const captureMetadata = {
sourceTruncated: entry.sourceTruncated === true,
processingTruncated: prepared.truncated,
inputRetainedBytes,
inputEncoding: typeof entry.body === "string"
? "utf8_text"
: "structured_object",
redactionLossy: prepared.unparseableFrames > 0,
unparseableRedactedFrames: prepared.unparseableFrames,
};
prepared.truncated ||= captureMetadata.sourceTruncated;
if (logDir === null) {
return {
headers,
stored: {
...captureMetadata,
redactedBody: prepared.value,
redactedBodyBytes: prepared.bytes,
bodyTruncated: prepared.truncated,
bodyCaptureLimitBytes: limit,
originalRedactedBodyBytes: prepared.originalBytes,
bodySha256: prepared.value === undefined ? undefined : sha256(prepared.value),
},
};
}
let stored;
try {
stored = await writeBodyArtifact(logDir, entry, headers, prepared.value, prepared.truncated);
}
catch {
stored = {
redactedBody: prepared.value,
redactedBodyBytes: prepared.bytes,
bodyTruncated: prepared.truncated,
bodyWriteFailed: true,
};
}
return {
headers,
stored: {
...stored,
...captureMetadata,
bodyCaptureLimitBytes: limit,
originalRedactedBodyBytes: prepared.originalBytes,
},
};
}