@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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +236 -236
- package/dist/utils/logSanitize.d.ts +7 -3
- package/dist/utils/logSanitize.js +27 -12
- package/dist/utils/logger.js +83 -35
- package/package.json +1 -1
|
@@ -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
|
|
20
|
-
*
|
|
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,;'\"&]+"
|
|
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
|
|
90
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
};
|
package/dist/utils/logger.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Tabular data display
|
|
15
15
|
*/
|
|
16
16
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
-
import { isSensitiveLogKey, sanitizeForLog
|
|
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 `{}`.
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
|
115
|
-
* and only when `shouldLog(level)` was true. Nothing runs for
|
|
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,
|
|
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 (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
148
|
-
out[key] = isSensitiveLogKey(key) ? "***" :
|
|
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,
|
|
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
|
|
452
|
-
// bearer/known-prefix tokens
|
|
453
|
-
// the next `logger.debug("…", options)`
|
|
454
|
-
// `getLogs()`, a `log-event` sink or the
|
|
455
|
-
// left verbatim: they carry the diagnostics
|
|
456
|
-
// with, and every credential-bearing path
|
|
457
|
-
|
|
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"
|
|
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.
|
|
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": {
|