@juspay/neurolink 13.8.26 → 13.8.27

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.
@@ -14,12 +14,16 @@
14
14
  * tgp_ (Together), fw_ (Fireworks), pplx- (Perplexity), pa- (Voyage),
15
15
  * jina_ (Jina), fish- (Fish Audio)
16
16
  * - Generic key=value pairs: api_key=…, access_token: …, secret_key=…
17
+ * - Bare JWTs (`eyJ…` header, payload and signature)
18
+ * - Values of any key ending in `token`: breezeToken=…, "id_token": "…"
17
19
  */
18
20
  /**
19
- * Whether `key` names a value that must never appear in a log — the same list
20
- * {@link sanitizeRecord} masks, matched case-insensitively.
21
+ * Whether `key` names a value that must never appear in a log: a key on the
22
+ * list above, matched case-insensitively, or any key ending in `token`
23
+ * (`breezeToken`, `id_token`) unless its value is a number or boolean
24
+ * (`costPerToken`, `hasRefreshToken`).
21
25
  */
22
- export declare function isSensitiveLogKey(key: string): boolean;
26
+ export declare function isSensitiveLogKey(key: string, value?: unknown): boolean;
23
27
  /**
24
28
  * Truncate `text` to `maxLen` chars then replace embedded secrets with `***`.
25
29
  *
@@ -14,6 +14,8 @@
14
14
  * tgp_ (Together), fw_ (Fireworks), pplx- (Perplexity), pa- (Voyage),
15
15
  * jina_ (Jina), fish- (Fish Audio)
16
16
  * - Generic key=value pairs: api_key=…, access_token: …, secret_key=…
17
+ * - Bare JWTs (`eyJ…` header, payload and signature)
18
+ * - Values of any key ending in `token`: breezeToken=…, "id_token": "…"
17
19
  */
18
20
  import { createHash } from "crypto";
19
21
  import { basename, resolve as resolvePath } from "path";
@@ -47,7 +49,15 @@ const SECRET_PATTERN = new RegExp(
47
49
  // Bare tokens by known prefix (e.g. `sk-abc…`, `r8_xyz…`)
48
50
  `|\\b(?:${PREFIX_PATTERN})[_\\-][A-Za-z0-9_\\-\\.]{8,}\\b` +
49
51
  // Generic key=value pairs (URL params, JSON bodies, header dumps)
50
- "|\\b(?:api[_-]?key|access[_-]?token|secret[_-]?key|refresh[_-]?token)['\"]?\\s*[:=]\\s*['\"]?[^\\s,;'\"&]+", "gi");
52
+ "|\\b(?:api[_-]?key|access[_-]?token|secret[_-]?key|refresh[_-]?token)['\"]?\\s*[:=]\\s*['\"]?[^\\s,;'\"&]+" +
53
+ // JWTs — the signature segment is empty on an unsigned token. A match
54
+ // starts only where a run of token characters starts, so `eyJ-eyJ-…`
55
+ // is scanned once rather than once per `eyJ`.
56
+ "|(?<![A-Za-z0-9_\\-])eyJ[A-Za-z0-9_\\-]{8,}\\.[A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]*" +
57
+ // Any other key ending in `token` (`breezeToken=…`, `"id_token":"…"`),
58
+ // as isSensitiveLogKey masks object keys: the key is kept, and a value
59
+ // that is a number, boolean or null is left alone
60
+ "|\\b([A-Za-z0-9_]*token['\"]?\\s*[:=]\\s*)(?:(['\"])[^'\"]*|(?!(?:true|false|null|-?\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?)(?![^\\s,;'\"&}\\])]))[^\\s,;'\"&}\\])]+)", "gi");
51
61
  /** Header names that should always be redacted regardless of value shape. */
