@n8n/utils 1.46.1 → 1.48.0

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 (43) hide show
  1. package/dist/assert.cjs.map +1 -1
  2. package/dist/assert.mjs.map +1 -1
  3. package/dist/errors/get-error-message.cjs +10 -0
  4. package/dist/errors/get-error-message.cjs.map +1 -0
  5. package/dist/errors/get-error-message.d.cts +5 -0
  6. package/dist/errors/get-error-message.d.mts +5 -0
  7. package/dist/errors/get-error-message.mjs +9 -0
  8. package/dist/errors/get-error-message.mjs.map +1 -0
  9. package/dist/is-unknown-array.cjs +9 -0
  10. package/dist/is-unknown-array.cjs.map +1 -0
  11. package/dist/is-unknown-array.d.cts +5 -0
  12. package/dist/is-unknown-array.d.mts +5 -0
  13. package/dist/is-unknown-array.mjs +8 -0
  14. package/dist/is-unknown-array.mjs.map +1 -0
  15. package/dist/redaction/redact-text.cjs +3 -3
  16. package/dist/redaction/redact-text.cjs.map +1 -1
  17. package/dist/redaction/redact-text.mjs +3 -3
  18. package/dist/redaction/redact-text.mjs.map +1 -1
  19. package/dist/redaction/sanitize-error-detail.cjs +12 -0
  20. package/dist/redaction/sanitize-error-detail.cjs.map +1 -0
  21. package/dist/redaction/sanitize-error-detail.d.cts +5 -0
  22. package/dist/redaction/sanitize-error-detail.d.mts +5 -0
  23. package/dist/redaction/sanitize-error-detail.mjs +11 -0
  24. package/dist/redaction/sanitize-error-detail.mjs.map +1 -0
  25. package/dist/redaction/sensitive-headers.cjs +21 -0
  26. package/dist/redaction/sensitive-headers.cjs.map +1 -0
  27. package/dist/redaction/sensitive-headers.d.cts +5 -0
  28. package/dist/redaction/sensitive-headers.d.mts +5 -0
  29. package/dist/redaction/sensitive-headers.mjs +20 -0
  30. package/dist/redaction/sensitive-headers.mjs.map +1 -0
  31. package/dist/redaction/sensitive-key.cjs +11 -0
  32. package/dist/redaction/sensitive-key.cjs.map +1 -0
  33. package/dist/redaction/sensitive-key.d.cts +6 -0
  34. package/dist/redaction/sensitive-key.d.mts +6 -0
  35. package/dist/redaction/sensitive-key.mjs +9 -0
  36. package/dist/redaction/sensitive-key.mjs.map +1 -0
  37. package/dist/scrub-secrets.cjs +8 -5
  38. package/dist/scrub-secrets.cjs.map +1 -1
  39. package/dist/scrub-secrets.d.cts +1 -1
  40. package/dist/scrub-secrets.d.mts +1 -1
  41. package/dist/scrub-secrets.mjs +8 -5
  42. package/dist/scrub-secrets.mjs.map +1 -1
  43. package/package.json +10 -10
