@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
286 lines (285 loc) • 15.4 kB
TypeScript
/**
* Shared log-sanitization helpers.
*
* Centralises truncate + secret-redaction patterns so every provider stays
* consistent and any regex improvements only need one change.
*
* Coverage:
* - `Authorization: Bearer <token>` (with required whitespace)
* - `Authorization: Token <token>` (Replicate uses this, not Bearer)
* - `Authorization: Basic <base64>` (D-ID and similar)
* - Bare tokens by known provider prefix:
* sk-/pk- (OpenAI, Anthropic, Stability),
* r8_ (Replicate), gsk_ (Groq), csk- (Cerebras), xai- (xAI),
* tgp_ (Together), fw_ (Fireworks), pplx- (Perplexity), pa- (Voyage),
* jina_ (Jina), fish- (Fish Audio)
* - Generic key=value pairs: api_key=…, access_token: …, secret_key=…
*/
/**
* Truncate `text` to `maxLen` chars then replace embedded secrets with `***`.
*
* Use this for free-form text logged from response/request bodies. For
* structured data (records, headers) prefer {@link sanitizeRecord} and
* {@link sanitizeHeaders} which know to redact by key name as well.
*
* @param text - Raw text to sanitize (typically an HTTP response body).
* @param maxLen - Maximum number of characters to keep (default 500).
*/
export declare function sanitizeForLog(text: string, maxLen?: number): string;
/**
* Stringify non-string message/tool content without ever throwing.
* JSON.stringify on a giant multimodal/tool-result payload can exceed V8's
* maximum string length and throw `RangeError: Invalid string length` — a
* generation turn must degrade to a placeholder, not abort, when that
* happens. Shared by the SDK core and providers (single source of truth).
*/
export declare function stringifyContentSafe(content: unknown): string;
/**
* Serialize an arbitrary value for a debug log without ever throwing or
* producing an unbounded string.
*
* `JSON.stringify` on a full options object (conversation history + tool
* outputs) can exceed V8's maximum string length and throw
* `RangeError: Invalid string length`, which — when evaluated eagerly inside
* a logger call — aborts the surrounding generation turn (observed in
* production). This helper caps the output and converts any stringify
* failure (RangeError, circular structure, BigInt, …) into a placeholder.
*
* Callers MUST still gate on `logger.shouldLog("debug")` per the Logger
* Guard rule — this helper makes serialization safe, not free.
*
* @param value - Arbitrary value to serialize.
* @param maxLen - Maximum number of characters to keep (default 10 000).
*/
export declare function safeDebugSerialize(value: unknown, maxLen?: number): string;
/**
* Strip embedded `user:pass@` credentials from a URL's authority component
* before logging it or surfacing it in a user-facing error.
*
* Turns `https://user:secret@host/path` into `https://***@host/path` while
* leaving credential-free URLs untouched, so the host/port/path stay useful
* for diagnostics. Use this for any `baseURL`/endpoint a caller may have
* supplied with inline credentials. The match is global, so every `//…@`
* authority in the string is redacted (e.g. a proxy chain or a URL embedded
* in a query parameter), not just the first. IPv6 literal hosts (`//u:p@[::1]/x`)
* and percent-encoded userinfo (`//u:p%40x@host/x`) are handled naturally —
* neither `[`/`]` nor `%` stop either pass below, so the bracketed host or
* encoded byte just rides along as part of the (non-redacted) remainder.
*
* This is best-effort defense-in-depth for free-form logging, deliberately
* regex-based so it can scrub *multiple* embedded authorities out of
* arbitrary text (a real URL parser only ever parses one URL at a time).
* It is NOT the primary protection for user-facing errors — that's
* {@link redactUrlForError}, which parses the single input via `new URL()`
* and reconstructs from protocol+host+pathname, never touching userinfo at
* all. Prefer that function whenever the input is known to be exactly one
* URL.
*
* Two passes, deliberately:
* 1. Well-formed authorities — credentials with no raw `/` — bounded at the
* authority's first `/` (or string end). Greedy backtracking naturally
* absorbs multiple `@` segments within one authority (e.g. `//a@b@host`
* → `//***@host`) while leaving a second, separate `//...@` authority
* elsewhere in the string (e.g. one embedded in a query parameter)
* untouched, because pass 1's own exclusion of `/` stops it at the first
* `/`, before it can ever reach a nested authority.
* 2. Malformed authorities whose credentials contain an unescaped `/`
* (e.g. `//user:sec/ret@host/path`) — pass 1 can't find an `@` before
* hitting that `/`, so this pass allows `/` and redacts up to the last
* remaining `@`. It stops before any *nested* `//` (via a negative
* lookahead) rather than excluding a specific character like `*` —
* excluding a literal character is what let a password containing both
* `/` and `*` (e.g. `//user:pa*ss/word@host/path`, legal under RFC 3986)
* slip through both passes entirely in an earlier version of this
* function.
*
* @param url - The URL (or URL-shaped string) to redact.
*/
export declare function redactUrlCredentials(url: string): string;
/**
* Reduce a URL to its origin + pathname for safe inclusion in error messages
* and logs, dropping the query string and fragment — where presigned-URL
* signatures, SAS tokens, and other secrets commonly live — while keeping
* enough of the URL (scheme, host, path) for the error to stay diagnostic.
*
* Falls back to a best-effort redacted string (query/fragment and
* `user:pass@` stripped) when the input isn't a parseable absolute URL.
* Either way the result is capped at 200 chars so a very long host/path
* can't blow up the error message.
*
* @param url - The URL (or URL-shaped string) to redact.
*/
export declare function redactUrlForError(url: string): string;
/**
* Query-parameter names that commonly carry authentication/signature
* secrets in presigned URLs, matched case-insensitively: generic
* `token`/`signature`, AWS SigV4 (`X-Amz-Signature`, `X-Amz-Credential`,
* `X-Amz-Security-Token`, and the legacy `AWSAccessKeyId`/`Signature`/
* `Expires` query-auth params), Azure SAS (`sig`/`se`/`sp`/`sr`/`sv`), and
* GCS V4 signed URLs (`X-Goog-Signature`, `X-Goog-Credential`,
* `X-Goog-Algorithm`, `X-Goog-Date`, `X-Goog-Expires`,
* `X-Goog-SignedHeaders`).
*
* Unlike {@link redactUrlForError} (which drops the *entire* query string
* for a one-off error message), this is used where a URL is normalized
* into a long-lived in-memory cache key — content-varying query params
* must survive so distinct resources still get distinct keys, but a
* presigned URL's secret must never persist as a Map key for the process
* lifetime.
*/
export declare const SENSITIVE_URL_QUERY_PARAM_DENYLIST: readonly string[];
/**
* Strip common tracking/analytics query params (`utm_*`, `fbclid`, `gclid`,
* `_ga`) from `parsed` in place, so callers that build a cache key from a
* URL don't miss a cache hit solely because two otherwise-identical URLs
* carry different tracking noise.
*
* Shared by `ImageCache.normalizeUrl` and `fileDetector.cacheKeyForUrl` —
* both build a long-lived in-memory cache key from a URL and should
* normalize tracking params the same way. Call this before
* {@link stripSensitiveUrlParamsForCacheKey} so both normalizations apply
* consistently regardless of caller.
*
* @param parsed - A `URL` instance to strip tracking params from in place.
*/
export declare function stripTrackingParams(parsed: URL): void;
/**
* Build a secret-free, but still auth-context-distinct, cache key from a
* URL that may carry presigned-URL secrets in its query string.
*
* Naively stripping {@link SENSITIVE_URL_QUERY_PARAM_DENYLIST} params before
* using a URL as a long-lived cache key (an earlier version of this code did
* exactly that) creates a correctness bug, not just a hygiene one: two
* *different* presigned URLs for the same underlying object — e.g. one
* whose signature has since expired or been revoked, and a fresh one —
* strip down to the *same* key and collide. A cache populated under the
* first would silently serve its bytes back for the second, without ever
* revalidating against the origin.
*
* Fix: still remove the sensitive params from `parsed` in place (so the
* plaintext key never retains a raw secret — the original goal), but fold a
* short, non-reversible hash of the *removed* key=value pairs into the
* returned suffix whenever any were present, so distinct auth contexts keep
* distinct cache keys. Two requests presenting the exact same secret value
* still collide (correct — that's genuinely the same authorized request);
* two different secret values for the same object no longer do.
*
* Callers combine the return value with the now-stripped `parsed.toString()`
* to form the final cache key (see `ImageCache.normalizeUrl` and
* `fileDetector.cacheKeyForUrl`), rather than this function returning the
* full key itself, so each caller can apply its own additional
* normalization (e.g. tracking-param removal) in between.
*
* Matching against {@link SENSITIVE_URL_QUERY_PARAM_DENYLIST} is
* case-insensitive (both the denylist lookup and the value folded into the
* hash lower-case the param name), so the original casing of a sensitive
* param's name is not preserved anywhere in the returned key. That's fine
* here — this produces a cache key, not a user-facing URL, and the same
* lower-cased name always hashes to the same suffix for the same value
* regardless of how the caller happened to case it (`Token=` vs `token=`).
* Non-sensitive params left on `parsed` keep their original casing untouched.
*
* @param parsed - A `URL` instance to strip sensitive params from in place.
* @returns "" when no sensitive params were present; otherwise a short hex
* suffix — prefixed with `\0` (a byte `URL#toString()` never emits, since
* every URL component is percent-encoded, so it can't collide with real
* URL content) — to append to the caller's cache key.
*/
export declare function stripSensitiveUrlParamsForCacheKey(parsed: URL): string;
/**
* Build a secret-free, auth-context-distinct cache key from a URL, for any
* caller that needs a long-lived in-memory Map key derived from a URL.
*
* Centralizes the exact sequence both existing callers used independently:
* parse via `new URL()`, strip tracking noise ({@link stripTrackingParams}),
* then strip and hash-suffix sensitive auth params
* ({@link stripSensitiveUrlParamsForCacheKey}) so distinct presigned URLs for
* the same object don't collide (see that function's doc comment for why a
* naive strip-only approach is a correctness bug, not just a hygiene one).
*
* Shared by `ImageCache.normalizeUrl` and `fileDetector.cacheKeyForUrl` — both
* build a cache key from a URL and must normalize it identically, or the same
* URL could hash to two different keys depending on which cache looked it up.
*
* Falls back to the raw `url` string unchanged when it isn't a parseable
* absolute URL (mirrors both callers' pre-existing catch behavior).
*
* @param url - The URL (or URL-shaped string) to normalize into a cache key.
*/
export declare function normalizeUrlForCache(url: string): string;
/**
* Scrub any embedded absolute URLs out of free-form text — typically the
* `.message` of an underlying network/DNS/TLS error thrown by `fetch`,
* undici's `request()`, or Node's resolver, all of which frequently embed
* the full request URL (query string, credentials, and all) verbatim.
* Interpolating that message raw into a thrown or logged error would leak a
* presigned URL's token even after the explicit URL argument was already
* redacted via {@link redactUrlForError}. Finds each `scheme://…` run in the
* text and replaces it with its redacted form.
*
* @param text - Free-form text (typically `Error.message`) to scrub.
*/
export declare function redactUrlsInText(text: string): string;
/**
* Redact every occurrence of `filePath` from an error message, checking both
* the path exactly as given and its `path.resolve()`'d form.
*
* Node's own fs errors (ENOENT/EACCES) embed the exact string passed to the
* fs call, so a plain substring match usually suffices — but wrapped or
* lower-level errors sometimes normalize the path first (relative→absolute,
* trailing-slash variations), which an exact-match-only redaction would miss.
* This is defense-in-depth, not a guarantee: a `realpath`-resolved (symlink-
* following) variant can still differ from both forms and would slip through.
*
* @param message - The error message to scrub.
* @param filePath - The path to redact (both as given and resolved).
*/
export declare function redactPathFromMessage(message: string, filePath: string): string;
/**
* Build a sanitized copy of a network/URL/file error, safe to attach as
* `Error.cause` on a rethrow.
*
* Redacting only the OUTER thrown error's message isn't enough: if the RAW
* underlying error is attached as `cause`, anything that walks the cause
* chain (cause-aware logging, telemetry spans) can still recover an
* unredacted URL/path/secret via `error.cause.message`. This returns a NEW
* `Error` whose message has been through {@link redactUrlsInText} — and,
* when `options.filePath` is supplied, {@link redactPathFromMessage} too, so
* a filesystem error's ENOENT/EACCES text doesn't leak the host's directory
* layout via the cause chain the way the outer message already avoids.
* `.name` and `.code` (`NodeJS.ErrnoException`) are copied over so retry
* classification (e.g. `isRetryableNetworkError`) still works when it
* inspects the cause. The raw original is never referenced by the result.
*
* Handles non-`Error` thrown values too (a raw string/object can just as
* easily carry an unredacted URL or path) by running their `String()` form
* through the same redaction before wrapping.
*
* @param error - The raw underlying error (or thrown value) to sanitize.
* @param options - `filePath`: a known filesystem path to redact to its
* basename, in addition to URL redaction.
*/
export declare function sanitizeErrorCause(error: unknown, options?: {
filePath?: string;
}): Error;
/**
* Recursively sanitize a record/array, returning a structurally identical
* value with sensitive keys redacted and string values run through
* {@link sanitizeForLog}.
*
* Safe to call on any JSON-shaped data. Cycles are detected and replaced
* with the string `"[Circular]"` to avoid infinite recursion when logging
* mid-stream objects that reference themselves.
*
* @param value - The value to sanitize.
* @param maxStringLen - Per-string truncation cap (default 1000).
*/
export declare function sanitizeRecord<T>(value: T, maxStringLen?: number): T;
/**
* Sanitize an HTTP headers object — redacts sensitive header names entirely
* (`***`) and applies {@link sanitizeForLog} to remaining values.
*
* Accepts both `Headers` instances and plain-object header maps so providers
* can log either shape uniformly.
*/
export declare function sanitizeHeaders(headers: Headers | Record<string, string | undefined> | undefined): Record<string, string>;