52
62
  const SENSITIVE_HEADER_NAMES = [
53
63
  "authorization",
@@ -84,14 +94,22 @@ const SENSITIVE_OBJECT_KEYS = [
84
94
  "secretAccessKey",
85
95
  // PII, not a secret — but it has no business in debug log dumps.
86
96
  "userEmail",
97
+ "phoneNumber",
87
98
  ];
88
99
  /**
89
- * Whether `key` names a value that must never appear in a log — the same list
90
- * {@link sanitizeRecord} masks, matched case-insensitively.
100
+ * Whether `key` names a value that must never appear in a log: a key on the
101
+ * list above, matched case-insensitively, or any key ending in `token`
102
+ * (`breezeToken`, `id_token`) unless its value is a number or boolean
103
+ * (`costPerToken`, `hasRefreshToken`).
91
104
  */
92
- export function isSensitiveLogKey(key) {
105
+ export function isSensitiveLogKey(key, value) {
93
106
  const lower = key.toLowerCase();
94
- return SENSITIVE_OBJECT_KEYS.some((name) => name.toLowerCase() === lower);
107
+ if (SENSITIVE_OBJECT_KEYS.some((name) => name.toLowerCase() === lower)) {
108
+ return true;
109
+ }
110
+ return (lower.endsWith("token") &&
111
+ typeof value !== "number" &&
112
+ typeof value !== "boolean");
95
113
  }
96
114
  /**
97
115
  * Truncate `text` to `maxLen` chars then replace embedded secrets with `***`.
@@ -107,7 +125,9 @@ export function sanitizeForLog(text, maxLen = 500) {
107
125
  if (!text) {
108
126
  return text;
109
127
  }
110
- return text.slice(0, maxLen).replace(SECRET_PATTERN, "***");
128
+ return text
129
+ .slice(0, maxLen)
130
+ .replace(SECRET_PATTERN, (_match, key, quote) => key === undefined ? "***" : `${key}${quote ?? ""}***`);
111
131
  }
112
132
  /**
113
133
  * Stringify non-string message/tool content without ever throwing.
@@ -530,12 +550,7 @@ export function sanitizeRecord(value, maxStringLen = 1000) {
530
550
  }
531
551
  const out = {};
532
552
  for (const [k, val] of Object.entries(v)) {
533
- if (SENSITIVE_OBJECT_KEYS.some((name) => name.toLowerCase() === k.toLowerCase())) {
534
- out[k] = "***";
535
- }
536
- else {
537
- out[k] = walk(val);
538
- }
553
+ out[k] = isSensitiveLogKey(k, val) ? "***" : walk(val);
539
554
  }
540
555
  return out;
541
556
  };
@@ -14,7 +14,7 @@
14
14
  * - Tabular data display
15
15
  */
16
16
  import { AsyncLocalStorage } from "node:async_hooks";
17
- import { isSensitiveLogKey, sanitizeForLog, sanitizeRecord, } from "./logSanitize.js";
17
+ import { isSensitiveLogKey, sanitizeForLog } from "./logSanitize.js";
18
18
  /**
19
19
  * Identifies which NeuroLink instance is emitting, for the async operation
20
20
  * currently on the stack. Populated by `runInInstanceScope()` — which the SDK
@@ -92,8 +92,6 @@ function cloneLogData(data) {
92
92
  * logger's "zero truncation" promise for debug data stands.
93
93
  */
94
94
  const REDACTION_STRING_CAP = 1_000_000;
95
- /** Depth past which a payload is handed to `sanitizeRecord` wholesale. */
96
- const REDACTION_MAX_DEPTH = 32;
97
95
  function isPlainObject(value) {
98
96
  const proto = Object.getPrototypeOf(value);
99
97
  return proto === Object.prototype || proto === null;
@@ -105,17 +103,21 @@ function isPlainObject(value) {
105
103
  * `sanitizeRecord` itself is not used directly on the whole payload because
106
104
  * its `Object.entries` walk flattens what debug data is full of: an `Error`
107
105
  * becomes `{}` (message and stack gone), a `Buffer` becomes thousands of
108
- * numeric keys, a `Date`/`Map`/`Set` becomes `{}`. Those are left as they are
109
- * — `serializeData` and `structuredClone` already know how to carry them —
110
- * and only plain containers are walked, with sensitive keys masked and
111
- * string values scrubbed of bearer tokens and known key prefixes. An `Error`
112
- * gets its message scrubbed in place of the instance.
106
+ * numeric keys, a `Date`/`Map`/`Set` becomes `{}`. Here binary data, dates
107
+ * and regexps are kept as they are, a `Map` or `Set` is copied with its
108
+ * entries redacted, and any other object is reduced to what `JSON.stringify`
109
+ * prints for it (its `toJSON()` result or its own fields) — the same fields
110
+ * the console and a `structuredClone` for a sink would expose. Sensitive keys
111
+ * are masked, strings scrubbed of bearer tokens and known key prefixes, an
112
+ * object reached by several paths is redacted once and its copy shared, a
113
+ * cycle becomes `"[Circular]"`, and an `Error` gets its message scrubbed in
114
+ * place of the instance.
113
115
  *
114
- * Cost: one recursive pass over objects a call site already chose to log,
115
- * and only when `shouldLog(level)` was true. Nothing runs for the 99% of
116
- * `logger.debug` calls that are filtered out.
116
+ * Cost: one pass over the objects a call site already chose to log, each
117
+ * redacted once, and only when `shouldLog(level)` was true. Nothing runs for
118
+ * the 99% of `logger.debug` calls that are filtered out.
117
119
  */
118
- function redactLogData(data, depth = 0) {
120
+ function redactLogData(data, redacted = new Map()) {
119
121
  if (data === null || data === undefined) {
120
122
  return data;
121
123
  }
@@ -133,19 +135,47 @@ function redactLogData(data, depth = 0) {
133
135
  : undefined;
134
136
  return copy;
135
137
  }
136
- if (depth >= REDACTION_MAX_DEPTH) {
137
- return sanitizeRecord(data, REDACTION_STRING_CAP);
138
- }
139
- if (Array.isArray(data)) {
140
- return data.map((item) => redactLogData(item, depth + 1));
141
- }
142
- if (!isPlainObject(data)) {
143
- // Buffer, Date, Map, Set, class instances: carried as they are.
138
+ if (ArrayBuffer.isView(data) ||
139
+ data instanceof ArrayBuffer ||
140
+ data instanceof Date ||
141
+ data instanceof RegExp) {
144
142
  return data;
145
143
  }
144
+ if (redacted.has(data)) {
145
+ return redacted.get(data);
146
+ }
147
+ // Until this object's copy is built, a reference back to it is a cycle.
148
+ redacted.set(data, "[Circular]");
149
+ const copy = redactContainer(data, (value) => redactLogData(value, redacted));
150
+ redacted.set(data, copy);
151
+ return copy;
152
+ }
153
+ /** One level of {@link redactLogData}, for an object it has not seen yet. */
154
+ function redactContainer(data, walk) {
155
+ if (Array.isArray(data)) {
156
+ return data.map(walk);
157
+ }
158
+ if (data instanceof Map) {
159
+ return new Map(Array.from(data, ([key, value]) => [
160
+ key,
161
+ typeof key === "string" && isSensitiveLogKey(key, value)
162
+ ? "***"
163
+ : walk(value),
164
+ ]));
165
+ }
166
+ if (data instanceof Set) {
167
+ return new Set(Array.from(data, walk));
168
+ }
169
+ if (!isPlainObject(data) &&
170
+ "toJSON" in data &&
171
+ typeof data.toJSON === "function") {
172
+ const json = data.toJSON();
173
+ return walk(json);
174
+ }
175
+ const entries = Object.entries(data);
146
176
  const out = {};
147
- for (const [key, value] of Object.entries(data)) {
148
- out[key] = isSensitiveLogKey(key) ? "***" : redactLogData(value, depth + 1);
177
+ for (const [key, value] of entries) {
178
+ out[key] = isSensitiveLogKey(key, value) ? "***" : walk(value);
149
179
  }
150
180
  return out;
151
181
  }
@@ -443,18 +473,33 @@ class NeuroLinkLogger {
443
473
  * @param message - The message text to log
444
474
  * @param data - Optional additional context data to include
445
475
  */
446
- log(level, message, rawData) {
476
+ log(level, rawMessage, rawData) {
447
477
  if (!this.shouldLog(level)) {
448
478
  return;
449
479
  }
450
480
  // Central redaction: a `debug`/`info` payload is masked by key
451
- // (`apiKey`, `authorization`, `credentials`, …) and scrubbed of
452
- // bearer/known-prefix tokens before it is stored, emitted or printed, so
453
- // the next `logger.debug("…", options)` cannot leak a credential into
454
- // `getLogs()`, a `log-event` sink or the console. `warn`/`error` are
455
- // left verbatim: they carry the diagnostics an operator is debugging
456
- // with, and every credential-bearing path already redacts at source.
457
- const data = level === "debug" || level === "info" ? redactLogData(rawData) : rawData;
481
+ // (`apiKey`, `authorization`, `credentials`, …), and it and the message
482
+ // are scrubbed of bearer/known-prefix tokens and JWTs before they are
483
+ // stored, emitted or printed, so the next `logger.debug("…", options)`
484
+ // cannot leak a credential into `getLogs()`, a `log-event` sink or the
485
+ // console. `warn`/`error` are left verbatim: they carry the diagnostics
486
+ // an operator is debugging with, and every credential-bearing path
487
+ // already redacts at source.
488
+ const redact = level === "debug" || level === "info";
489
+ const message = redact
490
+ ? sanitizeForLog(rawMessage, REDACTION_STRING_CAP)
491
+ : rawMessage;
492
+ let data = rawData;
493
+ if (redact) {
494
+ try {
495
+ data = redactLogData(rawData);
496
+ }
497
+ catch {
498
+ // Too deep (a stack overflow) or a throwing getter: drop the payload
499
+ // rather than log it unredacted or throw into the caller.
500
+ data = "[unredactable log data]";
501
+ }
502
+ }
458
503
  const entry = {
459
504
  level,
460
505
  message,
@@ -649,14 +694,17 @@ const neuroLinkLogger = new NeuroLinkLogger();
649
694
  * @param args - Array of arguments passed to the logger
650
695
  * @param logMethod - Function that will perform the actual logging
651
696
  */
652
- function processLoggerArgs(args, logMethod) {
697
+ function processLoggerArgs(args, logMethod, redact = false) {
653
698
  if (args.length === 0) {
654
699
  return;
655
700
  }
656
- // Serialize the first argument robustly to handle complex objects
701
+ // Serialize the first argument robustly to handle complex objects. An
702
+ // object is masked by key first: once it is a string, keys can't be found.
657
703
  const message = (() => {
658
704
  try {
659
- return typeof args[0] === "string" ? args[0] : JSON.stringify(args[0]);
705
+ return typeof args[0] === "string"
706
+ ? args[0]
707
+ : JSON.stringify(redact ? redactLogData(args[0]) : args[0]);
660
708
  }
661
709
  catch {
662
710
  return "[Unserializable Object]";
@@ -679,12 +727,12 @@ function processLoggerArgs(args, logMethod) {
679
727
  export const logger = {
680
728
  debug: (...args) => {
681
729
  if (neuroLinkLogger.shouldLog("debug")) {
682
- processLoggerArgs(args, (message, data) => neuroLinkLogger.debug(message, data));
730
+ processLoggerArgs(args, (message, data) => neuroLinkLogger.debug(message, data), true);
683
731
  }
684
732
  },
685
733
  info: (...args) => {
686
734
  if (neuroLinkLogger.shouldLog("info")) {
687
- processLoggerArgs(args, (message, data) => neuroLinkLogger.info(message, data));
735
+ processLoggerArgs(args, (message, data) => neuroLinkLogger.info(message, data), true);
688
736
  }
689
737
  },
690
738
  warn: (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "13.8.26",
3
+ "version": "13.8.27",
4
4
  "packageManager": "pnpm@10.34.9+sha512.f49b5a8ea0fdedb44bb3c0c1d6575092a53c2950d95b7ad15df11f17974877a2de3a01c07e934e9b28ce716e51c09a94962c7306d09be2e1e4b18a8c8762fd32",
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": {