@@ -1 +1 @@
1
- {"version":3,"file":"assert.cjs","names":[],"sources":["../src/assert.ts"],"sourcesContent":["/**\n * Asserts given condition\n */\nexport function assert(condition: unknown, message?: string): asserts condition {\n\tif (!condition) {\n\t\t// eslint-disable-next-line n8n-local-rules/no-plain-errors\n\t\tthrow new Error(message ?? 'Assertion failed');\n\t}\n}\n"],"mappings":";;;;;AAGA,SAAgB,OAAO,WAAoB,SAAqC;CAC/E,IAAI,CAAC,WAEJ,MAAM,IAAI,MAAM,WAAW,kBAAkB;AAE/C"}
1
+ {"version":3,"file":"assert.cjs","names":[],"sources":["../src/assert.ts"],"sourcesContent":["/**\n * Asserts given condition\n */\nexport function assert(condition: unknown, message?: string): asserts condition {\n\tif (!condition) {\n\t\tthrow new Error(message ?? 'Assertion failed');\n\t}\n}\n"],"mappings":";;;;;AAGA,SAAgB,OAAO,WAAoB,SAAqC;CAC/E,IAAI,CAAC,WACJ,MAAM,IAAI,MAAM,WAAW,kBAAkB;AAE/C"}
@@ -1 +1 @@
1
- {"version":3,"file":"assert.mjs","names":[],"sources":["../src/assert.ts"],"sourcesContent":["/**\n * Asserts given condition\n */\nexport function assert(condition: unknown, message?: string): asserts condition {\n\tif (!condition) {\n\t\t// eslint-disable-next-line n8n-local-rules/no-plain-errors\n\t\tthrow new Error(message ?? 'Assertion failed');\n\t}\n}\n"],"mappings":";;;;AAGA,SAAgB,OAAO,WAAoB,SAAqC;CAC/E,IAAI,CAAC,WAEJ,MAAM,IAAI,MAAM,WAAW,kBAAkB;AAE/C"}
1
+ {"version":3,"file":"assert.mjs","names":[],"sources":["../src/assert.ts"],"sourcesContent":["/**\n * Asserts given condition\n */\nexport function assert(condition: unknown, message?: string): asserts condition {\n\tif (!condition) {\n\t\tthrow new Error(message ?? 'Assertion failed');\n\t}\n}\n"],"mappings":";;;;AAGA,SAAgB,OAAO,WAAoB,SAAqC;CAC/E,IAAI,CAAC,WACJ,MAAM,IAAI,MAAM,WAAW,kBAAkB;AAE/C"}
@@ -0,0 +1,10 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/errors/get-error-message.ts
3
+ /** Returns the message of `error`, or its string form if it is not an `Error`. */
4
+ function getErrorMessage(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ //#endregion
8
+ exports.getErrorMessage = getErrorMessage;
9
+
10
+ //# sourceMappingURL=get-error-message.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-error-message.cjs","names":[],"sources":["../../src/errors/get-error-message.ts"],"sourcesContent":["/** Returns the message of `error`, or its string form if it is not an `Error`. */\nexport function getErrorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n"],"mappings":";;;AACA,SAAgB,gBAAgB,OAAwB;CACvD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC7D"}
@@ -0,0 +1,5 @@
1
+ //#region src/errors/get-error-message.d.ts
2
+ declare function getErrorMessage(error: unknown): string;
3
+ //#endregion
4
+ export { getErrorMessage };
5
+ //# sourceMappingURL=get-error-message.d.cts.map
@@ -0,0 +1,5 @@
1
+ //#region src/errors/get-error-message.d.ts
2
+ declare function getErrorMessage(error: unknown): string;
3
+ //#endregion
4
+ export { getErrorMessage };
5
+ //# sourceMappingURL=get-error-message.d.mts.map
@@ -0,0 +1,9 @@
1
+ //#region src/errors/get-error-message.ts
2
+ /** Returns the message of `error`, or its string form if it is not an `Error`. */
3
+ function getErrorMessage(error) {
4
+ return error instanceof Error ? error.message : String(error);
5
+ }
6
+ //#endregion
7
+ export { getErrorMessage };
8
+
9
+ //# sourceMappingURL=get-error-message.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-error-message.mjs","names":[],"sources":["../../src/errors/get-error-message.ts"],"sourcesContent":["/** Returns the message of `error`, or its string form if it is not an `Error`. */\nexport function getErrorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n"],"mappings":";;AACA,SAAgB,gBAAgB,OAAwB;CACvD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC7D"}
@@ -0,0 +1,9 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/is-unknown-array.ts
3
+ function isUnknownArray(value) {
4
+ return Array.isArray(value);
5
+ }
6
+ //#endregion
7
+ exports.isUnknownArray = isUnknownArray;
8
+
9
+ //# sourceMappingURL=is-unknown-array.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-unknown-array.cjs","names":[],"sources":["../src/is-unknown-array.ts"],"sourcesContent":["// `Array.isArray` narrows an `unknown` to `any[]`; this keeps the elements\n// opaque so downstream checks have to narrow them explicitly.\nexport function isUnknownArray(value: unknown): value is readonly unknown[] {\n\treturn Array.isArray(value);\n}\n"],"mappings":";;AAEA,SAAgB,eAAe,OAA6C;CAC3E,OAAO,MAAM,QAAQ,KAAK;AAC3B"}
@@ -0,0 +1,5 @@
1
+ //#region src/is-unknown-array.d.ts
2
+ declare function isUnknownArray(value: unknown): value is readonly unknown[];
3
+ //#endregion
4
+ export { isUnknownArray };
5
+ //# sourceMappingURL=is-unknown-array.d.cts.map
@@ -0,0 +1,5 @@
1
+ //#region src/is-unknown-array.d.ts
2
+ declare function isUnknownArray(value: unknown): value is readonly unknown[];
3
+ //#endregion
4
+ export { isUnknownArray };
5
+ //# sourceMappingURL=is-unknown-array.d.mts.map
@@ -0,0 +1,8 @@
1
+ //#region src/is-unknown-array.ts
2
+ function isUnknownArray(value) {
3
+ return Array.isArray(value);
4
+ }
5
+ //#endregion
6
+ export { isUnknownArray };
7
+
8
+ //# sourceMappingURL=is-unknown-array.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-unknown-array.mjs","names":[],"sources":["../src/is-unknown-array.ts"],"sourcesContent":["// `Array.isArray` narrows an `unknown` to `any[]`; this keeps the elements\n// opaque so downstream checks have to narrow them explicitly.\nexport function isUnknownArray(value: unknown): value is readonly unknown[] {\n\treturn Array.isArray(value);\n}\n"],"mappings":";AAEA,SAAgB,eAAe,OAA6C;CAC3E,OAAO,MAAM,QAAQ,KAAK;AAC3B"}
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_redaction_pii_patterns = require("./pii-patterns.cjs");
3
+ const require_redaction_sensitive_key = require("./sensitive-key.cjs");
3
4
  //#region src/redaction/redact-text.ts
4
5
  const DEFAULT_PLACEHOLDER = "[REDACTED]";
5
6
  /**
@@ -12,7 +13,7 @@ function redactText(input, opts = {}) {
12
13
  secrets: opts.secrets ?? true,
13
14
  detect: opts.detect ?? []
14
15
  }, opts.piiPatterns);
15
- const ordered = opts.preserveUrlStructure ? [...patterns.filter((pattern) => pattern.category === "url"), ...patterns.filter((pattern) => pattern.category !== "url")] : patterns;
16
+ const ordered = [...patterns.filter((pattern) => pattern.category === "url"), ...patterns.filter((pattern) => pattern.category !== "url")];
16
17
  const matches = [];
17
18
  let text = input;
18
19
  for (const pattern of ordered) text = text.replace(pattern.regex, (match) => {
@@ -82,7 +83,6 @@ function findMatchRanges(input, opts = {}) {
82
83
  return ranges;
83
84
  }
84
85
  const MAX_DEEP_DEPTH = 8;
85
- const SENSITIVE_KEY_PATTERN = /(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[._-])token$)/i;
86
86
  /**
87
87
  * Recursively redact string values inside an arbitrary JSON-like value
88
88
  * (tool results, structured payloads). Object keys are left intact; only
@@ -93,7 +93,7 @@ function redactDeep(value, opts = {}, depth = 0) {
93
93
  return redactDeepValue(value, opts, depth);
94
94
  }
95
95
  function redactDeepValue(value, opts, depth, key) {
96
- if (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) return {
96
+ if (opts.redactSensitiveKeys && key && require_redaction_sensitive_key.isSensitiveKey(key)) return {
97
97
  value: opts.placeholder ?? "[REDACTED]",
98
98
  matches: [{ category: "secret" }]
99
99
  };
@@ -1 +1 @@
1
- {"version":3,"file":"redact-text.cjs","names":["resolvePatterns"],"sources":["../../src/redaction/redact-text.ts"],"sourcesContent":["import type { PiiDetectionType, PiiPatternTable, RedactionCategory } from './pii-patterns';\nimport { resolvePatterns } from './pii-patterns';\n\nexport const DEFAULT_PLACEHOLDER = '[REDACTED]';\n\nexport interface RedactionOptions {\n\t/** Scan for credential/secret patterns. Defaults to `true`. */\n\tsecrets?: boolean;\n\t/** PII categories to scan for. Defaults to none. */\n\tdetect?: readonly PiiDetectionType[];\n\t/** Replacement text for a match. Defaults to `[REDACTED]`. */\n\tplaceholder?: string;\n\t/** For `url` matches, keep origin + path + query names and redact the\n\t * value-bearing parts: query values, token-like path segments (webhook\n\t * secrets), userinfo, fragment. Off by default so guardrail behavior is\n\t * unchanged; telemetry/trace scrubbing opts in. */\n\tpreserveUrlStructure?: boolean;\n\t/** Replace values under secret-shaped object keys. */\n\tredactSensitiveKeys?: boolean;\n\t/**\n\t * Detection table to resolve PII categories against. Defaults to the\n\t * browser-safe {@link PII_PATTERNS}; `@n8n/agents` passes a table whose\n\t * `crypto-wallet` entry carries the Node-only Base58Check validator.\n\t */\n\tpiiPatterns?: PiiPatternTable;\n}\n\nexport interface RedactionResult {\n\t/** The input with every detected match replaced by the placeholder. */\n\ttext: string;\n\t/** One entry per replaced match (category only — never the value). */\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Redact secret/PII patterns from a complete string. Pure and idempotent —\n * already-redacted placeholders are left untouched by the underlying patterns.\n */\nexport function redactText(input: string, opts: RedactionOptions = {}): RedactionResult {\n\tconst placeholder = opts.placeholder ?? DEFAULT_PLACEHOLDER;\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\t// In preserve mode the url pass runs FIRST: it rewrites URLs with a URL-safe\n\t// placeholder before other patterns can plant one containing `]` mid-URL —\n\t// `]` stops the url regex, which would hide the URL's tail (and any secrets\n\t// in it) from this pass entirely.\n\tconst ordered = opts.preserveUrlStructure\n\t\t? [\n\t\t\t\t...patterns.filter((pattern) => pattern.category === 'url'),\n\t\t\t\t...patterns.filter((pattern) => pattern.category !== 'url'),\n\t\t\t]\n\t\t: patterns;\n\n\tconst matches: Array<{ category: RedactionCategory }> = [];\n\tlet text = input;\n\n\tfor (const pattern of ordered) {\n\t\t// `replace` with a global regex scans from 0 and resets lastIndex, so the\n\t\t// shared precompiled regex is safe to reuse across calls.\n\t\ttext = text.replace(pattern.regex, (match) => {\n\t\t\tif (pattern.validate && !pattern.validate(match)) return match;\n\t\t\tif (opts.preserveUrlStructure && pattern.category === 'url') {\n\t\t\t\tconst rebuilt = stripUrlSensitiveParts(match, placeholder);\n\t\t\t\tif (rebuilt !== match) matches.push({ category: pattern.category });\n\t\t\t\treturn rebuilt;\n\t\t\t}\n\t\t\tmatches.push({ category: pattern.category });\n\t\t\treturn placeholder;\n\t\t});\n\t}\n\n\treturn { text, matches };\n}\n\n/** True for a path segment that looks like an embedded token — webhook-style\n * services (Slack/Discord/Telegram, …) carry their secret as a path segment.\n * Shape-based on purpose: per-service URL grammars don't scale across hundreds\n * of integrations. Conservative: words, readable slugs and digit-only ids are\n * kept. */\nfunction isTokenLikeSegment(segment: string): boolean {\n\tif (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\\d/.test(segment)) return true;\n\t// Long single-class opaque blob (e.g. a letters-only token) — real words stay\n\t// shorter and readable slugs contain separators.\n\treturn segment.length >= 24 && /^[A-Za-z]+$/.test(segment);\n}\n\n/** Keep origin + path (token-like segments redacted) + query names; redact\n * query values, drop userinfo and fragment. The replacement is URL-safe (no\n * `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒\n * fully redacted. */\nfunction stripUrlSensitiveParts(match: string, placeholder: string): string {\n\tconst urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, '') || 'REDACTED';\n\ttry {\n\t\tconst url = new URL(match);\n\t\tconst pathname = url.pathname\n\t\t\t.split('/')\n\t\t\t.map((segment) => (isTokenLikeSegment(segment) ? urlPlaceholder : segment))\n\t\t\t.join('/');\n\t\tconst names = [...url.searchParams.keys()];\n\t\tconst query =\n\t\t\tnames.length > 0\n\t\t\t\t? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join('&')}`\n\t\t\t\t: '';\n\t\treturn `${url.origin}${pathname}${query}`;\n\t} catch {\n\t\treturn placeholder;\n\t}\n}\n\n/**\n * Find the `[start, end)` ranges of every (validated) match in `input`. Used by\n * the streaming redactor to avoid emitting through the middle of a complete\n * match that contains internal whitespace (e.g. a spaced credit-card number).\n */\nexport function findMatchRanges(\n\tinput: string,\n\topts: RedactionOptions = {},\n): Array<[number, number]> {\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\n\tconst ranges: Array<[number, number]> = [];\n\tfor (const pattern of patterns) {\n\t\tconst { regex } = pattern;\n\t\t// Reset before the scan loop; reusing the shared global regex is safe\n\t\t// because usage is synchronous and the loop always runs to completion.\n\t\tregex.lastIndex = 0;\n\t\tlet match: RegExpExecArray | null;\n\t\twhile ((match = regex.exec(input)) !== null) {\n\t\t\tif (match[0].length === 0) {\n\t\t\t\tregex.lastIndex++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pattern.validate && !pattern.validate(match[0])) continue;\n\t\t\tranges.push([match.index, match.index + match[0].length]);\n\t\t}\n\t}\n\treturn ranges;\n}\n\nconst MAX_DEEP_DEPTH = 8;\nconst SENSITIVE_KEY_PATTERN =\n\t/(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[._-])token$)/i;\n\nexport interface DeepRedactionResult {\n\tvalue: unknown;\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Recursively redact string values inside an arbitrary JSON-like value\n * (tool results, structured payloads). Object keys are left intact; only\n * string values are scanned. Recursion is depth-bounded as a cheap guard\n * against pathological/cyclic structures.\n */\nexport function redactDeep(\n\tvalue: unknown,\n\topts: RedactionOptions = {},\n\tdepth = 0,\n): DeepRedactionResult {\n\treturn redactDeepValue(value, opts, depth);\n}\n\nfunction redactDeepValue(\n\tvalue: unknown,\n\topts: RedactionOptions,\n\tdepth: number,\n\tkey?: string,\n): DeepRedactionResult {\n\tif (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) {\n\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t}\n\n\tif (typeof value === 'string') {\n\t\tconst { text, matches } = redactText(value, opts);\n\t\treturn { value: text, matches };\n\t}\n\n\t// Fail closed at the recursion bound: a subtree we refuse to walk is withheld\n\t// rather than passed through unscanned. Real payloads don't nest this deep,\n\t// so the only things reaching here are pathological or cyclic.\n\tif (depth >= MAX_DEEP_DEPTH) {\n\t\tif (value !== null && typeof value === 'object') {\n\t\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t\t}\n\t\treturn { value, matches: [] };\n\t}\n\n\tif (Array.isArray(value)) {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next = value.map((item) => {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\treturn result.value;\n\t\t});\n\t\treturn { value: next, matches };\n\t}\n\n\tif (value !== null && typeof value === 'object') {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next: Record<string, unknown> = {};\n\t\tfor (const [key, item] of Object.entries(value)) {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\tnext[key] = result.value;\n\t\t}\n\t\treturn { value: next, matches };\n\t}\n\n\treturn { value, matches: [] };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"redact-text.cjs","names":["resolvePatterns","isSensitiveKey"],"sources":["../../src/redaction/redact-text.ts"],"sourcesContent":["import type { PiiDetectionType, PiiPatternTable, RedactionCategory } from './pii-patterns';\nimport { resolvePatterns } from './pii-patterns';\nimport { isSensitiveKey } from './sensitive-key';\n\nexport const DEFAULT_PLACEHOLDER = '[REDACTED]';\n\nexport interface RedactionOptions {\n\t/** Scan for credential/secret patterns. Defaults to `true`. */\n\tsecrets?: boolean;\n\t/** PII categories to scan for. Defaults to none. */\n\tdetect?: readonly PiiDetectionType[];\n\t/** Replacement text for a match. Defaults to `[REDACTED]`. */\n\tplaceholder?: string;\n\t/** For `url` matches, keep origin + path + query names and redact the\n\t * value-bearing parts: query values, token-like path segments (webhook\n\t * secrets), userinfo, fragment. Off by default so guardrail behavior is\n\t * unchanged; telemetry/trace scrubbing opts in. */\n\tpreserveUrlStructure?: boolean;\n\t/** Replace values under secret-shaped object keys. */\n\tredactSensitiveKeys?: boolean;\n\t/**\n\t * Detection table to resolve PII categories against. Defaults to the\n\t * browser-safe {@link PII_PATTERNS}; `@n8n/agents` passes a table whose\n\t * `crypto-wallet` entry carries the Node-only Base58Check validator.\n\t */\n\tpiiPatterns?: PiiPatternTable;\n}\n\nexport interface RedactionResult {\n\t/** The input with every detected match replaced by the placeholder. */\n\ttext: string;\n\t/** One entry per replaced match (category only — never the value). */\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Redact secret/PII patterns from a complete string. Pure and idempotent —\n * already-redacted placeholders are left untouched by the underlying patterns.\n */\nexport function redactText(input: string, opts: RedactionOptions = {}): RedactionResult {\n\tconst placeholder = opts.placeholder ?? DEFAULT_PLACEHOLDER;\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\t// The url pass runs FIRST: it rewrites URLs (whole, or with a URL-safe\n\t// placeholder in preserve mode) before other patterns can plant one\n\t// containing `]` mid-URL — `]` stops the url regex, which would hide the\n\t// URL's tail (and any secrets in it) from this pass entirely.\n\tconst ordered = [\n\t\t...patterns.filter((pattern) => pattern.category === 'url'),\n\t\t...patterns.filter((pattern) => pattern.category !== 'url'),\n\t];\n\n\tconst matches: Array<{ category: RedactionCategory }> = [];\n\tlet text = input;\n\n\tfor (const pattern of ordered) {\n\t\t// `replace` with a global regex scans from 0 and resets lastIndex, so the\n\t\t// shared precompiled regex is safe to reuse across calls.\n\t\ttext = text.replace(pattern.regex, (match) => {\n\t\t\tif (pattern.validate && !pattern.validate(match)) return match;\n\t\t\tif (opts.preserveUrlStructure && pattern.category === 'url') {\n\t\t\t\tconst rebuilt = stripUrlSensitiveParts(match, placeholder);\n\t\t\t\tif (rebuilt !== match) matches.push({ category: pattern.category });\n\t\t\t\treturn rebuilt;\n\t\t\t}\n\t\t\tmatches.push({ category: pattern.category });\n\t\t\treturn placeholder;\n\t\t});\n\t}\n\n\treturn { text, matches };\n}\n\n/** True for a path segment that looks like an embedded token — webhook-style\n * services (Slack/Discord/Telegram, …) carry their secret as a path segment.\n * Shape-based on purpose: per-service URL grammars don't scale across hundreds\n * of integrations. Conservative: words, readable slugs and digit-only ids are\n * kept. */\nfunction isTokenLikeSegment(segment: string): boolean {\n\tif (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\\d/.test(segment)) return true;\n\t// Long single-class opaque blob (e.g. a letters-only token) — real words stay\n\t// shorter and readable slugs contain separators.\n\treturn segment.length >= 24 && /^[A-Za-z]+$/.test(segment);\n}\n\n/** Keep origin + path (token-like segments redacted) + query names; redact\n * query values, drop userinfo and fragment. The replacement is URL-safe (no\n * `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒\n * fully redacted. */\nfunction stripUrlSensitiveParts(match: string, placeholder: string): string {\n\tconst urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, '') || 'REDACTED';\n\ttry {\n\t\tconst url = new URL(match);\n\t\tconst pathname = url.pathname\n\t\t\t.split('/')\n\t\t\t.map((segment) => (isTokenLikeSegment(segment) ? urlPlaceholder : segment))\n\t\t\t.join('/');\n\t\tconst names = [...url.searchParams.keys()];\n\t\tconst query =\n\t\t\tnames.length > 0\n\t\t\t\t? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join('&')}`\n\t\t\t\t: '';\n\t\treturn `${url.origin}${pathname}${query}`;\n\t} catch {\n\t\treturn placeholder;\n\t}\n}\n\n/**\n * Find the `[start, end)` ranges of every (validated) match in `input`. Used by\n * the streaming redactor to avoid emitting through the middle of a complete\n * match that contains internal whitespace (e.g. a spaced credit-card number).\n */\nexport function findMatchRanges(\n\tinput: string,\n\topts: RedactionOptions = {},\n): Array<[number, number]> {\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\n\tconst ranges: Array<[number, number]> = [];\n\tfor (const pattern of patterns) {\n\t\tconst { regex } = pattern;\n\t\t// Reset before the scan loop; reusing the shared global regex is safe\n\t\t// because usage is synchronous and the loop always runs to completion.\n\t\tregex.lastIndex = 0;\n\t\tlet match: RegExpExecArray | null;\n\t\twhile ((match = regex.exec(input)) !== null) {\n\t\t\tif (match[0].length === 0) {\n\t\t\t\tregex.lastIndex++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pattern.validate && !pattern.validate(match[0])) continue;\n\t\t\tranges.push([match.index, match.index + match[0].length]);\n\t\t}\n\t}\n\treturn ranges;\n}\n\nconst MAX_DEEP_DEPTH = 8;\n\nexport interface DeepRedactionResult {\n\tvalue: unknown;\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Recursively redact string values inside an arbitrary JSON-like value\n * (tool results, structured payloads). Object keys are left intact; only\n * string values are scanned. Recursion is depth-bounded as a cheap guard\n * against pathological/cyclic structures.\n */\nexport function redactDeep(\n\tvalue: unknown,\n\topts: RedactionOptions = {},\n\tdepth = 0,\n): DeepRedactionResult {\n\treturn redactDeepValue(value, opts, depth);\n}\n\nfunction redactDeepValue(\n\tvalue: unknown,\n\topts: RedactionOptions,\n\tdepth: number,\n\tkey?: string,\n): DeepRedactionResult {\n\tif (opts.redactSensitiveKeys && key && isSensitiveKey(key)) {\n\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t}\n\n\tif (typeof value === 'string') {\n\t\tconst { text, matches } = redactText(value, opts);\n\t\treturn { value: text, matches };\n\t}\n\n\t// Fail closed at the recursion bound: a subtree we refuse to walk is withheld\n\t// rather than passed through unscanned. Real payloads don't nest this deep,\n\t// so the only things reaching here are pathological or cyclic.\n\tif (depth >= MAX_DEEP_DEPTH) {\n\t\tif (value !== null && typeof value === 'object') {\n\t\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t\t}\n\t\treturn { value, matches: [] };\n\t}\n\n\tif (Array.isArray(value)) {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next = value.map((item) => {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\treturn result.value;\n\t\t});\n\t\treturn { value: next, matches };\n\t}\n\n\tif (value !== null && typeof value === 'object') {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next: Record<string, unknown> = {};\n\t\tfor (const [key, item] of Object.entries(value)) {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\tnext[key] = result.value;\n\t\t}\n\t\treturn { value: next, matches };\n\t}\n\n\treturn { value, matches: [] };\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { resolvePatterns } from "./pii-patterns.mjs";
2
+ import { isSensitiveKey } from "./sensitive-key.mjs";
2
3
  //#region src/redaction/redact-text.ts
3
4
  const DEFAULT_PLACEHOLDER = "[REDACTED]";
4
5
  /**
@@ -11,7 +12,7 @@ function redactText(input, opts = {}) {
11
12
  secrets: opts.secrets ?? true,
12
13
  detect: opts.detect ?? []
13
14
  }, opts.piiPatterns);
14
- const ordered = opts.preserveUrlStructure ? [...patterns.filter((pattern) => pattern.category === "url"), ...patterns.filter((pattern) => pattern.category !== "url")] : patterns;
15
+ const ordered = [...patterns.filter((pattern) => pattern.category === "url"), ...patterns.filter((pattern) => pattern.category !== "url")];
15
16
  const matches = [];
16
17
  let text = input;
17
18
  for (const pattern of ordered) text = text.replace(pattern.regex, (match) => {
@@ -81,7 +82,6 @@ function findMatchRanges(input, opts = {}) {
81
82
  return ranges;
82
83
  }
83
84
  const MAX_DEEP_DEPTH = 8;
84
- const SENSITIVE_KEY_PATTERN = /(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[._-])token$)/i;
85
85
  /**
86
86
  * Recursively redact string values inside an arbitrary JSON-like value
87
87
  * (tool results, structured payloads). Object keys are left intact; only
@@ -92,7 +92,7 @@ function redactDeep(value, opts = {}, depth = 0) {
92
92
  return redactDeepValue(value, opts, depth);
93
93
  }
94
94
  function redactDeepValue(value, opts, depth, key) {
95
- if (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) return {
95
+ if (opts.redactSensitiveKeys && key && isSensitiveKey(key)) return {
96
96
  value: opts.placeholder ?? "[REDACTED]",
97
97
  matches: [{ category: "secret" }]
98
98
  };
@@ -1 +1 @@
1
- {"version":3,"file":"redact-text.mjs","names":[],"sources":["../../src/redaction/redact-text.ts"],"sourcesContent":["import type { PiiDetectionType, PiiPatternTable, RedactionCategory } from './pii-patterns';\nimport { resolvePatterns } from './pii-patterns';\n\nexport const DEFAULT_PLACEHOLDER = '[REDACTED]';\n\nexport interface RedactionOptions {\n\t/** Scan for credential/secret patterns. Defaults to `true`. */\n\tsecrets?: boolean;\n\t/** PII categories to scan for. Defaults to none. */\n\tdetect?: readonly PiiDetectionType[];\n\t/** Replacement text for a match. Defaults to `[REDACTED]`. */\n\tplaceholder?: string;\n\t/** For `url` matches, keep origin + path + query names and redact the\n\t * value-bearing parts: query values, token-like path segments (webhook\n\t * secrets), userinfo, fragment. Off by default so guardrail behavior is\n\t * unchanged; telemetry/trace scrubbing opts in. */\n\tpreserveUrlStructure?: boolean;\n\t/** Replace values under secret-shaped object keys. */\n\tredactSensitiveKeys?: boolean;\n\t/**\n\t * Detection table to resolve PII categories against. Defaults to the\n\t * browser-safe {@link PII_PATTERNS}; `@n8n/agents` passes a table whose\n\t * `crypto-wallet` entry carries the Node-only Base58Check validator.\n\t */\n\tpiiPatterns?: PiiPatternTable;\n}\n\nexport interface RedactionResult {\n\t/** The input with every detected match replaced by the placeholder. */\n\ttext: string;\n\t/** One entry per replaced match (category only — never the value). */\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Redact secret/PII patterns from a complete string. Pure and idempotent —\n * already-redacted placeholders are left untouched by the underlying patterns.\n */\nexport function redactText(input: string, opts: RedactionOptions = {}): RedactionResult {\n\tconst placeholder = opts.placeholder ?? DEFAULT_PLACEHOLDER;\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\t// In preserve mode the url pass runs FIRST: it rewrites URLs with a URL-safe\n\t// placeholder before other patterns can plant one containing `]` mid-URL —\n\t// `]` stops the url regex, which would hide the URL's tail (and any secrets\n\t// in it) from this pass entirely.\n\tconst ordered = opts.preserveUrlStructure\n\t\t? [\n\t\t\t\t...patterns.filter((pattern) => pattern.category === 'url'),\n\t\t\t\t...patterns.filter((pattern) => pattern.category !== 'url'),\n\t\t\t]\n\t\t: patterns;\n\n\tconst matches: Array<{ category: RedactionCategory }> = [];\n\tlet text = input;\n\n\tfor (const pattern of ordered) {\n\t\t// `replace` with a global regex scans from 0 and resets lastIndex, so the\n\t\t// shared precompiled regex is safe to reuse across calls.\n\t\ttext = text.replace(pattern.regex, (match) => {\n\t\t\tif (pattern.validate && !pattern.validate(match)) return match;\n\t\t\tif (opts.preserveUrlStructure && pattern.category === 'url') {\n\t\t\t\tconst rebuilt = stripUrlSensitiveParts(match, placeholder);\n\t\t\t\tif (rebuilt !== match) matches.push({ category: pattern.category });\n\t\t\t\treturn rebuilt;\n\t\t\t}\n\t\t\tmatches.push({ category: pattern.category });\n\t\t\treturn placeholder;\n\t\t});\n\t}\n\n\treturn { text, matches };\n}\n\n/** True for a path segment that looks like an embedded token — webhook-style\n * services (Slack/Discord/Telegram, …) carry their secret as a path segment.\n * Shape-based on purpose: per-service URL grammars don't scale across hundreds\n * of integrations. Conservative: words, readable slugs and digit-only ids are\n * kept. */\nfunction isTokenLikeSegment(segment: string): boolean {\n\tif (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\\d/.test(segment)) return true;\n\t// Long single-class opaque blob (e.g. a letters-only token) — real words stay\n\t// shorter and readable slugs contain separators.\n\treturn segment.length >= 24 && /^[A-Za-z]+$/.test(segment);\n}\n\n/** Keep origin + path (token-like segments redacted) + query names; redact\n * query values, drop userinfo and fragment. The replacement is URL-safe (no\n * `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒\n * fully redacted. */\nfunction stripUrlSensitiveParts(match: string, placeholder: string): string {\n\tconst urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, '') || 'REDACTED';\n\ttry {\n\t\tconst url = new URL(match);\n\t\tconst pathname = url.pathname\n\t\t\t.split('/')\n\t\t\t.map((segment) => (isTokenLikeSegment(segment) ? urlPlaceholder : segment))\n\t\t\t.join('/');\n\t\tconst names = [...url.searchParams.keys()];\n\t\tconst query =\n\t\t\tnames.length > 0\n\t\t\t\t? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join('&')}`\n\t\t\t\t: '';\n\t\treturn `${url.origin}${pathname}${query}`;\n\t} catch {\n\t\treturn placeholder;\n\t}\n}\n\n/**\n * Find the `[start, end)` ranges of every (validated) match in `input`. Used by\n * the streaming redactor to avoid emitting through the middle of a complete\n * match that contains internal whitespace (e.g. a spaced credit-card number).\n */\nexport function findMatchRanges(\n\tinput: string,\n\topts: RedactionOptions = {},\n): Array<[number, number]> {\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\n\tconst ranges: Array<[number, number]> = [];\n\tfor (const pattern of patterns) {\n\t\tconst { regex } = pattern;\n\t\t// Reset before the scan loop; reusing the shared global regex is safe\n\t\t// because usage is synchronous and the loop always runs to completion.\n\t\tregex.lastIndex = 0;\n\t\tlet match: RegExpExecArray | null;\n\t\twhile ((match = regex.exec(input)) !== null) {\n\t\t\tif (match[0].length === 0) {\n\t\t\t\tregex.lastIndex++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pattern.validate && !pattern.validate(match[0])) continue;\n\t\t\tranges.push([match.index, match.index + match[0].length]);\n\t\t}\n\t}\n\treturn ranges;\n}\n\nconst MAX_DEEP_DEPTH = 8;\nconst SENSITIVE_KEY_PATTERN =\n\t/(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[._-])token$)/i;\n\nexport interface DeepRedactionResult {\n\tvalue: unknown;\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Recursively redact string values inside an arbitrary JSON-like value\n * (tool results, structured payloads). Object keys are left intact; only\n * string values are scanned. Recursion is depth-bounded as a cheap guard\n * against pathological/cyclic structures.\n */\nexport function redactDeep(\n\tvalue: unknown,\n\topts: RedactionOptions = {},\n\tdepth = 0,\n): DeepRedactionResult {\n\treturn redactDeepValue(value, opts, depth);\n}\n\nfunction redactDeepValue(\n\tvalue: unknown,\n\topts: RedactionOptions,\n\tdepth: number,\n\tkey?: string,\n): DeepRedactionResult {\n\tif (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) {\n\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t}\n\n\tif (typeof value === 'string') {\n\t\tconst { text, matches } = redactText(value, opts);\n\t\treturn { value: text, matches };\n\t}\n\n\t// Fail closed at the recursion bound: a subtree we refuse to walk is withheld\n\t// rather than passed through unscanned. Real payloads don't nest this deep,\n\t// so the only things reaching here are pathological or cyclic.\n\tif (depth >= MAX_DEEP_DEPTH) {\n\t\tif (value !== null && typeof value === 'object') {\n\t\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t\t}\n\t\treturn { value, matches: [] };\n\t}\n\n\tif (Array.isArray(value)) {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next = value.map((item) => {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\treturn result.value;\n\t\t});\n\t\treturn { value: next, matches };\n\t}\n\n\tif (value !== null && typeof value === 'object') {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next: Record<string, unknown> = {};\n\t\tfor (const [key, item] of Object.entries(value)) {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\tnext[key] = result.value;\n\t\t}\n\t\treturn { value: next, matches };\n\t}\n\n\treturn { value, matches: [] };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"redact-text.mjs","names":[],"sources":["../../src/redaction/redact-text.ts"],"sourcesContent":["import type { PiiDetectionType, PiiPatternTable, RedactionCategory } from './pii-patterns';\nimport { resolvePatterns } from './pii-patterns';\nimport { isSensitiveKey } from './sensitive-key';\n\nexport const DEFAULT_PLACEHOLDER = '[REDACTED]';\n\nexport interface RedactionOptions {\n\t/** Scan for credential/secret patterns. Defaults to `true`. */\n\tsecrets?: boolean;\n\t/** PII categories to scan for. Defaults to none. */\n\tdetect?: readonly PiiDetectionType[];\n\t/** Replacement text for a match. Defaults to `[REDACTED]`. */\n\tplaceholder?: string;\n\t/** For `url` matches, keep origin + path + query names and redact the\n\t * value-bearing parts: query values, token-like path segments (webhook\n\t * secrets), userinfo, fragment. Off by default so guardrail behavior is\n\t * unchanged; telemetry/trace scrubbing opts in. */\n\tpreserveUrlStructure?: boolean;\n\t/** Replace values under secret-shaped object keys. */\n\tredactSensitiveKeys?: boolean;\n\t/**\n\t * Detection table to resolve PII categories against. Defaults to the\n\t * browser-safe {@link PII_PATTERNS}; `@n8n/agents` passes a table whose\n\t * `crypto-wallet` entry carries the Node-only Base58Check validator.\n\t */\n\tpiiPatterns?: PiiPatternTable;\n}\n\nexport interface RedactionResult {\n\t/** The input with every detected match replaced by the placeholder. */\n\ttext: string;\n\t/** One entry per replaced match (category only — never the value). */\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Redact secret/PII patterns from a complete string. Pure and idempotent —\n * already-redacted placeholders are left untouched by the underlying patterns.\n */\nexport function redactText(input: string, opts: RedactionOptions = {}): RedactionResult {\n\tconst placeholder = opts.placeholder ?? DEFAULT_PLACEHOLDER;\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\t// The url pass runs FIRST: it rewrites URLs (whole, or with a URL-safe\n\t// placeholder in preserve mode) before other patterns can plant one\n\t// containing `]` mid-URL — `]` stops the url regex, which would hide the\n\t// URL's tail (and any secrets in it) from this pass entirely.\n\tconst ordered = [\n\t\t...patterns.filter((pattern) => pattern.category === 'url'),\n\t\t...patterns.filter((pattern) => pattern.category !== 'url'),\n\t];\n\n\tconst matches: Array<{ category: RedactionCategory }> = [];\n\tlet text = input;\n\n\tfor (const pattern of ordered) {\n\t\t// `replace` with a global regex scans from 0 and resets lastIndex, so the\n\t\t// shared precompiled regex is safe to reuse across calls.\n\t\ttext = text.replace(pattern.regex, (match) => {\n\t\t\tif (pattern.validate && !pattern.validate(match)) return match;\n\t\t\tif (opts.preserveUrlStructure && pattern.category === 'url') {\n\t\t\t\tconst rebuilt = stripUrlSensitiveParts(match, placeholder);\n\t\t\t\tif (rebuilt !== match) matches.push({ category: pattern.category });\n\t\t\t\treturn rebuilt;\n\t\t\t}\n\t\t\tmatches.push({ category: pattern.category });\n\t\t\treturn placeholder;\n\t\t});\n\t}\n\n\treturn { text, matches };\n}\n\n/** True for a path segment that looks like an embedded token — webhook-style\n * services (Slack/Discord/Telegram, …) carry their secret as a path segment.\n * Shape-based on purpose: per-service URL grammars don't scale across hundreds\n * of integrations. Conservative: words, readable slugs and digit-only ids are\n * kept. */\nfunction isTokenLikeSegment(segment: string): boolean {\n\tif (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\\d/.test(segment)) return true;\n\t// Long single-class opaque blob (e.g. a letters-only token) — real words stay\n\t// shorter and readable slugs contain separators.\n\treturn segment.length >= 24 && /^[A-Za-z]+$/.test(segment);\n}\n\n/** Keep origin + path (token-like segments redacted) + query names; redact\n * query values, drop userinfo and fragment. The replacement is URL-safe (no\n * `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒\n * fully redacted. */\nfunction stripUrlSensitiveParts(match: string, placeholder: string): string {\n\tconst urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, '') || 'REDACTED';\n\ttry {\n\t\tconst url = new URL(match);\n\t\tconst pathname = url.pathname\n\t\t\t.split('/')\n\t\t\t.map((segment) => (isTokenLikeSegment(segment) ? urlPlaceholder : segment))\n\t\t\t.join('/');\n\t\tconst names = [...url.searchParams.keys()];\n\t\tconst query =\n\t\t\tnames.length > 0\n\t\t\t\t? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join('&')}`\n\t\t\t\t: '';\n\t\treturn `${url.origin}${pathname}${query}`;\n\t} catch {\n\t\treturn placeholder;\n\t}\n}\n\n/**\n * Find the `[start, end)` ranges of every (validated) match in `input`. Used by\n * the streaming redactor to avoid emitting through the middle of a complete\n * match that contains internal whitespace (e.g. a spaced credit-card number).\n */\nexport function findMatchRanges(\n\tinput: string,\n\topts: RedactionOptions = {},\n): Array<[number, number]> {\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\n\tconst ranges: Array<[number, number]> = [];\n\tfor (const pattern of patterns) {\n\t\tconst { regex } = pattern;\n\t\t// Reset before the scan loop; reusing the shared global regex is safe\n\t\t// because usage is synchronous and the loop always runs to completion.\n\t\tregex.lastIndex = 0;\n\t\tlet match: RegExpExecArray | null;\n\t\twhile ((match = regex.exec(input)) !== null) {\n\t\t\tif (match[0].length === 0) {\n\t\t\t\tregex.lastIndex++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pattern.validate && !pattern.validate(match[0])) continue;\n\t\t\tranges.push([match.index, match.index + match[0].length]);\n\t\t}\n\t}\n\treturn ranges;\n}\n\nconst MAX_DEEP_DEPTH = 8;\n\nexport interface DeepRedactionResult {\n\tvalue: unknown;\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Recursively redact string values inside an arbitrary JSON-like value\n * (tool results, structured payloads). Object keys are left intact; only\n * string values are scanned. Recursion is depth-bounded as a cheap guard\n * against pathological/cyclic structures.\n */\nexport function redactDeep(\n\tvalue: unknown,\n\topts: RedactionOptions = {},\n\tdepth = 0,\n): DeepRedactionResult {\n\treturn redactDeepValue(value, opts, depth);\n}\n\nfunction redactDeepValue(\n\tvalue: unknown,\n\topts: RedactionOptions,\n\tdepth: number,\n\tkey?: string,\n): DeepRedactionResult {\n\tif (opts.redactSensitiveKeys && key && isSensitiveKey(key)) {\n\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t}\n\n\tif (typeof value === 'string') {\n\t\tconst { text, matches } = redactText(value, opts);\n\t\treturn { value: text, matches };\n\t}\n\n\t// Fail closed at the recursion bound: a subtree we refuse to walk is withheld\n\t// rather than passed through unscanned. Real payloads don't nest this deep,\n\t// so the only things reaching here are pathological or cyclic.\n\tif (depth >= MAX_DEEP_DEPTH) {\n\t\tif (value !== null && typeof value === 'object') {\n\t\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t\t}\n\t\treturn { value, matches: [] };\n\t}\n\n\tif (Array.isArray(value)) {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next = value.map((item) => {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\treturn result.value;\n\t\t});\n\t\treturn { value: next, matches };\n\t}\n\n\tif (value !== null && typeof value === 'object') {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next: Record<string, unknown> = {};\n\t\tfor (const [key, item] of Object.entries(value)) {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\tnext[key] = result.value;\n\t\t}\n\t\treturn { value: next, matches };\n\t}\n\n\treturn { value, matches: [] };\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_scrub_secrets = require("../scrub-secrets.cjs");
3
+ //#region src/redaction/sanitize-error-detail.ts
4
+ const QUOTED_URL_QUERY = /(["'])(https?:\/\/(?:(?!\1)[^\s?<>])+)(?:\?(?:\\.|(?!\1(?!\w))[^\s<>\\])*)?/gi;
5
+ const BARE_URL_QUERY = /(https?:\/\/[^\s?<>]+)(?:\?[^\s<>]*)?/gi;
6
+ function sanitizeErrorDetail(message, maxLength) {
7
+ return require_scrub_secrets.scrubSecretsInText(message).replace(QUOTED_URL_QUERY, "$1$2").replace(BARE_URL_QUERY, "$1").slice(0, maxLength);
8
+ }
9
+ //#endregion
10
+ exports.sanitizeErrorDetail = sanitizeErrorDetail;
11
+
12
+ //# sourceMappingURL=sanitize-error-detail.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-error-detail.cjs","names":["scrubSecretsInText"],"sources":["../../src/redaction/sanitize-error-detail.ts"],"sourcesContent":["import { scrubSecretsInText } from '../scrub-secrets';\n\n// A delimiter quote ends a quoted URL. An escaped quote stays inside the query.\n// An apostrophe inside a word also stays inside the query. A bare URL ends at\n// whitespace or an angle bracket. The optional query group matches URLs with and\n// without queries in one pass.\nconst QUOTED_URL_QUERY =\n\t/([\"'])(https?:\\/\\/(?:(?!\\1)[^\\s?<>])+)(?:\\?(?:\\\\.|(?!\\1(?!\\w))[^\\s<>\\\\])*)?/gi;\nconst BARE_URL_QUERY = /(https?:\\/\\/[^\\s?<>]+)(?:\\?[^\\s<>]*)?/gi;\n\nexport function sanitizeErrorDetail(message: string, maxLength: number): string {\n\treturn scrubSecretsInText(message)\n\t\t.replace(QUOTED_URL_QUERY, '$1$2')\n\t\t.replace(BARE_URL_QUERY, '$1')\n\t\t.slice(0, maxLength);\n}\n"],"mappings":";;;AAMA,MAAM,mBACL;AACD,MAAM,iBAAiB;AAEvB,SAAgB,oBAAoB,SAAiB,WAA2B;CAC/E,OAAOA,sBAAAA,mBAAmB,OAAO,CAAC,CAChC,QAAQ,kBAAkB,MAAM,CAAC,CACjC,QAAQ,gBAAgB,IAAI,CAAC,CAC7B,MAAM,GAAG,SAAS;AACrB"}
@@ -0,0 +1,5 @@
1
+ //#region src/redaction/sanitize-error-detail.d.ts
2
+ declare function sanitizeErrorDetail(message: string, maxLength: number): string;
3
+ //#endregion
4
+ export { sanitizeErrorDetail };
5
+ //# sourceMappingURL=sanitize-error-detail.d.cts.map
@@ -0,0 +1,5 @@
1
+ //#region src/redaction/sanitize-error-detail.d.ts
2
+ declare function sanitizeErrorDetail(message: string, maxLength: number): string;
3
+ //#endregion
4
+ export { sanitizeErrorDetail };
5
+ //# sourceMappingURL=sanitize-error-detail.d.mts.map
@@ -0,0 +1,11 @@
1
+ import { scrubSecretsInText } from "../scrub-secrets.mjs";
2
+ //#region src/redaction/sanitize-error-detail.ts
3
+ const QUOTED_URL_QUERY = /(["'])(https?:\/\/(?:(?!\1)[^\s?<>])+)(?:\?(?:\\.|(?!\1(?!\w))[^\s<>\\])*)?/gi;
4
+ const BARE_URL_QUERY = /(https?:\/\/[^\s?<>]+)(?:\?[^\s<>]*)?/gi;
5
+ function sanitizeErrorDetail(message, maxLength) {
6
+ return scrubSecretsInText(message).replace(QUOTED_URL_QUERY, "$1$2").replace(BARE_URL_QUERY, "$1").slice(0, maxLength);
7
+ }
8
+ //#endregion
9
+ export { sanitizeErrorDetail };
10
+
11
+ //# sourceMappingURL=sanitize-error-detail.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-error-detail.mjs","names":[],"sources":["../../src/redaction/sanitize-error-detail.ts"],"sourcesContent":["import { scrubSecretsInText } from '../scrub-secrets';\n\n// A delimiter quote ends a quoted URL. An escaped quote stays inside the query.\n// An apostrophe inside a word also stays inside the query. A bare URL ends at\n// whitespace or an angle bracket. The optional query group matches URLs with and\n// without queries in one pass.\nconst QUOTED_URL_QUERY =\n\t/([\"'])(https?:\\/\\/(?:(?!\\1)[^\\s?<>])+)(?:\\?(?:\\\\.|(?!\\1(?!\\w))[^\\s<>\\\\])*)?/gi;\nconst BARE_URL_QUERY = /(https?:\\/\\/[^\\s?<>]+)(?:\\?[^\\s<>]*)?/gi;\n\nexport function sanitizeErrorDetail(message: string, maxLength: number): string {\n\treturn scrubSecretsInText(message)\n\t\t.replace(QUOTED_URL_QUERY, '$1$2')\n\t\t.replace(BARE_URL_QUERY, '$1')\n\t\t.slice(0, maxLength);\n}\n"],"mappings":";;AAMA,MAAM,mBACL;AACD,MAAM,iBAAiB;AAEvB,SAAgB,oBAAoB,SAAiB,WAA2B;CAC/E,OAAO,mBAAmB,OAAO,CAAC,CAChC,QAAQ,kBAAkB,MAAM,CAAC,CACjC,QAAQ,gBAAgB,IAAI,CAAC,CAC7B,MAAM,GAAG,SAAS;AACrB"}
@@ -0,0 +1,21 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/redaction/sensitive-headers.ts
3
+ const ALWAYS_SENSITIVE_HEADERS = [
4
+ "authorization",
5
+ "proxy-authorization",
6
+ "x-authorization",
7
+ "cookie",
8
+ "set-cookie",
9
+ "x-api-key",
10
+ "api-key",
11
+ "apikey",
12
+ "x-auth-token",
13
+ "x-access-token",
14
+ "x-refresh-token",
15
+ "x-csrf-token",
16
+ "x-xsrf-token"
17
+ ];
18
+ //#endregion
19
+ exports.ALWAYS_SENSITIVE_HEADERS = ALWAYS_SENSITIVE_HEADERS;
20
+
21
+ //# sourceMappingURL=sensitive-headers.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitive-headers.cjs","names":[],"sources":["../../src/redaction/sensitive-headers.ts"],"sourcesContent":["export const ALWAYS_SENSITIVE_HEADERS: readonly string[] = [\n\t'authorization',\n\t'proxy-authorization',\n\t'x-authorization',\n\t'cookie',\n\t'set-cookie',\n\t'x-api-key',\n\t'api-key',\n\t'apikey',\n\t'x-auth-token',\n\t'x-access-token',\n\t'x-refresh-token',\n\t'x-csrf-token',\n\t'x-xsrf-token',\n];\n"],"mappings":";;AAAA,MAAa,2BAA8C;CAC1D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD"}
@@ -0,0 +1,5 @@
1
+ //#region src/redaction/sensitive-headers.d.ts
2
+ declare const ALWAYS_SENSITIVE_HEADERS: readonly string[];
3
+ //#endregion
4
+ export { ALWAYS_SENSITIVE_HEADERS };
5
+ //# sourceMappingURL=sensitive-headers.d.cts.map
@@ -0,0 +1,5 @@
1
+ //#region src/redaction/sensitive-headers.d.ts
2
+ declare const ALWAYS_SENSITIVE_HEADERS: readonly string[];
3
+ //#endregion
4
+ export { ALWAYS_SENSITIVE_HEADERS };
5
+ //# sourceMappingURL=sensitive-headers.d.mts.map
@@ -0,0 +1,20 @@
1
+ //#region src/redaction/sensitive-headers.ts
2
+ const ALWAYS_SENSITIVE_HEADERS = [
3
+ "authorization",
4
+ "proxy-authorization",
5
+ "x-authorization",
6
+ "cookie",
7
+ "set-cookie",
8
+ "x-api-key",
9
+ "api-key",
10
+ "apikey",
11
+ "x-auth-token",
12
+ "x-access-token",
13
+ "x-refresh-token",
14
+ "x-csrf-token",
15
+ "x-xsrf-token"
16
+ ];
17
+ //#endregion
18
+ export { ALWAYS_SENSITIVE_HEADERS };
19
+
20
+ //# sourceMappingURL=sensitive-headers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitive-headers.mjs","names":[],"sources":["../../src/redaction/sensitive-headers.ts"],"sourcesContent":["export const ALWAYS_SENSITIVE_HEADERS: readonly string[] = [\n\t'authorization',\n\t'proxy-authorization',\n\t'x-authorization',\n\t'cookie',\n\t'set-cookie',\n\t'x-api-key',\n\t'api-key',\n\t'apikey',\n\t'x-auth-token',\n\t'x-access-token',\n\t'x-refresh-token',\n\t'x-csrf-token',\n\t'x-xsrf-token',\n];\n"],"mappings":";AAAA,MAAa,2BAA8C;CAC1D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD"}
@@ -0,0 +1,11 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/redaction/sensitive-key.ts
3
+ const SENSITIVE_KEY_PATTERN = /(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|passwd|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[^a-z0-9])token$)/i;
4
+ function isSensitiveKey(key) {
5
+ return SENSITIVE_KEY_PATTERN.test(key);
6
+ }
7
+ //#endregion
8
+ exports.SENSITIVE_KEY_PATTERN = SENSITIVE_KEY_PATTERN;
9
+ exports.isSensitiveKey = isSensitiveKey;
10
+
11
+ //# sourceMappingURL=sensitive-key.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitive-key.cjs","names":[],"sources":["../../src/redaction/sensitive-key.ts"],"sourcesContent":["// A bare `token` counts as a suffix after any separator (`bot_token`, `x.token`,\n// `auth:token`). It does not count as a prefix (`token_type`, `token_count`) or\n// inside a word (`mytoken`).\nexport const SENSITIVE_KEY_PATTERN =\n\t/(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|passwd|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[^a-z0-9])token$)/i;\n\nexport function isSensitiveKey(key: string): boolean {\n\treturn SENSITIVE_KEY_PATTERN.test(key);\n}\n"],"mappings":";;AAGA,MAAa,wBACZ;AAED,SAAgB,eAAe,KAAsB;CACpD,OAAO,sBAAsB,KAAK,GAAG;AACtC"}
@@ -0,0 +1,6 @@
1
+ //#region src/redaction/sensitive-key.d.ts
2
+ declare const SENSITIVE_KEY_PATTERN: RegExp;
3
+ declare function isSensitiveKey(key: string): boolean;
4
+ //#endregion
5
+ export { SENSITIVE_KEY_PATTERN, isSensitiveKey };
6
+ //# sourceMappingURL=sensitive-key.d.cts.map
@@ -0,0 +1,6 @@
1
+ //#region src/redaction/sensitive-key.d.ts
2
+ declare const SENSITIVE_KEY_PATTERN: RegExp;
3
+ declare function isSensitiveKey(key: string): boolean;
4
+ //#endregion
5
+ export { SENSITIVE_KEY_PATTERN, isSensitiveKey };
6
+ //# sourceMappingURL=sensitive-key.d.mts.map
@@ -0,0 +1,9 @@
1
+ //#region src/redaction/sensitive-key.ts
2
+ const SENSITIVE_KEY_PATTERN = /(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|passwd|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[^a-z0-9])token$)/i;
3
+ function isSensitiveKey(key) {
4
+ return SENSITIVE_KEY_PATTERN.test(key);
5
+ }
6
+ //#endregion
7
+ export { SENSITIVE_KEY_PATTERN, isSensitiveKey };
8
+
9
+ //# sourceMappingURL=sensitive-key.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitive-key.mjs","names":[],"sources":["../../src/redaction/sensitive-key.ts"],"sourcesContent":["// A bare `token` counts as a suffix after any separator (`bot_token`, `x.token`,\n// `auth:token`). It does not count as a prefix (`token_type`, `token_count`) or\n// inside a word (`mytoken`).\nexport const SENSITIVE_KEY_PATTERN =\n\t/(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|passwd|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[^a-z0-9])token$)/i;\n\nexport function isSensitiveKey(key: string): boolean {\n\treturn SENSITIVE_KEY_PATTERN.test(key);\n}\n"],"mappings":";AAGA,MAAa,wBACZ;AAED,SAAgB,eAAe,KAAsB;CACpD,OAAO,sBAAsB,KAAK,GAAG;AACtC"}
@@ -14,14 +14,17 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
14
  * positives on file paths, IDs, or base64 payloads would make the output
15
15
  * unreadable.
16
16
  */
17
- const SECRET_KEYS = "password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token";
17
+ const SECRET_KEYS = "password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|session[_-]?cookie|token";
18
+ const QUOTED_KEY_PREFIX = "(?:[\\w.-]*[._-])?";
18
19
  const SECRET_VALUE_PATTERNS = [
19
20
  /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,
20
21
  /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
21
- /\b(?:Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{12,}/gi,
22
+ /(?<=\bauthorization\s*[:=]\s*["']?)(?:Bearer|Basic|Token)\s+(?:\\.|[^\s"',;\\])+/gi,
23
+ /\b(?:Bearer\s+[^\s"',;]{12,}|(?:Basic|Token)\s+[A-Za-z0-9._~+/=-]{12,})/gi,
22
24
  /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,
23
25
  /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,
24
26
  /\bAIza[0-9A-Za-z_-]{35}\b/g,
27
+ /\bya29\.[0-9A-Za-z_-]{20,}\b/g,
25
28
  /\bxox[abprso]-[A-Za-z0-9-]{10,}/g,
26
29
  /\bgh[psoru]_[A-Za-z0-9]{20,}/g,
27
30
  /\bgithub_pat_[A-Za-z0-9_]{22,}/g,
@@ -30,9 +33,9 @@ const SECRET_VALUE_PATTERNS = [
30
33
  /\b[MNO][A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/g,
31
34
  /\blin_(?:api|oauth)_[A-Za-z0-9]{20,}/g,
32
35
  /(?<=:\/\/)[^\s:/@]+:[^\s:/@]+(?=@)/g,
33
- new RegExp(`"(?:${SECRET_KEYS})"\\s*:\\s*"(?!\\[(?:redacted|REDACTED)(?::[^"\\]]*)?\\]")(?:[^"\\\\\\r\\n]|\\\\.)*"`, "gi"),
34
- new RegExp(`'(?:${SECRET_KEYS})'\\s*:\\s*'(?!\\[(?:redacted|REDACTED)(?::[^'\\]]*)?\\]')(?:[^'\\\\\\r\\n]|\\\\.)*'`, "gi"),
35
- new RegExp(`(?<!\\[(?:redacted|REDACTED):)\\b(?:${SECRET_KEYS})\\s*[:=]\\s*(?!\\[?(?:redacted|REDACTED)\\b)\\S+`, "gi")
36
+ new RegExp(`(["'])${QUOTED_KEY_PREFIX}(?:${SECRET_KEYS})\\1\\s*:\\s*(["'])(?!\\[(?:redacted|REDACTED)(?::[^\\]]*)?\\]\\2)(?:(?!\\2)[^\\\\\\r\\n]|\\\\.)*\\2`, "gi"),
37
+ new RegExp(`\\\\(["'])${QUOTED_KEY_PREFIX}(?:${SECRET_KEYS})\\\\\\1\\s*:\\s*\\\\(["'])(?!\\[(?:redacted|REDACTED)(?::[^\\]]*)?\\]\\\\\\2)(?:(?!\\2)[^\\\\\\r\\n]|\\\\(?!\\2)[^\\\\\\r\\n]|\\\\\\\\(?:\\\\.|[^\\\\\\r\\n]))*\\\\\\2`, "gi"),
38
+ new RegExp(`(?<!\\[(?:redacted|REDACTED):)\\b(?:[\\w.-]{0,128}[._-])?(?:${SECRET_KEYS})\\s*[:=]\\s*(?!\\[?(?:redacted|REDACTED)\\b)(?:"(?:[^"\\\\\\r\\n]|\\\\.)*"|'(?:[^'\\\\\\r\\n]|\\\\.)*'|\\S+)`, "gi")
36
39
  ];
37
40
  function scrubSecretsInText(input) {
38
41
  let out = input;
@@ -1 +1 @@
1
- {"version":3,"file":"scrub-secrets.cjs","names":[],"sources":["../src/scrub-secrets.ts"],"sourcesContent":["/**\n * Replace common credential patterns in free-form text with `[REDACTED]`.\n *\n * Used before persisting or transmitting user-supplied text (telemetry\n * excerpts, eval report HTML, free-form feedback) where keys/tokens\n * accidentally pasted into prompts or command lines could otherwise leak\n * downstream.\n *\n * Conservative by design: matches well-known prefixed tokens, explicit\n * `key=value` pairs, and quoted JSON/JS-object fields with sensitive\n * names. We don't attempt to redact arbitrary long opaque strings — false\n * positives on file paths, IDs, or base64 payloads would make the output\n * unreadable.\n */\nexport const SECRET_KEYS =\n\t'password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token';\n\nexport const SECRET_VALUE_PATTERNS: readonly RegExp[] = [\n\t// PEM private-key blocks (RSA/EC/DSA/OpenSSH/PGP). Whole block, multiline.\n\t/-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,\n\t// JWTs: `eyJ<header>.eyJ<payload>.<signature>` (both leading segments are\n\t// base64url of a `{\"` object, which makes this highly distinctive).\n\t/\\beyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g,\n\t// Authorization-header substrings: `Bearer <token>`, `Basic <token>`, `Token <token>`\n\t/\\b(?:Bearer|Basic|Token)\\s+[A-Za-z0-9._~+/=-]{12,}/gi,\n\t// OpenAI / Anthropic API keys\n\t/\\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,\n\t// Stripe secret/restricted/publishable keys (`sk_live_…`, `rk_test_…`, …)\n\t/\\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,\n\t// Google API keys\n\t/\\bAIza[0-9A-Za-z_-]{35}\\b/g,\n\t// Slack tokens (xoxb, xoxp, xoxa, xoxr, xoxs, xoxo)\n\t/\\bxox[abprso]-[A-Za-z0-9-]{10,}/g,\n\t// GitHub tokens (ghp, ghs, gho, ghr, ghu)\n\t/\\bgh[psoru]_[A-Za-z0-9]{20,}/g,\n\t// GitHub fine-grained personal access tokens\n\t/\\bgithub_pat_[A-Za-z0-9_]{22,}/g,\n\t// AWS access key id\n\t/\\bAKIA[0-9A-Z]{16}\\b/g,\n\t// Telegram bot token (`<bot id>:<35-char secret>`, also inside `/bot…/` URLs)\n\t/\\b(?:bot)?\\d{8,10}:[A-Za-z0-9_-]{35}\\b/g,\n\t// Discord bot token: three base64url segments, the first being the encoded\n\t// application id (so it starts with the encoding of a snowflake's leading digit).\n\t/\\b[MNO][A-Za-z0-9_-]{22,}\\.[A-Za-z0-9_-]{6}\\.[A-Za-z0-9_-]{27,}\\b/g,\n\t// Linear API keys (`lin_api_…`) and OAuth tokens (`lin_oauth_…`)\n\t/\\blin_(?:api|oauth)_[A-Za-z0-9]{20,}/g,\n\t// Credentials embedded in a URL: `scheme://user:password@` — redact the userinfo.\n\t/(?<=:\\/\\/)[^\\s:/@]+:[^\\s:/@]+(?=@)/g,\n\t// JSON-shaped `\"key\": \"value\"` — matches the quoted field as a whole.\n\t// Run before the loose pattern so nested objects like\n\t// `{\"credentials\": {\"apiKey\": \"...\"}}` don't have the outer key consume\n\t// the inner key on its way to a non-quoted (object) value. The value\n\t// body uses the unrolled JSON-string idiom `(?:[^\"\\\\\\r\\n]|\\\\.)*`: the\n\t// negated class excludes the backslash so a backslash can only be consumed\n\t// by the `\\\\.` escape branch. Keep the two alternatives disjoint (don't\n\t// fold `\\\\` back into the negated class) — that keeps every run of\n\t// backslashes to a single, unambiguous parse, so matching stays fast on any\n\t// input. An escaped quote inside the value (`\"abc\\\"def\"`) still doesn't end\n\t// the match early, via the escape branch. The negative lookahead skips\n\t// values that are already a `[redacted]` / `[REDACTED]` / typed\n\t// `[REDACTED:<type>:<index>]` placeholder so this stays idempotent when\n\t// chained behind upstream object-walking redaction (langsmith trace\n\t// payloads, mcp-browser markers).\n\tnew RegExp(\n\t\t`\"(?:${SECRET_KEYS})\"\\\\s*:\\\\s*\"(?!\\\\[(?:redacted|REDACTED)(?::[^\"\\\\]]*)?\\\\]\")(?:[^\"\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*\"`,\n\t\t'gi',\n\t),\n\t// JS-object-shaped `'key': 'value'`\n\tnew RegExp(\n\t\t`'(?:${SECRET_KEYS})'\\\\s*:\\\\s*'(?!\\\\[(?:redacted|REDACTED)(?::[^'\\\\]]*)?\\\\]')(?:[^'\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*'`,\n\t\t'gi',\n\t),\n\t// Generic `password=...` / `api_key=...` / `secret=...` style assignments.\n\t// The negative lookbehind skips a keyword sitting at the `<type>` position of\n\t// an upstream `[REDACTED:<type>:<index>]` marker (e.g. mcp-browser output), so\n\t// the `secret:1]` tail isn't re-matched into a nested `[REDACTED:[REDACTED]`.\n\t// Checking only the `[REDACTED:` prefix suffices: inside a marker a keyword can\n\t// only start a `\\b` match right after that prefix — every other keyword-shaped\n\t// substring is preceded by `_` (snake_case type slug) or a digit, so no word\n\t// boundary opens there. The value lookahead skips values that are already a\n\t// redaction placeholder (bracketed, typed, or URL-safe bare form) — the same\n\t// idempotency convention as the quoted forms.\n\tnew RegExp(\n\t\t`(?<!\\\\[(?:redacted|REDACTED):)\\\\b(?:${SECRET_KEYS})\\\\s*[:=]\\\\s*(?!\\\\[?(?:redacted|REDACTED)\\\\b)\\\\S+`,\n\t\t'gi',\n\t),\n];\n\nexport function scrubSecretsInText(input: string): string {\n\tlet out = input;\n\tfor (const pattern of SECRET_VALUE_PATTERNS) {\n\t\tout = out.replace(pattern, '[REDACTED]');\n\t}\n\treturn out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAcA,MAAa,cACZ;AAED,MAAa,wBAA2C;CAEvD;CAGA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAGA;CAEA;CAEA;CAgBA,IAAI,OACH,OAAO,YAAY,uFACnB,IACD;CAEA,IAAI,OACH,OAAO,YAAY,uFACnB,IACD;CAWA,IAAI,OACH,uCAAuC,YAAY,oDACnD,IACD;AACD;AAEA,SAAgB,mBAAmB,OAAuB;CACzD,IAAI,MAAM;CACV,KAAK,MAAM,WAAW,uBACrB,MAAM,IAAI,QAAQ,SAAS,YAAY;CAExC,OAAO;AACR"}
1
+ {"version":3,"file":"scrub-secrets.cjs","names":[],"sources":["../src/scrub-secrets.ts"],"sourcesContent":["/**\n * Replace common credential patterns in free-form text with `[REDACTED]`.\n *\n * Used before persisting or transmitting user-supplied text (telemetry\n * excerpts, eval report HTML, free-form feedback) where keys/tokens\n * accidentally pasted into prompts or command lines could otherwise leak\n * downstream.\n *\n * Conservative by design: matches well-known prefixed tokens, explicit\n * `key=value` pairs, and quoted JSON/JS-object fields with sensitive\n * names. We don't attempt to redact arbitrary long opaque strings — false\n * positives on file paths, IDs, or base64 payloads would make the output\n * unreadable.\n */\nexport const SECRET_KEYS =\n\t'password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|session[_-]?cookie|token';\n\n// `\\b` never fires inside a compound key (`webhook_secret`, `bot_token`)\n// because `_` is a word character, so key patterns also accept an optional\n// snake/kebab/dotted prefix (`slack.token`) before the secret word. The\n// quoted-field matchers can only start at a quote, so scanning the whole key\n// stays linear. The unanchored assignment matcher can start at every `\\b` of a\n// long hyphenated or dotted non-secret token, where an unbounded prefix\n// backtracks through every separator at every start position (quadratic) — so\n// that one is length-bounded, and a `_`-joined unquoted key with a longer\n// prefix is the accepted gap.\nconst QUOTED_KEY_PREFIX = '(?:[\\\\w.-]*[._-])?';\nconst BOUNDED_KEY_PREFIX = '(?:[\\\\w.-]{0,128}[._-])?';\n\nexport const SECRET_VALUE_PATTERNS: readonly RegExp[] = [\n\t// PEM private-key blocks (RSA/EC/DSA/OpenSSH/PGP). Whole block, multiline.\n\t/-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,\n\t// JWTs: `eyJ<header>.eyJ<payload>.<signature>` (both leading segments are\n\t// base64url of a `{\"` object, which makes this highly distinctive).\n\t/\\beyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g,\n\t// Authorization-header substrings. After an `Authorization:` label, match\n\t// the `Bearer`, `Basic`, and `Token` schemes with values of any length. The\n\t// value can be quoted. Elsewhere, a 12+ character minimum keeps prose such\n\t// as \"a Bearer token\" or \"basic usage\" readable. Scheme values are opaque\n\t// in practice (`id|secret`, `user:key`), so they run to the next delimiter\n\t// rather than a token68 character class.\n\t// An escaped character inside the value is part of the value.\n\t/(?<=\\bauthorization\\s*[:=]\\s*[\"']?)(?:Bearer|Basic|Token)\\s+(?:\\\\.|[^\\s\"',;\\\\])+/gi,\n\t/\\b(?:Bearer\\s+[^\\s\"',;]{12,}|(?:Basic|Token)\\s+[A-Za-z0-9._~+/=-]{12,})/gi,\n\t// OpenAI / Anthropic API keys\n\t/\\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,\n\t// Stripe secret/restricted/publishable keys (`sk_live_…`, `rk_test_…`, …)\n\t/\\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,\n\t// Google API keys\n\t/\\bAIza[0-9A-Za-z_-]{35}\\b/g,\n\t// Google OAuth access tokens\n\t/\\bya29\\.[0-9A-Za-z_-]{20,}\\b/g,\n\t// Slack tokens (xoxb, xoxp, xoxa, xoxr, xoxs, xoxo)\n\t/\\bxox[abprso]-[A-Za-z0-9-]{10,}/g,\n\t// GitHub tokens (ghp, ghs, gho, ghr, ghu)\n\t/\\bgh[psoru]_[A-Za-z0-9]{20,}/g,\n\t// GitHub fine-grained personal access tokens\n\t/\\bgithub_pat_[A-Za-z0-9_]{22,}/g,\n\t// AWS access key id\n\t/\\bAKIA[0-9A-Z]{16}\\b/g,\n\t// Telegram bot token (`<bot id>:<35-char secret>`, also inside `/bot…/` URLs)\n\t/\\b(?:bot)?\\d{8,10}:[A-Za-z0-9_-]{35}\\b/g,\n\t// Discord bot token: three base64url segments, the first being the encoded\n\t// application id (so it starts with the encoding of a snowflake's leading digit).\n\t/\\b[MNO][A-Za-z0-9_-]{22,}\\.[A-Za-z0-9_-]{6}\\.[A-Za-z0-9_-]{27,}\\b/g,\n\t// Linear API keys (`lin_api_…`) and OAuth tokens (`lin_oauth_…`)\n\t/\\blin_(?:api|oauth)_[A-Za-z0-9]{20,}/g,\n\t// Credentials embedded in a URL: `scheme://user:password@` — redact the userinfo.\n\t/(?<=:\\/\\/)[^\\s:/@]+:[^\\s:/@]+(?=@)/g,\n\t// Quoted `\"key\": \"value\"` (JSON) and `'key': 'value'` (JS object) fields,\n\t// matched as a whole. The key's quote and the value's quote are captured\n\t// separately so a mixed form like `\"password\": 'secret'` is covered too.\n\t// Run before the loose pattern so nested objects like\n\t// `{\"credentials\": {\"apiKey\": \"...\"}}` don't have the outer key consume\n\t// the inner key on its way to a non-quoted (object) value. The value\n\t// body uses the unrolled JSON-string idiom `(?:(?!\\2)[^\\\\\\r\\n]|\\\\.)*`: the\n\t// negated class excludes the backslash so a backslash can only be consumed\n\t// by the `\\\\.` escape branch. Keep the two alternatives disjoint (don't\n\t// fold `\\\\` back into the negated class) — that keeps every run of\n\t// backslashes to a single, unambiguous parse, so matching stays fast on any\n\t// input. An escaped quote inside the value (`\"abc\\\"def\"`) still doesn't end\n\t// the match early, via the escape branch. The negative lookahead skips\n\t// values that are already a `[redacted]` / `[REDACTED]` / typed\n\t// `[REDACTED:<type>:<index>]` placeholder so this stays idempotent when\n\t// chained behind upstream object-walking redaction (langsmith trace\n\t// payloads, mcp-browser markers).\n\tnew RegExp(\n\t\t`([\"'])${QUOTED_KEY_PREFIX}(?:${SECRET_KEYS})\\\\1\\\\s*:\\\\s*([\"'])(?!\\\\[(?:redacted|REDACTED)(?::[^\\\\]]*)?\\\\]\\\\2)(?:(?!\\\\2)[^\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*\\\\2`,\n\t\t'gi',\n\t),\n\t// The same field inside a JSON-encoded string (an upstream response body\n\t// serialized into an error `message`) has every quote escaped:\n\t// `\\\"api_key\\\": \\\"…\\\"`. The pattern above can't see it because the key is\n\t// followed by `\\`, not by its quote. Here the value body has three units that\n\t// are disjoint by their first two characters — a plain character, a `\\x`\n\t// escape other than the closing `\\\"`, or `\\\\` together with the token it\n\t// escapes — so an inner escaped quote (`\\\\\\\"`) is consumed as one unit rather\n\t// than ending the match early, and every input still has a single parse.\n\tnew RegExp(\n\t\t`\\\\\\\\([\"'])${QUOTED_KEY_PREFIX}(?:${SECRET_KEYS})\\\\\\\\\\\\1\\\\s*:\\\\s*\\\\\\\\([\"'])(?!\\\\[(?:redacted|REDACTED)(?::[^\\\\]]*)?\\\\]\\\\\\\\\\\\2)(?:(?!\\\\2)[^\\\\\\\\\\\\r\\\\n]|\\\\\\\\(?!\\\\2)[^\\\\\\\\\\\\r\\\\n]|\\\\\\\\\\\\\\\\(?:\\\\\\\\.|[^\\\\\\\\\\\\r\\\\n]))*\\\\\\\\\\\\2`,\n\t\t'gi',\n\t),\n\t// Generic `password=...` / `api_key=...` / `secret=...` style assignments.\n\t// The negative lookbehind skips a keyword sitting at the `<type>` position of\n\t// an upstream `[REDACTED:<type>:<index>]` marker (e.g. mcp-browser output), so\n\t// the `secret:1]` tail isn't re-matched into a nested `[REDACTED:[REDACTED]`.\n\t// Checking only the `[REDACTED:` prefix suffices: inside a marker a keyword can\n\t// only start a `\\b` match right after that prefix — every other keyword-shaped\n\t// substring is preceded by `_` (snake_case type slug) or a digit, so no word\n\t// boundary opens there. The value lookahead skips values that are already a\n\t// redaction placeholder (bracketed, typed, or URL-safe bare form) — the same\n\t// idempotency convention as the quoted forms. A quoted value is consumed as\n\t// a whole, including escapes, so `secret=\"alpha beta\"` does not leave a tail.\n\tnew RegExp(\n\t\t`(?<!\\\\[(?:redacted|REDACTED):)\\\\b${BOUNDED_KEY_PREFIX}(?:${SECRET_KEYS})\\\\s*[:=]\\\\s*(?!\\\\[?(?:redacted|REDACTED)\\\\b)(?:\"(?:[^\"\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*\"|'(?:[^'\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*'|\\\\S+)`,\n\t\t'gi',\n\t),\n];\n\nexport function scrubSecretsInText(input: string): string {\n\tlet out = input;\n\tfor (const pattern of SECRET_VALUE_PATTERNS) {\n\t\tout = out.replace(pattern, '[REDACTED]');\n\t}\n\treturn out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAcA,MAAa,cACZ;AAWD,MAAM,oBAAoB;AAG1B,MAAa,wBAA2C;CAEvD;CAGA;CAQA;CACA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAGA;CAEA;CAEA;CAkBA,IAAI,OACH,SAAS,kBAAkB,KAAK,YAAY,uGAC5C,IACD;CASA,IAAI,OACH,aAAa,kBAAkB,KAAK,YAAY,0KAChD,IACD;CAYA,IAAI,OACH,+DAA4D,YAAY,gHACxE,IACD;AACD;AAEA,SAAgB,mBAAmB,OAAuB;CACzD,IAAI,MAAM;CACV,KAAK,MAAM,WAAW,uBACrB,MAAM,IAAI,QAAQ,SAAS,YAAY;CAExC,OAAO;AACR"}
@@ -1,5 +1,5 @@
1
1
  //#region src/scrub-secrets.d.ts
2
- declare const SECRET_KEYS = "password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token";
2
+ declare const SECRET_KEYS = "password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|session[_-]?cookie|token";
3
3
  declare const SECRET_VALUE_PATTERNS: readonly RegExp[];
4
4
  declare function scrubSecretsInText(input: string): string;
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  //#region src/scrub-secrets.d.ts
2
- declare const SECRET_KEYS = "password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token";
2
+ declare const SECRET_KEYS = "password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|session[_-]?cookie|token";
3
3
  declare const SECRET_VALUE_PATTERNS: readonly RegExp[];
4
4
  declare function scrubSecretsInText(input: string): string;
5
5
  //#endregion
@@ -13,14 +13,17 @@
13
13
  * positives on file paths, IDs, or base64 payloads would make the output
14
14
  * unreadable.
15
15
  */
16
- const SECRET_KEYS = "password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token";
16
+ const SECRET_KEYS = "password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|session[_-]?cookie|token";
17
+ const QUOTED_KEY_PREFIX = "(?:[\\w.-]*[._-])?";
17
18
  const SECRET_VALUE_PATTERNS = [
18
19
  /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,
19
20
  /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
20
- /\b(?:Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{12,}/gi,
21
+ /(?<=\bauthorization\s*[:=]\s*["']?)(?:Bearer|Basic|Token)\s+(?:\\.|[^\s"',;\\])+/gi,
22
+ /\b(?:Bearer\s+[^\s"',;]{12,}|(?:Basic|Token)\s+[A-Za-z0-9._~+/=-]{12,})/gi,
21
23
  /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,
22
24
  /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,
23
25
  /\bAIza[0-9A-Za-z_-]{35}\b/g,
26
+ /\bya29\.[0-9A-Za-z_-]{20,}\b/g,
24
27
  /\bxox[abprso]-[A-Za-z0-9-]{10,}/g,
25
28
  /\bgh[psoru]_[A-Za-z0-9]{20,}/g,
26
29
  /\bgithub_pat_[A-Za-z0-9_]{22,}/g,
@@ -29,9 +32,9 @@ const SECRET_VALUE_PATTERNS = [
29
32
  /\b[MNO][A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/g,
30
33
  /\blin_(?:api|oauth)_[A-Za-z0-9]{20,}/g,
31
34
  /(?<=:\/\/)[^\s:/@]+:[^\s:/@]+(?=@)/g,
32
- new RegExp(`"(?:${SECRET_KEYS})"\\s*:\\s*"(?!\\[(?:redacted|REDACTED)(?::[^"\\]]*)?\\]")(?:[^"\\\\\\r\\n]|\\\\.)*"`, "gi"),
33
- new RegExp(`'(?:${SECRET_KEYS})'\\s*:\\s*'(?!\\[(?:redacted|REDACTED)(?::[^'\\]]*)?\\]')(?:[^'\\\\\\r\\n]|\\\\.)*'`, "gi"),
34
- new RegExp(`(?<!\\[(?:redacted|REDACTED):)\\b(?:${SECRET_KEYS})\\s*[:=]\\s*(?!\\[?(?:redacted|REDACTED)\\b)\\S+`, "gi")
35
+ new RegExp(`(["'])${QUOTED_KEY_PREFIX}(?:${SECRET_KEYS})\\1\\s*:\\s*(["'])(?!\\[(?:redacted|REDACTED)(?::[^\\]]*)?\\]\\2)(?:(?!\\2)[^\\\\\\r\\n]|\\\\.)*\\2`, "gi"),
36
+ new RegExp(`\\\\(["'])${QUOTED_KEY_PREFIX}(?:${SECRET_KEYS})\\\\\\1\\s*:\\s*\\\\(["'])(?!\\[(?:redacted|REDACTED)(?::[^\\]]*)?\\]\\\\\\2)(?:(?!\\2)[^\\\\\\r\\n]|\\\\(?!\\2)[^\\\\\\r\\n]|\\\\\\\\(?:\\\\.|[^\\\\\\r\\n]))*\\\\\\2`, "gi"),
37
+ new RegExp(`(?<!\\[(?:redacted|REDACTED):)\\b(?:[\\w.-]{0,128}[._-])?(?:${SECRET_KEYS})\\s*[:=]\\s*(?!\\[?(?:redacted|REDACTED)\\b)(?:"(?:[^"\\\\\\r\\n]|\\\\.)*"|'(?:[^'\\\\\\r\\n]|\\\\.)*'|\\S+)`, "gi")
35
38
  ];
36
39
  function scrubSecretsInText(input) {
37
40
  let out = input;
@@ -1 +1 @@
1
- {"version":3,"file":"scrub-secrets.mjs","names":[],"sources":["../src/scrub-secrets.ts"],"sourcesContent":["/**\n * Replace common credential patterns in free-form text with `[REDACTED]`.\n *\n * Used before persisting or transmitting user-supplied text (telemetry\n * excerpts, eval report HTML, free-form feedback) where keys/tokens\n * accidentally pasted into prompts or command lines could otherwise leak\n * downstream.\n *\n * Conservative by design: matches well-known prefixed tokens, explicit\n * `key=value` pairs, and quoted JSON/JS-object fields with sensitive\n * names. We don't attempt to redact arbitrary long opaque strings — false\n * positives on file paths, IDs, or base64 payloads would make the output\n * unreadable.\n */\nexport const SECRET_KEYS =\n\t'password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token';\n\nexport const SECRET_VALUE_PATTERNS: readonly RegExp[] = [\n\t// PEM private-key blocks (RSA/EC/DSA/OpenSSH/PGP). Whole block, multiline.\n\t/-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,\n\t// JWTs: `eyJ<header>.eyJ<payload>.<signature>` (both leading segments are\n\t// base64url of a `{\"` object, which makes this highly distinctive).\n\t/\\beyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g,\n\t// Authorization-header substrings: `Bearer <token>`, `Basic <token>`, `Token <token>`\n\t/\\b(?:Bearer|Basic|Token)\\s+[A-Za-z0-9._~+/=-]{12,}/gi,\n\t// OpenAI / Anthropic API keys\n\t/\\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,\n\t// Stripe secret/restricted/publishable keys (`sk_live_…`, `rk_test_…`, …)\n\t/\\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,\n\t// Google API keys\n\t/\\bAIza[0-9A-Za-z_-]{35}\\b/g,\n\t// Slack tokens (xoxb, xoxp, xoxa, xoxr, xoxs, xoxo)\n\t/\\bxox[abprso]-[A-Za-z0-9-]{10,}/g,\n\t// GitHub tokens (ghp, ghs, gho, ghr, ghu)\n\t/\\bgh[psoru]_[A-Za-z0-9]{20,}/g,\n\t// GitHub fine-grained personal access tokens\n\t/\\bgithub_pat_[A-Za-z0-9_]{22,}/g,\n\t// AWS access key id\n\t/\\bAKIA[0-9A-Z]{16}\\b/g,\n\t// Telegram bot token (`<bot id>:<35-char secret>`, also inside `/bot…/` URLs)\n\t/\\b(?:bot)?\\d{8,10}:[A-Za-z0-9_-]{35}\\b/g,\n\t// Discord bot token: three base64url segments, the first being the encoded\n\t// application id (so it starts with the encoding of a snowflake's leading digit).\n\t/\\b[MNO][A-Za-z0-9_-]{22,}\\.[A-Za-z0-9_-]{6}\\.[A-Za-z0-9_-]{27,}\\b/g,\n\t// Linear API keys (`lin_api_…`) and OAuth tokens (`lin_oauth_…`)\n\t/\\blin_(?:api|oauth)_[A-Za-z0-9]{20,}/g,\n\t// Credentials embedded in a URL: `scheme://user:password@` — redact the userinfo.\n\t/(?<=:\\/\\/)[^\\s:/@]+:[^\\s:/@]+(?=@)/g,\n\t// JSON-shaped `\"key\": \"value\"` — matches the quoted field as a whole.\n\t// Run before the loose pattern so nested objects like\n\t// `{\"credentials\": {\"apiKey\": \"...\"}}` don't have the outer key consume\n\t// the inner key on its way to a non-quoted (object) value. The value\n\t// body uses the unrolled JSON-string idiom `(?:[^\"\\\\\\r\\n]|\\\\.)*`: the\n\t// negated class excludes the backslash so a backslash can only be consumed\n\t// by the `\\\\.` escape branch. Keep the two alternatives disjoint (don't\n\t// fold `\\\\` back into the negated class) — that keeps every run of\n\t// backslashes to a single, unambiguous parse, so matching stays fast on any\n\t// input. An escaped quote inside the value (`\"abc\\\"def\"`) still doesn't end\n\t// the match early, via the escape branch. The negative lookahead skips\n\t// values that are already a `[redacted]` / `[REDACTED]` / typed\n\t// `[REDACTED:<type>:<index>]` placeholder so this stays idempotent when\n\t// chained behind upstream object-walking redaction (langsmith trace\n\t// payloads, mcp-browser markers).\n\tnew RegExp(\n\t\t`\"(?:${SECRET_KEYS})\"\\\\s*:\\\\s*\"(?!\\\\[(?:redacted|REDACTED)(?::[^\"\\\\]]*)?\\\\]\")(?:[^\"\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*\"`,\n\t\t'gi',\n\t),\n\t// JS-object-shaped `'key': 'value'`\n\tnew RegExp(\n\t\t`'(?:${SECRET_KEYS})'\\\\s*:\\\\s*'(?!\\\\[(?:redacted|REDACTED)(?::[^'\\\\]]*)?\\\\]')(?:[^'\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*'`,\n\t\t'gi',\n\t),\n\t// Generic `password=...` / `api_key=...` / `secret=...` style assignments.\n\t// The negative lookbehind skips a keyword sitting at the `<type>` position of\n\t// an upstream `[REDACTED:<type>:<index>]` marker (e.g. mcp-browser output), so\n\t// the `secret:1]` tail isn't re-matched into a nested `[REDACTED:[REDACTED]`.\n\t// Checking only the `[REDACTED:` prefix suffices: inside a marker a keyword can\n\t// only start a `\\b` match right after that prefix — every other keyword-shaped\n\t// substring is preceded by `_` (snake_case type slug) or a digit, so no word\n\t// boundary opens there. The value lookahead skips values that are already a\n\t// redaction placeholder (bracketed, typed, or URL-safe bare form) — the same\n\t// idempotency convention as the quoted forms.\n\tnew RegExp(\n\t\t`(?<!\\\\[(?:redacted|REDACTED):)\\\\b(?:${SECRET_KEYS})\\\\s*[:=]\\\\s*(?!\\\\[?(?:redacted|REDACTED)\\\\b)\\\\S+`,\n\t\t'gi',\n\t),\n];\n\nexport function scrubSecretsInText(input: string): string {\n\tlet out = input;\n\tfor (const pattern of SECRET_VALUE_PATTERNS) {\n\t\tout = out.replace(pattern, '[REDACTED]');\n\t}\n\treturn out;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAa,cACZ;AAED,MAAa,wBAA2C;CAEvD;CAGA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAGA;CAEA;CAEA;CAgBA,IAAI,OACH,OAAO,YAAY,uFACnB,IACD;CAEA,IAAI,OACH,OAAO,YAAY,uFACnB,IACD;CAWA,IAAI,OACH,uCAAuC,YAAY,oDACnD,IACD;AACD;AAEA,SAAgB,mBAAmB,OAAuB;CACzD,IAAI,MAAM;CACV,KAAK,MAAM,WAAW,uBACrB,MAAM,IAAI,QAAQ,SAAS,YAAY;CAExC,OAAO;AACR"}
1
+ {"version":3,"file":"scrub-secrets.mjs","names":[],"sources":["../src/scrub-secrets.ts"],"sourcesContent":["/**\n * Replace common credential patterns in free-form text with `[REDACTED]`.\n *\n * Used before persisting or transmitting user-supplied text (telemetry\n * excerpts, eval report HTML, free-form feedback) where keys/tokens\n * accidentally pasted into prompts or command lines could otherwise leak\n * downstream.\n *\n * Conservative by design: matches well-known prefixed tokens, explicit\n * `key=value` pairs, and quoted JSON/JS-object fields with sensitive\n * names. We don't attempt to redact arbitrary long opaque strings — false\n * positives on file paths, IDs, or base64 payloads would make the output\n * unreadable.\n */\nexport const SECRET_KEYS =\n\t'password|passwd|secret|credentials?|api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|session[_-]?cookie|token';\n\n// `\\b` never fires inside a compound key (`webhook_secret`, `bot_token`)\n// because `_` is a word character, so key patterns also accept an optional\n// snake/kebab/dotted prefix (`slack.token`) before the secret word. The\n// quoted-field matchers can only start at a quote, so scanning the whole key\n// stays linear. The unanchored assignment matcher can start at every `\\b` of a\n// long hyphenated or dotted non-secret token, where an unbounded prefix\n// backtracks through every separator at every start position (quadratic) — so\n// that one is length-bounded, and a `_`-joined unquoted key with a longer\n// prefix is the accepted gap.\nconst QUOTED_KEY_PREFIX = '(?:[\\\\w.-]*[._-])?';\nconst BOUNDED_KEY_PREFIX = '(?:[\\\\w.-]{0,128}[._-])?';\n\nexport const SECRET_VALUE_PATTERNS: readonly RegExp[] = [\n\t// PEM private-key blocks (RSA/EC/DSA/OpenSSH/PGP). Whole block, multiline.\n\t/-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,\n\t// JWTs: `eyJ<header>.eyJ<payload>.<signature>` (both leading segments are\n\t// base64url of a `{\"` object, which makes this highly distinctive).\n\t/\\beyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g,\n\t// Authorization-header substrings. After an `Authorization:` label, match\n\t// the `Bearer`, `Basic`, and `Token` schemes with values of any length. The\n\t// value can be quoted. Elsewhere, a 12+ character minimum keeps prose such\n\t// as \"a Bearer token\" or \"basic usage\" readable. Scheme values are opaque\n\t// in practice (`id|secret`, `user:key`), so they run to the next delimiter\n\t// rather than a token68 character class.\n\t// An escaped character inside the value is part of the value.\n\t/(?<=\\bauthorization\\s*[:=]\\s*[\"']?)(?:Bearer|Basic|Token)\\s+(?:\\\\.|[^\\s\"',;\\\\])+/gi,\n\t/\\b(?:Bearer\\s+[^\\s\"',;]{12,}|(?:Basic|Token)\\s+[A-Za-z0-9._~+/=-]{12,})/gi,\n\t// OpenAI / Anthropic API keys\n\t/\\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,\n\t// Stripe secret/restricted/publishable keys (`sk_live_…`, `rk_test_…`, …)\n\t/\\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,\n\t// Google API keys\n\t/\\bAIza[0-9A-Za-z_-]{35}\\b/g,\n\t// Google OAuth access tokens\n\t/\\bya29\\.[0-9A-Za-z_-]{20,}\\b/g,\n\t// Slack tokens (xoxb, xoxp, xoxa, xoxr, xoxs, xoxo)\n\t/\\bxox[abprso]-[A-Za-z0-9-]{10,}/g,\n\t// GitHub tokens (ghp, ghs, gho, ghr, ghu)\n\t/\\bgh[psoru]_[A-Za-z0-9]{20,}/g,\n\t// GitHub fine-grained personal access tokens\n\t/\\bgithub_pat_[A-Za-z0-9_]{22,}/g,\n\t// AWS access key id\n\t/\\bAKIA[0-9A-Z]{16}\\b/g,\n\t// Telegram bot token (`<bot id>:<35-char secret>`, also inside `/bot…/` URLs)\n\t/\\b(?:bot)?\\d{8,10}:[A-Za-z0-9_-]{35}\\b/g,\n\t// Discord bot token: three base64url segments, the first being the encoded\n\t// application id (so it starts with the encoding of a snowflake's leading digit).\n\t/\\b[MNO][A-Za-z0-9_-]{22,}\\.[A-Za-z0-9_-]{6}\\.[A-Za-z0-9_-]{27,}\\b/g,\n\t// Linear API keys (`lin_api_…`) and OAuth tokens (`lin_oauth_…`)\n\t/\\blin_(?:api|oauth)_[A-Za-z0-9]{20,}/g,\n\t// Credentials embedded in a URL: `scheme://user:password@` — redact the userinfo.\n\t/(?<=:\\/\\/)[^\\s:/@]+:[^\\s:/@]+(?=@)/g,\n\t// Quoted `\"key\": \"value\"` (JSON) and `'key': 'value'` (JS object) fields,\n\t// matched as a whole. The key's quote and the value's quote are captured\n\t// separately so a mixed form like `\"password\": 'secret'` is covered too.\n\t// Run before the loose pattern so nested objects like\n\t// `{\"credentials\": {\"apiKey\": \"...\"}}` don't have the outer key consume\n\t// the inner key on its way to a non-quoted (object) value. The value\n\t// body uses the unrolled JSON-string idiom `(?:(?!\\2)[^\\\\\\r\\n]|\\\\.)*`: the\n\t// negated class excludes the backslash so a backslash can only be consumed\n\t// by the `\\\\.` escape branch. Keep the two alternatives disjoint (don't\n\t// fold `\\\\` back into the negated class) — that keeps every run of\n\t// backslashes to a single, unambiguous parse, so matching stays fast on any\n\t// input. An escaped quote inside the value (`\"abc\\\"def\"`) still doesn't end\n\t// the match early, via the escape branch. The negative lookahead skips\n\t// values that are already a `[redacted]` / `[REDACTED]` / typed\n\t// `[REDACTED:<type>:<index>]` placeholder so this stays idempotent when\n\t// chained behind upstream object-walking redaction (langsmith trace\n\t// payloads, mcp-browser markers).\n\tnew RegExp(\n\t\t`([\"'])${QUOTED_KEY_PREFIX}(?:${SECRET_KEYS})\\\\1\\\\s*:\\\\s*([\"'])(?!\\\\[(?:redacted|REDACTED)(?::[^\\\\]]*)?\\\\]\\\\2)(?:(?!\\\\2)[^\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*\\\\2`,\n\t\t'gi',\n\t),\n\t// The same field inside a JSON-encoded string (an upstream response body\n\t// serialized into an error `message`) has every quote escaped:\n\t// `\\\"api_key\\\": \\\"…\\\"`. The pattern above can't see it because the key is\n\t// followed by `\\`, not by its quote. Here the value body has three units that\n\t// are disjoint by their first two characters — a plain character, a `\\x`\n\t// escape other than the closing `\\\"`, or `\\\\` together with the token it\n\t// escapes — so an inner escaped quote (`\\\\\\\"`) is consumed as one unit rather\n\t// than ending the match early, and every input still has a single parse.\n\tnew RegExp(\n\t\t`\\\\\\\\([\"'])${QUOTED_KEY_PREFIX}(?:${SECRET_KEYS})\\\\\\\\\\\\1\\\\s*:\\\\s*\\\\\\\\([\"'])(?!\\\\[(?:redacted|REDACTED)(?::[^\\\\]]*)?\\\\]\\\\\\\\\\\\2)(?:(?!\\\\2)[^\\\\\\\\\\\\r\\\\n]|\\\\\\\\(?!\\\\2)[^\\\\\\\\\\\\r\\\\n]|\\\\\\\\\\\\\\\\(?:\\\\\\\\.|[^\\\\\\\\\\\\r\\\\n]))*\\\\\\\\\\\\2`,\n\t\t'gi',\n\t),\n\t// Generic `password=...` / `api_key=...` / `secret=...` style assignments.\n\t// The negative lookbehind skips a keyword sitting at the `<type>` position of\n\t// an upstream `[REDACTED:<type>:<index>]` marker (e.g. mcp-browser output), so\n\t// the `secret:1]` tail isn't re-matched into a nested `[REDACTED:[REDACTED]`.\n\t// Checking only the `[REDACTED:` prefix suffices: inside a marker a keyword can\n\t// only start a `\\b` match right after that prefix — every other keyword-shaped\n\t// substring is preceded by `_` (snake_case type slug) or a digit, so no word\n\t// boundary opens there. The value lookahead skips values that are already a\n\t// redaction placeholder (bracketed, typed, or URL-safe bare form) — the same\n\t// idempotency convention as the quoted forms. A quoted value is consumed as\n\t// a whole, including escapes, so `secret=\"alpha beta\"` does not leave a tail.\n\tnew RegExp(\n\t\t`(?<!\\\\[(?:redacted|REDACTED):)\\\\b${BOUNDED_KEY_PREFIX}(?:${SECRET_KEYS})\\\\s*[:=]\\\\s*(?!\\\\[?(?:redacted|REDACTED)\\\\b)(?:\"(?:[^\"\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*\"|'(?:[^'\\\\\\\\\\\\r\\\\n]|\\\\\\\\.)*'|\\\\S+)`,\n\t\t'gi',\n\t),\n];\n\nexport function scrubSecretsInText(input: string): string {\n\tlet out = input;\n\tfor (const pattern of SECRET_VALUE_PATTERNS) {\n\t\tout = out.replace(pattern, '[REDACTED]');\n\t}\n\treturn out;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAa,cACZ;AAWD,MAAM,oBAAoB;AAG1B,MAAa,wBAA2C;CAEvD;CAGA;CAQA;CACA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAGA;CAEA;CAEA;CAkBA,IAAI,OACH,SAAS,kBAAkB,KAAK,YAAY,uGAC5C,IACD;CASA,IAAI,OACH,aAAa,kBAAkB,KAAK,YAAY,0KAChD,IACD;CAYA,IAAI,OACH,+DAA4D,YAAY,gHACxE,IACD;AACD;AAEA,SAAgB,mBAAmB,OAAuB;CACzD,IAAI,MAAM;CACV,KAAK,MAAM,WAAW,uBACrB,MAAM,IAAI,QAAQ,SAAS,YAAY;CAExC,OAAO;AACR"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@n8n/utils",
3
3
  "type": "module",
4
- "version": "1.46.1",
4
+ "version": "1.48.0",
5
5
  "files": [
6
6
  "dist",
7
- "LICENSE.md",
8
- "LICENSE_EE.md"
7
+ "LICENSE_EE.md",
8
+ "LICENSE.md"
9
9
  ],
10
10
  "exports": {
11
11
  "./dist/*": {
@@ -27,19 +27,19 @@
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
- "nanoid": "3.3.18",
31
- "@n8n/constants": "0.37.1"
30
+ "@n8n/constants": "0.39.0",
31
+ "nanoid": "3.3.18"
32
32
  },
33
33
  "devDependencies": {
34
+ "@n8n/eslint-config": "0.0.1",
35
+ "@n8n/typescript-config": "1.12.0",
36
+ "@n8n/vitest-config": "1.22.0",
34
37
  "@testing-library/jest-dom": "^6.6.3",
35
- "@testing-library/user-event": "^14.6.1",
38
+ "fast-check": "^3.23.2",
36
39
  "tsdown": "^0.22.8",
37
40
  "typescript": "7.0.2",
38
41
  "vite": "^8.0.2",
39
- "vitest": "^4.1.9",
40
- "@n8n/eslint-config": "0.0.1",
41
- "@n8n/typescript-config": "1.11.0",
42
- "@n8n/vitest-config": "1.21.0"
42
+ "vitest": "^4.1.9"
43
43
  },
44
44
  "license": "SEE LICENSE IN LICENSE.md",
45
45
  "homepage": "https://n8n.io",