New:Socket for Asana Is Now Available.Learn more
Get Started

@n8n/utils

Package Overview
Dependencies
Maintainers
5
Versions
52
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@n8n/utils - npm Package Compare versions

Comparing version
1.44.0
to
1.45.0
+36
dist/errors/error-chain.cjs
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
//#region src/errors/error-chain.ts
/** Any non-null object. Unlike is-record.ts this accepts arrays, so a wrapped array stays in the chain. */
const isObjectLike = (value) => typeof value === "object" && value !== null;
const MAX_CHAIN_DEPTH = 5;
/** The keys errors wrap each other under. */
const WRAPPING_KEYS = [
"cause",
"errorResponse",
"reason"
];
/** The error and the errors it wraps, shallowest first, each visited once. */
function errorChain(error) {
if (!isObjectLike(error)) return [];
const seen = /* @__PURE__ */ new Set([error]);
const chain = [error];
let generation = [error];
for (let depth = 0; depth < MAX_CHAIN_DEPTH && generation.length > 0; depth++) {
const next = [];
for (const level of generation) for (const key of WRAPPING_KEYS) {
const wrapped = level[key];
if (isObjectLike(wrapped) && !seen.has(wrapped)) {
seen.add(wrapped);
next.push(wrapped);
}
}
chain.push(...next);
generation = next;
}
return chain;
}
//#endregion
exports.errorChain = errorChain;
exports.isObjectLike = isObjectLike;
//# sourceMappingURL=error-chain.cjs.map
{"version":3,"file":"error-chain.cjs","names":[],"sources":["../../src/errors/error-chain.ts"],"sourcesContent":["export type UnknownRecord = Readonly<Record<string, unknown>>;\n\n/** Any non-null object. Unlike is-record.ts this accepts arrays, so a wrapped array stays in the chain. */\nexport const isObjectLike = (value: unknown): value is UnknownRecord =>\n\ttypeof value === 'object' && value !== null;\n\nconst MAX_CHAIN_DEPTH = 5;\n\n/** The keys errors wrap each other under. */\nconst WRAPPING_KEYS = ['cause', 'errorResponse', 'reason'] as const;\n\n/** The error and the errors it wraps, shallowest first, each visited once. */\nexport function errorChain(error: unknown): UnknownRecord[] {\n\tif (!isObjectLike(error)) {\n\t\treturn [];\n\t}\n\n\tconst seen = new Set<UnknownRecord>([error]);\n\tconst chain: UnknownRecord[] = [error];\n\tlet generation: UnknownRecord[] = [error];\n\n\tfor (let depth = 0; depth < MAX_CHAIN_DEPTH && generation.length > 0; depth++) {\n\t\tconst next: UnknownRecord[] = [];\n\t\tfor (const level of generation) {\n\t\t\tfor (const key of WRAPPING_KEYS) {\n\t\t\t\tconst wrapped = level[key];\n\t\t\t\tif (isObjectLike(wrapped) && !seen.has(wrapped)) {\n\t\t\t\t\tseen.add(wrapped);\n\t\t\t\t\tnext.push(wrapped);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tchain.push(...next);\n\t\tgeneration = next;\n\t}\n\n\treturn chain;\n}\n"],"mappings":";;;AAGA,MAAa,gBAAgB,UAC5B,OAAO,UAAU,YAAY,UAAU;AAExC,MAAM,kBAAkB;;AAGxB,MAAM,gBAAgB;CAAC;CAAS;CAAiB;AAAQ;;AAGzD,SAAgB,WAAW,OAAiC;CAC3D,IAAI,CAAC,aAAa,KAAK,GACtB,OAAO,CAAC;CAGT,MAAM,uBAAO,IAAI,IAAmB,CAAC,KAAK,CAAC;CAC3C,MAAM,QAAyB,CAAC,KAAK;CACrC,IAAI,aAA8B,CAAC,KAAK;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,mBAAmB,WAAW,SAAS,GAAG,SAAS;EAC9E,MAAM,OAAwB,CAAC;EAC/B,KAAK,MAAM,SAAS,YACnB,KAAK,MAAM,OAAO,eAAe;GAChC,MAAM,UAAU,MAAM;GACtB,IAAI,aAAa,OAAO,KAAK,CAAC,KAAK,IAAI,OAAO,GAAG;IAChD,KAAK,IAAI,OAAO;IAChB,KAAK,KAAK,OAAO;GAClB;EACD;EAED,MAAM,KAAK,GAAG,IAAI;EAClB,aAAa;CACd;CAEA,OAAO;AACR"}
//#region src/errors/error-chain.d.ts
type UnknownRecord = Readonly<Record<string, unknown>>;
declare const isObjectLike: (value: unknown) => value is UnknownRecord;
declare function errorChain(error: unknown): UnknownRecord[];
//#endregion
export { UnknownRecord, errorChain, isObjectLike };
//# sourceMappingURL=error-chain.d.cts.map
//#region src/errors/error-chain.d.ts
type UnknownRecord = Readonly<Record<string, unknown>>;
declare const isObjectLike: (value: unknown) => value is UnknownRecord;
declare function errorChain(error: unknown): UnknownRecord[];
//#endregion
export { UnknownRecord, errorChain, isObjectLike };
//# sourceMappingURL=error-chain.d.mts.map
//#region src/errors/error-chain.ts
/** Any non-null object. Unlike is-record.ts this accepts arrays, so a wrapped array stays in the chain. */
const isObjectLike = (value) => typeof value === "object" && value !== null;
const MAX_CHAIN_DEPTH = 5;
/** The keys errors wrap each other under. */
const WRAPPING_KEYS = [
"cause",
"errorResponse",
"reason"
];
/** The error and the errors it wraps, shallowest first, each visited once. */
function errorChain(error) {
if (!isObjectLike(error)) return [];
const seen = /* @__PURE__ */ new Set([error]);
const chain = [error];
let generation = [error];
for (let depth = 0; depth < MAX_CHAIN_DEPTH && generation.length > 0; depth++) {
const next = [];
for (const level of generation) for (const key of WRAPPING_KEYS) {
const wrapped = level[key];
if (isObjectLike(wrapped) && !seen.has(wrapped)) {
seen.add(wrapped);
next.push(wrapped);
}
}
chain.push(...next);
generation = next;
}
return chain;
}
//#endregion
export { errorChain, isObjectLike };
//# sourceMappingURL=error-chain.mjs.map
{"version":3,"file":"error-chain.mjs","names":[],"sources":["../../src/errors/error-chain.ts"],"sourcesContent":["export type UnknownRecord = Readonly<Record<string, unknown>>;\n\n/** Any non-null object. Unlike is-record.ts this accepts arrays, so a wrapped array stays in the chain. */\nexport const isObjectLike = (value: unknown): value is UnknownRecord =>\n\ttypeof value === 'object' && value !== null;\n\nconst MAX_CHAIN_DEPTH = 5;\n\n/** The keys errors wrap each other under. */\nconst WRAPPING_KEYS = ['cause', 'errorResponse', 'reason'] as const;\n\n/** The error and the errors it wraps, shallowest first, each visited once. */\nexport function errorChain(error: unknown): UnknownRecord[] {\n\tif (!isObjectLike(error)) {\n\t\treturn [];\n\t}\n\n\tconst seen = new Set<UnknownRecord>([error]);\n\tconst chain: UnknownRecord[] = [error];\n\tlet generation: UnknownRecord[] = [error];\n\n\tfor (let depth = 0; depth < MAX_CHAIN_DEPTH && generation.length > 0; depth++) {\n\t\tconst next: UnknownRecord[] = [];\n\t\tfor (const level of generation) {\n\t\t\tfor (const key of WRAPPING_KEYS) {\n\t\t\t\tconst wrapped = level[key];\n\t\t\t\tif (isObjectLike(wrapped) && !seen.has(wrapped)) {\n\t\t\t\t\tseen.add(wrapped);\n\t\t\t\t\tnext.push(wrapped);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tchain.push(...next);\n\t\tgeneration = next;\n\t}\n\n\treturn chain;\n}\n"],"mappings":";;AAGA,MAAa,gBAAgB,UAC5B,OAAO,UAAU,YAAY,UAAU;AAExC,MAAM,kBAAkB;;AAGxB,MAAM,gBAAgB;CAAC;CAAS;CAAiB;AAAQ;;AAGzD,SAAgB,WAAW,OAAiC;CAC3D,IAAI,CAAC,aAAa,KAAK,GACtB,OAAO,CAAC;CAGT,MAAM,uBAAO,IAAI,IAAmB,CAAC,KAAK,CAAC;CAC3C,MAAM,QAAyB,CAAC,KAAK;CACrC,IAAI,aAA8B,CAAC,KAAK;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,mBAAmB,WAAW,SAAS,GAAG,SAAS;EAC9E,MAAM,OAAwB,CAAC;EAC/B,KAAK,MAAM,SAAS,YACnB,KAAK,MAAM,OAAO,eAAe;GAChC,MAAM,UAAU,MAAM;GACtB,IAAI,aAAa,OAAO,KAAK,CAAC,KAAK,IAAI,OAAO,GAAG;IAChD,KAAK,IAAI,OAAO;IAChB,KAAK,KAAK,OAAO;GAClB;EACD;EAED,MAAM,KAAK,GAAG,IAAI;EAClB,aAAa;CACd;CAEA,OAAO;AACR"}
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
//#region src/number/bytes.ts
/**
* Convert bytes to megabytes (rounded to nearest integer)
*/
function toMb(sizeInBytes) {
return Math.round(sizeInBytes / (1024 * 1024));
}
/**
* Format bytes to human-readable size with appropriate unit (B, KB, or MB)
*/
function formatBytes(sizeInBytes) {
if (sizeInBytes < 1024) return `${sizeInBytes}B`;
else if (sizeInBytes < 1024 * 1024) return `${Math.round(sizeInBytes / 1024)}KB`;
else return `${Math.round(sizeInBytes / (1024 * 1024))}MB`;
}
//#endregion
exports.formatBytes = formatBytes;
exports.toMb = toMb;
//# sourceMappingURL=bytes.cjs.map
{"version":3,"file":"bytes.cjs","names":[],"sources":["../../src/number/bytes.ts"],"sourcesContent":["/**\n * Convert bytes to megabytes (rounded to nearest integer)\n */\nexport function toMb(sizeInBytes: number): number {\n\treturn Math.round(sizeInBytes / (1024 * 1024));\n}\n\n/**\n * Format bytes to human-readable size with appropriate unit (B, KB, or MB)\n */\nexport function formatBytes(sizeInBytes: number): string {\n\tif (sizeInBytes < 1024) {\n\t\treturn `${sizeInBytes}B`;\n\t} else if (sizeInBytes < 1024 * 1024) {\n\t\treturn `${Math.round(sizeInBytes / 1024)}KB`;\n\t} else {\n\t\treturn `${Math.round(sizeInBytes / (1024 * 1024))}MB`;\n\t}\n}\n"],"mappings":";;;;;AAGA,SAAgB,KAAK,aAA6B;CACjD,OAAO,KAAK,MAAM,eAAe,OAAO,KAAK;AAC9C;;;;AAKA,SAAgB,YAAY,aAA6B;CACxD,IAAI,cAAc,MACjB,OAAO,GAAG,YAAY;MAChB,IAAI,cAAc,OAAO,MAC/B,OAAO,GAAG,KAAK,MAAM,cAAc,IAAI,EAAE;MAEzC,OAAO,GAAG,KAAK,MAAM,eAAe,OAAO,KAAK,EAAE;AAEpD"}
//#region src/number/bytes.d.ts
declare function toMb(sizeInBytes: number): number;
declare function formatBytes(sizeInBytes: number): string;
//#endregion
export { formatBytes, toMb };
//# sourceMappingURL=bytes.d.cts.map
//#region src/number/bytes.d.ts
declare function toMb(sizeInBytes: number): number;
declare function formatBytes(sizeInBytes: number): string;
//#endregion
export { formatBytes, toMb };
//# sourceMappingURL=bytes.d.mts.map
//#region src/number/bytes.ts
/**
* Convert bytes to megabytes (rounded to nearest integer)
*/
function toMb(sizeInBytes) {
return Math.round(sizeInBytes / (1024 * 1024));
}
/**
* Format bytes to human-readable size with appropriate unit (B, KB, or MB)
*/
function formatBytes(sizeInBytes) {
if (sizeInBytes < 1024) return `${sizeInBytes}B`;
else if (sizeInBytes < 1024 * 1024) return `${Math.round(sizeInBytes / 1024)}KB`;
else return `${Math.round(sizeInBytes / (1024 * 1024))}MB`;
}
//#endregion
export { formatBytes, toMb };
//# sourceMappingURL=bytes.mjs.map
{"version":3,"file":"bytes.mjs","names":[],"sources":["../../src/number/bytes.ts"],"sourcesContent":["/**\n * Convert bytes to megabytes (rounded to nearest integer)\n */\nexport function toMb(sizeInBytes: number): number {\n\treturn Math.round(sizeInBytes / (1024 * 1024));\n}\n\n/**\n * Format bytes to human-readable size with appropriate unit (B, KB, or MB)\n */\nexport function formatBytes(sizeInBytes: number): string {\n\tif (sizeInBytes < 1024) {\n\t\treturn `${sizeInBytes}B`;\n\t} else if (sizeInBytes < 1024 * 1024) {\n\t\treturn `${Math.round(sizeInBytes / 1024)}KB`;\n\t} else {\n\t\treturn `${Math.round(sizeInBytes / (1024 * 1024))}MB`;\n\t}\n}\n"],"mappings":";;;;AAGA,SAAgB,KAAK,aAA6B;CACjD,OAAO,KAAK,MAAM,eAAe,OAAO,KAAK;AAC9C;;;;AAKA,SAAgB,YAAY,aAA6B;CACxD,IAAI,cAAc,MACjB,OAAO,GAAG,YAAY;MAChB,IAAI,cAAc,OAAO,MAC/B,OAAO,GAAG,KAAK,MAAM,cAAc,IAAI,EAAE;MAEzC,OAAO,GAAG,KAAK,MAAM,eAAe,OAAO,KAAK,EAAE;AAEpD"}
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const require_scrub_secrets = require("../scrub-secrets.cjs");
//#region src/redaction/pii-patterns.ts
/** Compile a global regex once, adding the `g` flag if the source omits it. */
function globalRegex(source, flags = "") {
return new RegExp(source, flags.includes("g") ? flags : `${flags}g`);
}
/**
* Secret/credential patterns, sourced from {@link SECRET_VALUE_PATTERNS} so
* there is a single place that defines what a credential looks like.
*/
const SECRET_PATTERNS = require_scrub_secrets.SECRET_VALUE_PATTERNS.map((re) => ({
category: "secret",
regex: globalRegex(re.source, re.flags)
}));
/** Luhn checksum — used to keep credit-card redaction from firing on any long digit run. */
function passesLuhn(candidate) {
const digits = candidate.replace(/\D/g, "");
if (digits.length < 13 || digits.length > 19) return false;
let sum = 0;
let double = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = digits.charCodeAt(i) - 48;
if (double) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
double = !double;
}
return sum % 10 === 0;
}
/**
* Confidence gate for phone candidates, encoding the **E.164** standard: a
* leading `+`, a non-zero country code, and 7–15 digits total. Runs on the
* digit/`+`-only normalized form (separators stripped).
*/
function passesE164(candidate) {
return /^\+[1-9]\d{6,14}$/.test(candidate.replace(/[^\d+]/g, ""));
}
/**
* IBAN mod-97 checksum (ISO 13616): drop spaces, move the first 4 chars to the
* end, map letters A–Z → 10–35, and confirm the big-integer value mod 97 === 1.
*/
function passesIbanChecksum(candidate) {
const compact = candidate.replace(/\s/g, "").toUpperCase();
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(compact)) return false;
const rearranged = compact.slice(4) + compact.slice(0, 4);
let remainder = 0;
for (let i = 0; i < rearranged.length; i++) {
const code = rearranged.charCodeAt(i);
const value = code >= 65 ? code - 55 : code - 48;
remainder = value > 9 ? (remainder * 100 + value) % 97 : (remainder * 10 + value) % 97;
}
return remainder === 1;
}
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function base58Decode(input) {
const bytes = [];
for (let i = 0; i < input.length; i++) {
let carry = BASE58_ALPHABET.indexOf(input[i]);
if (carry === -1) return void 0;
for (let j = 0; j < bytes.length; j++) {
carry += bytes[j] * 58;
bytes[j] = carry & 255;
carry >>= 8;
}
while (carry > 0) {
bytes.push(carry & 255);
carry >>= 8;
}
}
for (let i = 0; i < input.length && input[i] === "1"; i++) bytes.push(0);
return Uint8Array.from(bytes.reverse());
}
/**
* Ethereum (`0x`+40 hex) or Bitcoin bech32 (`bc1`/`tb1`) — both distinctive
* enough to accept on shape alone.
*/
function isDistinctiveWalletShape(match) {
if (/^0x[0-9a-fA-F]{40}$/.test(match)) return true;
return /^(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}$/.test(match);
}
/**
* Default legacy-address gate: a Base58Check payload decodes to exactly 25
* bytes (1 version + 20 hash + 4 checksum). Verifying the checksum itself needs
* SHA-256, which has no synchronous cross-platform primitive — Node callers
* inject the stricter check via `createPiiPatterns`. Erring toward redaction is
* the safe direction: an unvalidated Base58 blob of that length is far more
* likely to be a credential than prose.
*/
function isLegacyWalletShape(match) {
return base58Decode(match)?.length === 25;
}
/** Ethereum, Bitcoin bech32, or a legacy Base58 address of plausible length. */
function isCryptoWalletShape(match) {
return isDistinctiveWalletShape(match) || isLegacyWalletShape(match);
}
/**
* Conservative, high-confidence PII patterns. Phone detection is best-effort:
* only well-structured (E.164) formats are matched. New {@link PiiDetectionType}
* categories slot in here; a category may map to `undefined` to declare it
* before a pattern exists, in which case it is excluded from detection.
*
* `overrides` swaps individual entries — used by `@n8n/agents` to layer its
* Node-only Base58Check validator onto `crypto-wallet`.
*/
function createPiiPatterns(overrides = {}) {
return {
email: {
category: "email",
regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g
},
"credit-card": {
category: "credit-card",
regex: /\b\d(?:[ -]?\d){12,18}\b/g,
validate: passesLuhn
},
"ssn-us": {
category: "ssn-us",
regex: /\b\d{3}-\d{2}-\d{4}\b/g
},
phone: {
category: "phone",
regex: /\+\d(?:[\s().-]*\d){6,14}\b/g,
validate: passesE164
},
iban: {
category: "iban",
regex: /\b[A-Za-z]{2}\d{2}[A-Za-z0-9]{11,30}\b|\b[A-Z]{2}\d{2}(?: [A-Z0-9]{1,4}){2,8}\b/g,
validate: passesIbanChecksum
},
"crypto-wallet": {
category: "crypto-wallet",
regex: /\b(?:0x[0-9a-fA-F]{40}|(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}|[13][1-9A-HJ-NP-Za-km-z]{25,34})\b/g,
validate: isCryptoWalletShape
},
mac: {
category: "mac",
regex: /\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g
},
ip: {
category: "ip",
regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b|\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}\b|\b(?:[A-Fa-f0-9]{1,4}:){1,7}:(?:[A-Fa-f0-9]{1,4})?\b/g,
validate: isIpAddress
},
url: {
category: "url",
regex: /\bhttps?:\/\/[^\s<>"')\]}]+/g
},
...overrides
};
}
/** IPv4 with octets ≤ 255, or a colon-delimited IPv6 (shape already constrained by the regex). */
function isIpAddress(match) {
if (match.includes(":")) return true;
const octets = match.split(".");
return octets.length === 4 && octets.every((o) => /^\d{1,3}$/.test(o) && Number(o) <= 255);
}
/** Browser-safe default table. Node callers layer stricter validators on top. */
const PII_PATTERNS = createPiiPatterns();
/**
* PII categories that actually have a detection pattern today — the source of
* truth for what redaction can detect. Any {@link PiiDetectionType} mapped to
* `undefined` in the table (declared but not yet implemented) is excluded here.
*/
const SUPPORTED_PII_CATEGORIES = Object.keys(PII_PATTERNS).filter((type) => PII_PATTERNS[type] !== void 0);
/** Resolve the active pattern set for the given options. */
function resolvePatterns(opts, piiPatterns = PII_PATTERNS) {
const patterns = [];
if (opts.secrets) patterns.push(...SECRET_PATTERNS);
for (const type of opts.detect) {
const pattern = piiPatterns[type];
if (pattern) patterns.push(pattern);
}
return patterns;
}
//#endregion
exports.PII_PATTERNS = PII_PATTERNS;
exports.SUPPORTED_PII_CATEGORIES = SUPPORTED_PII_CATEGORIES;
exports.base58Decode = base58Decode;
exports.createPiiPatterns = createPiiPatterns;
exports.isCryptoWalletShape = isCryptoWalletShape;
exports.passesIbanChecksum = passesIbanChecksum;
exports.passesLuhn = passesLuhn;
exports.resolvePatterns = resolvePatterns;
//# sourceMappingURL=pii-patterns.cjs.map
{"version":3,"file":"pii-patterns.cjs","names":["SECRET_VALUE_PATTERNS"],"sources":["../../src/redaction/pii-patterns.ts"],"sourcesContent":["import { SECRET_VALUE_PATTERNS } from '../scrub-secrets';\n\n/**\n * PII categories the detection vocabulary knows about. A category may be\n * declared here before a pattern exists for it — see {@link PII_PATTERNS}.\n */\nexport type PiiDetectionType =\n\t| 'email'\n\t| 'phone'\n\t| 'credit-card'\n\t| 'ssn-us'\n\t| 'iban'\n\t| 'crypto-wallet'\n\t| 'ip'\n\t| 'mac'\n\t| 'url';\n\n/**\n * A category attached to every redaction match so callers can log *what kind*\n * of sensitive content was removed without ever handling the value itself.\n * `'secret'` covers credential/token patterns; the rest mirror\n * {@link PiiDetectionType}.\n */\nexport type RedactionCategory = 'secret' | PiiDetectionType;\n\nexport interface RedactionPattern {\n\treadonly category: RedactionCategory;\n\t/**\n\t * Precompiled regex matching the sensitive value. Always global — the\n\t * redactor relies on `g` both for replace-all and for the `exec` scan loop.\n\t * Compiled once at module load; callers reset `lastIndex` before reuse.\n\t */\n\treadonly regex: RegExp;\n\t/**\n\t * Optional gate: a candidate match is only redacted when this returns\n\t * `true`. Used to suppress false positives (e.g. Luhn check for cards).\n\t */\n\treadonly validate?: (match: string) => boolean;\n}\n\nexport type PiiPatternTable = Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;\n\n/** Compile a global regex once, adding the `g` flag if the source omits it. */\nfunction globalRegex(source: string, flags = ''): RegExp {\n\treturn new RegExp(source, flags.includes('g') ? flags : `${flags}g`);\n}\n\n/**\n * Secret/credential patterns, sourced from {@link SECRET_VALUE_PATTERNS} so\n * there is a single place that defines what a credential looks like.\n */\nconst SECRET_PATTERNS: readonly RedactionPattern[] = SECRET_VALUE_PATTERNS.map((re) => ({\n\tcategory: 'secret',\n\tregex: globalRegex(re.source, re.flags),\n}));\n\n/** Luhn checksum — used to keep credit-card redaction from firing on any long digit run. */\nexport function passesLuhn(candidate: string): boolean {\n\tconst digits = candidate.replace(/\\D/g, '');\n\tif (digits.length < 13 || digits.length > 19) return false;\n\n\tlet sum = 0;\n\tlet double = false;\n\tfor (let i = digits.length - 1; i >= 0; i--) {\n\t\tlet digit = digits.charCodeAt(i) - 48;\n\t\tif (double) {\n\t\t\tdigit *= 2;\n\t\t\tif (digit > 9) digit -= 9;\n\t\t}\n\t\tsum += digit;\n\t\tdouble = !double;\n\t}\n\treturn sum % 10 === 0;\n}\n\n/**\n * Confidence gate for phone candidates, encoding the **E.164** standard: a\n * leading `+`, a non-zero country code, and 7–15 digits total. Runs on the\n * digit/`+`-only normalized form (separators stripped).\n */\nfunction passesE164(candidate: string): boolean {\n\treturn /^\\+[1-9]\\d{6,14}$/.test(candidate.replace(/[^\\d+]/g, ''));\n}\n\n/**\n * IBAN mod-97 checksum (ISO 13616): drop spaces, move the first 4 chars to the\n * end, map letters A–Z → 10–35, and confirm the big-integer value mod 97 === 1.\n */\nexport function passesIbanChecksum(candidate: string): boolean {\n\tconst compact = candidate.replace(/\\s/g, '').toUpperCase();\n\tif (!/^[A-Z]{2}\\d{2}[A-Z0-9]{11,30}$/.test(compact)) return false;\n\n\tconst rearranged = compact.slice(4) + compact.slice(0, 4);\n\tlet remainder = 0;\n\tfor (let i = 0; i < rearranged.length; i++) {\n\t\tconst code = rearranged.charCodeAt(i);\n\t\tconst value = code >= 65 ? code - 55 : code - 48; // 'A'→10 … 'Z'→35, '0'→0 … '9'→9\n\t\tremainder = value > 9 ? (remainder * 100 + value) % 97 : (remainder * 10 + value) % 97;\n\t}\n\treturn remainder === 1;\n}\n\nconst BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';\n\nexport function base58Decode(input: string): Uint8Array | undefined {\n\tconst bytes: number[] = [];\n\tfor (let i = 0; i < input.length; i++) {\n\t\tlet carry = BASE58_ALPHABET.indexOf(input[i]);\n\t\tif (carry === -1) return undefined;\n\t\tfor (let j = 0; j < bytes.length; j++) {\n\t\t\tcarry += bytes[j] * 58;\n\t\t\tbytes[j] = carry & 0xff;\n\t\t\tcarry >>= 8;\n\t\t}\n\t\twhile (carry > 0) {\n\t\t\tbytes.push(carry & 0xff);\n\t\t\tcarry >>= 8;\n\t\t}\n\t}\n\tfor (let i = 0; i < input.length && input[i] === '1'; i++) bytes.push(0);\n\treturn Uint8Array.from(bytes.reverse());\n}\n\n/**\n * Ethereum (`0x`+40 hex) or Bitcoin bech32 (`bc1`/`tb1`) — both distinctive\n * enough to accept on shape alone.\n */\nfunction isDistinctiveWalletShape(match: string): boolean {\n\tif (/^0x[0-9a-fA-F]{40}$/.test(match)) return true;\n\treturn /^(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}$/.test(match);\n}\n\n/**\n * Default legacy-address gate: a Base58Check payload decodes to exactly 25\n * bytes (1 version + 20 hash + 4 checksum). Verifying the checksum itself needs\n * SHA-256, which has no synchronous cross-platform primitive — Node callers\n * inject the stricter check via `createPiiPatterns`. Erring toward redaction is\n * the safe direction: an unvalidated Base58 blob of that length is far more\n * likely to be a credential than prose.\n */\nfunction isLegacyWalletShape(match: string): boolean {\n\treturn base58Decode(match)?.length === 25;\n}\n\n/** Ethereum, Bitcoin bech32, or a legacy Base58 address of plausible length. */\nexport function isCryptoWalletShape(match: string): boolean {\n\treturn isDistinctiveWalletShape(match) || isLegacyWalletShape(match);\n}\n\n/**\n * Conservative, high-confidence PII patterns. Phone detection is best-effort:\n * only well-structured (E.164) formats are matched. New {@link PiiDetectionType}\n * categories slot in here; a category may map to `undefined` to declare it\n * before a pattern exists, in which case it is excluded from detection.\n *\n * `overrides` swaps individual entries — used by `@n8n/agents` to layer its\n * Node-only Base58Check validator onto `crypto-wallet`.\n */\nexport function createPiiPatterns(\n\toverrides: Partial<Record<PiiDetectionType, RedactionPattern>> = {},\n): PiiPatternTable {\n\t/* eslint-disable @typescript-eslint/naming-convention -- category ids are the\n\t public `PiiDetectionType` vocabulary, which is kebab-case */\n\treturn {\n\t\temail: {\n\t\t\tcategory: 'email',\n\t\t\tregex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g,\n\t\t},\n\t\t'credit-card': {\n\t\t\tcategory: 'credit-card',\n\t\t\t// 13-19 digits, optionally grouped by single spaces or dashes.\n\t\t\tregex: /\\b\\d(?:[ -]?\\d){12,18}\\b/g,\n\t\t\tvalidate: passesLuhn,\n\t\t},\n\t\t'ssn-us': {\n\t\t\tcategory: 'ssn-us',\n\t\t\t// US Social Security Number, dashed form only (123-45-6789). Bare 9-digit\n\t\t\t// runs are intentionally not matched (too false-positive-prone). Per-country\n\t\t\t// national IDs each get their own `ssn-<cc>` category (e.g. a future `ssn-uk`).\n\t\t\tregex: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,\n\t\t},\n\t\tphone: {\n\t\t\tcategory: 'phone',\n\t\t\t// Best-effort, E.164 only: a leading `+` then 7–15 digits, tolerating\n\t\t\t// the spaces/parens/dots/dashes people write between groups\n\t\t\t// (e.g. `+1 (555) 123-4567`). Requiring the `+` keeps false positives\n\t\t\t// low — bare digit runs (IDs, dates, NANP without `+`) are not matched.\n\t\t\tregex: /\\+\\d(?:[\\s().-]*\\d){6,14}\\b/g,\n\t\t\tvalidate: passesE164,\n\t\t},\n\t\tiban: {\n\t\t\tcategory: 'iban',\n\t\t\t// Two forms: the compact (un-spaced) IBAN is matched case-insensitively so\n\t\t\t// lower/mixed-case IBANs are caught — with no internal spaces it can't bleed\n\t\t\t// into a following word. The spaced, group-of-4 form is matched upper-case\n\t\t\t// only: spaced IBANs are written upper-case by convention, and that keeps the\n\t\t\t// greedy body from swallowing following lower-case prose (which would fail the\n\t\t\t// checksum and suppress redaction, since the engine doesn't retry sub-matches).\n\t\t\t// `passesIbanChecksum` upper-cases, strips spaces, and verifies mod-97.\n\t\t\tregex: /\\b[A-Za-z]{2}\\d{2}[A-Za-z0-9]{11,30}\\b|\\b[A-Z]{2}\\d{2}(?: [A-Z0-9]{1,4}){2,8}\\b/g,\n\t\t\tvalidate: passesIbanChecksum,\n\t\t},\n\t\t'crypto-wallet': {\n\t\t\tcategory: 'crypto-wallet',\n\t\t\t// Ethereum `0x…40hex`, Bitcoin bech32 `bc1…`/`tb1…`, or Bitcoin Base58Check.\n\t\t\tregex:\n\t\t\t\t/\\b(?:0x[0-9a-fA-F]{40}|(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}|[13][1-9A-HJ-NP-Za-km-z]{25,34})\\b/g,\n\t\t\tvalidate: isCryptoWalletShape,\n\t\t},\n\t\t// `mac` is declared before `ip`: a MAC is colon-delimited hex and would also\n\t\t// match the IPv6 branch, so matching it as `mac` first keeps the category right.\n\t\tmac: {\n\t\t\tcategory: 'mac',\n\t\t\tregex: /\\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\\b/g,\n\t\t},\n\t\tip: {\n\t\t\tcategory: 'ip',\n\t\t\t// IPv4 (octets validated) or IPv6 (full and `::`-compressed forms).\n\t\t\tregex:\n\t\t\t\t/\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b|\\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}\\b|\\b(?:[A-Fa-f0-9]{1,4}:){1,7}:(?:[A-Fa-f0-9]{1,4})?\\b/g,\n\t\t\tvalidate: isIpAddress,\n\t\t},\n\t\turl: {\n\t\t\tcategory: 'url',\n\t\t\t// Whole http(s) URL. Stops at whitespace and common trailing delimiters.\n\t\t\tregex: /\\bhttps?:\\/\\/[^\\s<>\"')\\]}]+/g,\n\t\t},\n\t\t...overrides,\n\t};\n\t/* eslint-enable @typescript-eslint/naming-convention */\n}\n\n/** IPv4 with octets ≤ 255, or a colon-delimited IPv6 (shape already constrained by the regex). */\nfunction isIpAddress(match: string): boolean {\n\tif (match.includes(':')) return true;\n\tconst octets = match.split('.');\n\treturn octets.length === 4 && octets.every((o) => /^\\d{1,3}$/.test(o) && Number(o) <= 255);\n}\n\n/** Browser-safe default table. Node callers layer stricter validators on top. */\nexport const PII_PATTERNS = createPiiPatterns();\n\n/**\n * PII categories that actually have a detection pattern today — the source of\n * truth for what redaction can detect. Any {@link PiiDetectionType} mapped to\n * `undefined` in the table (declared but not yet implemented) is excluded here.\n */\nexport const SUPPORTED_PII_CATEGORIES: PiiDetectionType[] = (\n\tObject.keys(PII_PATTERNS) as PiiDetectionType[]\n).filter((type) => PII_PATTERNS[type] !== undefined);\n\n/** Resolve the active pattern set for the given options. */\nexport function resolvePatterns(\n\topts: {\n\t\tsecrets: boolean;\n\t\tdetect: readonly PiiDetectionType[];\n\t},\n\tpiiPatterns: PiiPatternTable = PII_PATTERNS,\n): RedactionPattern[] {\n\tconst patterns: RedactionPattern[] = [];\n\tif (opts.secrets) patterns.push(...SECRET_PATTERNS);\n\tfor (const type of opts.detect) {\n\t\tconst pattern = piiPatterns[type];\n\t\tif (pattern) patterns.push(pattern);\n\t}\n\treturn patterns;\n}\n"],"mappings":"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"}
//#region src/redaction/pii-patterns.d.ts
type PiiDetectionType = 'email' | 'phone' | 'credit-card' | 'ssn-us' | 'iban' | 'crypto-wallet' | 'ip' | 'mac' | 'url';
type RedactionCategory = 'secret' | PiiDetectionType;
interface RedactionPattern {
readonly category: RedactionCategory;
readonly regex: RegExp;
readonly validate?: (match: string) => boolean;
}
type PiiPatternTable = Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;
declare function passesLuhn(candidate: string): boolean;
declare function passesIbanChecksum(candidate: string): boolean;
declare function base58Decode(input: string): Uint8Array | undefined;
declare function isCryptoWalletShape(match: string): boolean;
declare function createPiiPatterns(overrides?: Partial<Record<PiiDetectionType, RedactionPattern>>): PiiPatternTable;
declare const PII_PATTERNS: Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;
declare const SUPPORTED_PII_CATEGORIES: PiiDetectionType[];
declare function resolvePatterns(opts: {
secrets: boolean;
detect: readonly PiiDetectionType[];
}, piiPatterns?: PiiPatternTable): RedactionPattern[];
//#endregion
export { PII_PATTERNS, PiiDetectionType, PiiPatternTable, RedactionCategory, RedactionPattern, SUPPORTED_PII_CATEGORIES, base58Decode, createPiiPatterns, isCryptoWalletShape, passesIbanChecksum, passesLuhn, resolvePatterns };
//# sourceMappingURL=pii-patterns.d.cts.map
//#region src/redaction/pii-patterns.d.ts
type PiiDetectionType = 'email' | 'phone' | 'credit-card' | 'ssn-us' | 'iban' | 'crypto-wallet' | 'ip' | 'mac' | 'url';
type RedactionCategory = 'secret' | PiiDetectionType;
interface RedactionPattern {
readonly category: RedactionCategory;
readonly regex: RegExp;
readonly validate?: (match: string) => boolean;
}
type PiiPatternTable = Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;
declare function passesLuhn(candidate: string): boolean;
declare function passesIbanChecksum(candidate: string): boolean;
declare function base58Decode(input: string): Uint8Array | undefined;
declare function isCryptoWalletShape(match: string): boolean;
declare function createPiiPatterns(overrides?: Partial<Record<PiiDetectionType, RedactionPattern>>): PiiPatternTable;
declare const PII_PATTERNS: Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;
declare const SUPPORTED_PII_CATEGORIES: PiiDetectionType[];
declare function resolvePatterns(opts: {
secrets: boolean;
detect: readonly PiiDetectionType[];
}, piiPatterns?: PiiPatternTable): RedactionPattern[];
//#endregion
export { PII_PATTERNS, PiiDetectionType, PiiPatternTable, RedactionCategory, RedactionPattern, SUPPORTED_PII_CATEGORIES, base58Decode, createPiiPatterns, isCryptoWalletShape, passesIbanChecksum, passesLuhn, resolvePatterns };
//# sourceMappingURL=pii-patterns.d.mts.map
import { SECRET_VALUE_PATTERNS } from "../scrub-secrets.mjs";
//#region src/redaction/pii-patterns.ts
/** Compile a global regex once, adding the `g` flag if the source omits it. */
function globalRegex(source, flags = "") {
return new RegExp(source, flags.includes("g") ? flags : `${flags}g`);
}
/**
* Secret/credential patterns, sourced from {@link SECRET_VALUE_PATTERNS} so
* there is a single place that defines what a credential looks like.
*/
const SECRET_PATTERNS = SECRET_VALUE_PATTERNS.map((re) => ({
category: "secret",
regex: globalRegex(re.source, re.flags)
}));
/** Luhn checksum — used to keep credit-card redaction from firing on any long digit run. */
function passesLuhn(candidate) {
const digits = candidate.replace(/\D/g, "");
if (digits.length < 13 || digits.length > 19) return false;
let sum = 0;
let double = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = digits.charCodeAt(i) - 48;
if (double) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
double = !double;
}
return sum % 10 === 0;
}
/**
* Confidence gate for phone candidates, encoding the **E.164** standard: a
* leading `+`, a non-zero country code, and 7–15 digits total. Runs on the
* digit/`+`-only normalized form (separators stripped).
*/
function passesE164(candidate) {
return /^\+[1-9]\d{6,14}$/.test(candidate.replace(/[^\d+]/g, ""));
}
/**
* IBAN mod-97 checksum (ISO 13616): drop spaces, move the first 4 chars to the
* end, map letters A–Z → 10–35, and confirm the big-integer value mod 97 === 1.
*/
function passesIbanChecksum(candidate) {
const compact = candidate.replace(/\s/g, "").toUpperCase();
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(compact)) return false;
const rearranged = compact.slice(4) + compact.slice(0, 4);
let remainder = 0;
for (let i = 0; i < rearranged.length; i++) {
const code = rearranged.charCodeAt(i);
const value = code >= 65 ? code - 55 : code - 48;
remainder = value > 9 ? (remainder * 100 + value) % 97 : (remainder * 10 + value) % 97;
}
return remainder === 1;
}
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function base58Decode(input) {
const bytes = [];
for (let i = 0; i < input.length; i++) {
let carry = BASE58_ALPHABET.indexOf(input[i]);
if (carry === -1) return void 0;
for (let j = 0; j < bytes.length; j++) {
carry += bytes[j] * 58;
bytes[j] = carry & 255;
carry >>= 8;
}
while (carry > 0) {
bytes.push(carry & 255);
carry >>= 8;
}
}
for (let i = 0; i < input.length && input[i] === "1"; i++) bytes.push(0);
return Uint8Array.from(bytes.reverse());
}
/**
* Ethereum (`0x`+40 hex) or Bitcoin bech32 (`bc1`/`tb1`) — both distinctive
* enough to accept on shape alone.
*/
function isDistinctiveWalletShape(match) {
if (/^0x[0-9a-fA-F]{40}$/.test(match)) return true;
return /^(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}$/.test(match);
}
/**
* Default legacy-address gate: a Base58Check payload decodes to exactly 25
* bytes (1 version + 20 hash + 4 checksum). Verifying the checksum itself needs
* SHA-256, which has no synchronous cross-platform primitive — Node callers
* inject the stricter check via `createPiiPatterns`. Erring toward redaction is
* the safe direction: an unvalidated Base58 blob of that length is far more
* likely to be a credential than prose.
*/
function isLegacyWalletShape(match) {
return base58Decode(match)?.length === 25;
}
/** Ethereum, Bitcoin bech32, or a legacy Base58 address of plausible length. */
function isCryptoWalletShape(match) {
return isDistinctiveWalletShape(match) || isLegacyWalletShape(match);
}
/**
* Conservative, high-confidence PII patterns. Phone detection is best-effort:
* only well-structured (E.164) formats are matched. New {@link PiiDetectionType}
* categories slot in here; a category may map to `undefined` to declare it
* before a pattern exists, in which case it is excluded from detection.
*
* `overrides` swaps individual entries — used by `@n8n/agents` to layer its
* Node-only Base58Check validator onto `crypto-wallet`.
*/
function createPiiPatterns(overrides = {}) {
return {
email: {
category: "email",
regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g
},
"credit-card": {
category: "credit-card",
regex: /\b\d(?:[ -]?\d){12,18}\b/g,
validate: passesLuhn
},
"ssn-us": {
category: "ssn-us",
regex: /\b\d{3}-\d{2}-\d{4}\b/g
},
phone: {
category: "phone",
regex: /\+\d(?:[\s().-]*\d){6,14}\b/g,
validate: passesE164
},
iban: {
category: "iban",
regex: /\b[A-Za-z]{2}\d{2}[A-Za-z0-9]{11,30}\b|\b[A-Z]{2}\d{2}(?: [A-Z0-9]{1,4}){2,8}\b/g,
validate: passesIbanChecksum
},
"crypto-wallet": {
category: "crypto-wallet",
regex: /\b(?:0x[0-9a-fA-F]{40}|(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}|[13][1-9A-HJ-NP-Za-km-z]{25,34})\b/g,
validate: isCryptoWalletShape
},
mac: {
category: "mac",
regex: /\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g
},
ip: {
category: "ip",
regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b|\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}\b|\b(?:[A-Fa-f0-9]{1,4}:){1,7}:(?:[A-Fa-f0-9]{1,4})?\b/g,
validate: isIpAddress
},
url: {
category: "url",
regex: /\bhttps?:\/\/[^\s<>"')\]}]+/g
},
...overrides
};
}
/** IPv4 with octets ≤ 255, or a colon-delimited IPv6 (shape already constrained by the regex). */
function isIpAddress(match) {
if (match.includes(":")) return true;
const octets = match.split(".");
return octets.length === 4 && octets.every((o) => /^\d{1,3}$/.test(o) && Number(o) <= 255);
}
/** Browser-safe default table. Node callers layer stricter validators on top. */
const PII_PATTERNS = createPiiPatterns();
/**
* PII categories that actually have a detection pattern today — the source of
* truth for what redaction can detect. Any {@link PiiDetectionType} mapped to
* `undefined` in the table (declared but not yet implemented) is excluded here.
*/
const SUPPORTED_PII_CATEGORIES = Object.keys(PII_PATTERNS).filter((type) => PII_PATTERNS[type] !== void 0);
/** Resolve the active pattern set for the given options. */
function resolvePatterns(opts, piiPatterns = PII_PATTERNS) {
const patterns = [];
if (opts.secrets) patterns.push(...SECRET_PATTERNS);
for (const type of opts.detect) {
const pattern = piiPatterns[type];
if (pattern) patterns.push(pattern);
}
return patterns;
}
//#endregion
export { PII_PATTERNS, SUPPORTED_PII_CATEGORIES, base58Decode, createPiiPatterns, isCryptoWalletShape, passesIbanChecksum, passesLuhn, resolvePatterns };
//# sourceMappingURL=pii-patterns.mjs.map
{"version":3,"file":"pii-patterns.mjs","names":[],"sources":["../../src/redaction/pii-patterns.ts"],"sourcesContent":["import { SECRET_VALUE_PATTERNS } from '../scrub-secrets';\n\n/**\n * PII categories the detection vocabulary knows about. A category may be\n * declared here before a pattern exists for it — see {@link PII_PATTERNS}.\n */\nexport type PiiDetectionType =\n\t| 'email'\n\t| 'phone'\n\t| 'credit-card'\n\t| 'ssn-us'\n\t| 'iban'\n\t| 'crypto-wallet'\n\t| 'ip'\n\t| 'mac'\n\t| 'url';\n\n/**\n * A category attached to every redaction match so callers can log *what kind*\n * of sensitive content was removed without ever handling the value itself.\n * `'secret'` covers credential/token patterns; the rest mirror\n * {@link PiiDetectionType}.\n */\nexport type RedactionCategory = 'secret' | PiiDetectionType;\n\nexport interface RedactionPattern {\n\treadonly category: RedactionCategory;\n\t/**\n\t * Precompiled regex matching the sensitive value. Always global — the\n\t * redactor relies on `g` both for replace-all and for the `exec` scan loop.\n\t * Compiled once at module load; callers reset `lastIndex` before reuse.\n\t */\n\treadonly regex: RegExp;\n\t/**\n\t * Optional gate: a candidate match is only redacted when this returns\n\t * `true`. Used to suppress false positives (e.g. Luhn check for cards).\n\t */\n\treadonly validate?: (match: string) => boolean;\n}\n\nexport type PiiPatternTable = Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;\n\n/** Compile a global regex once, adding the `g` flag if the source omits it. */\nfunction globalRegex(source: string, flags = ''): RegExp {\n\treturn new RegExp(source, flags.includes('g') ? flags : `${flags}g`);\n}\n\n/**\n * Secret/credential patterns, sourced from {@link SECRET_VALUE_PATTERNS} so\n * there is a single place that defines what a credential looks like.\n */\nconst SECRET_PATTERNS: readonly RedactionPattern[] = SECRET_VALUE_PATTERNS.map((re) => ({\n\tcategory: 'secret',\n\tregex: globalRegex(re.source, re.flags),\n}));\n\n/** Luhn checksum — used to keep credit-card redaction from firing on any long digit run. */\nexport function passesLuhn(candidate: string): boolean {\n\tconst digits = candidate.replace(/\\D/g, '');\n\tif (digits.length < 13 || digits.length > 19) return false;\n\n\tlet sum = 0;\n\tlet double = false;\n\tfor (let i = digits.length - 1; i >= 0; i--) {\n\t\tlet digit = digits.charCodeAt(i) - 48;\n\t\tif (double) {\n\t\t\tdigit *= 2;\n\t\t\tif (digit > 9) digit -= 9;\n\t\t}\n\t\tsum += digit;\n\t\tdouble = !double;\n\t}\n\treturn sum % 10 === 0;\n}\n\n/**\n * Confidence gate for phone candidates, encoding the **E.164** standard: a\n * leading `+`, a non-zero country code, and 7–15 digits total. Runs on the\n * digit/`+`-only normalized form (separators stripped).\n */\nfunction passesE164(candidate: string): boolean {\n\treturn /^\\+[1-9]\\d{6,14}$/.test(candidate.replace(/[^\\d+]/g, ''));\n}\n\n/**\n * IBAN mod-97 checksum (ISO 13616): drop spaces, move the first 4 chars to the\n * end, map letters A–Z → 10–35, and confirm the big-integer value mod 97 === 1.\n */\nexport function passesIbanChecksum(candidate: string): boolean {\n\tconst compact = candidate.replace(/\\s/g, '').toUpperCase();\n\tif (!/^[A-Z]{2}\\d{2}[A-Z0-9]{11,30}$/.test(compact)) return false;\n\n\tconst rearranged = compact.slice(4) + compact.slice(0, 4);\n\tlet remainder = 0;\n\tfor (let i = 0; i < rearranged.length; i++) {\n\t\tconst code = rearranged.charCodeAt(i);\n\t\tconst value = code >= 65 ? code - 55 : code - 48; // 'A'→10 … 'Z'→35, '0'→0 … '9'→9\n\t\tremainder = value > 9 ? (remainder * 100 + value) % 97 : (remainder * 10 + value) % 97;\n\t}\n\treturn remainder === 1;\n}\n\nconst BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';\n\nexport function base58Decode(input: string): Uint8Array | undefined {\n\tconst bytes: number[] = [];\n\tfor (let i = 0; i < input.length; i++) {\n\t\tlet carry = BASE58_ALPHABET.indexOf(input[i]);\n\t\tif (carry === -1) return undefined;\n\t\tfor (let j = 0; j < bytes.length; j++) {\n\t\t\tcarry += bytes[j] * 58;\n\t\t\tbytes[j] = carry & 0xff;\n\t\t\tcarry >>= 8;\n\t\t}\n\t\twhile (carry > 0) {\n\t\t\tbytes.push(carry & 0xff);\n\t\t\tcarry >>= 8;\n\t\t}\n\t}\n\tfor (let i = 0; i < input.length && input[i] === '1'; i++) bytes.push(0);\n\treturn Uint8Array.from(bytes.reverse());\n}\n\n/**\n * Ethereum (`0x`+40 hex) or Bitcoin bech32 (`bc1`/`tb1`) — both distinctive\n * enough to accept on shape alone.\n */\nfunction isDistinctiveWalletShape(match: string): boolean {\n\tif (/^0x[0-9a-fA-F]{40}$/.test(match)) return true;\n\treturn /^(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}$/.test(match);\n}\n\n/**\n * Default legacy-address gate: a Base58Check payload decodes to exactly 25\n * bytes (1 version + 20 hash + 4 checksum). Verifying the checksum itself needs\n * SHA-256, which has no synchronous cross-platform primitive — Node callers\n * inject the stricter check via `createPiiPatterns`. Erring toward redaction is\n * the safe direction: an unvalidated Base58 blob of that length is far more\n * likely to be a credential than prose.\n */\nfunction isLegacyWalletShape(match: string): boolean {\n\treturn base58Decode(match)?.length === 25;\n}\n\n/** Ethereum, Bitcoin bech32, or a legacy Base58 address of plausible length. */\nexport function isCryptoWalletShape(match: string): boolean {\n\treturn isDistinctiveWalletShape(match) || isLegacyWalletShape(match);\n}\n\n/**\n * Conservative, high-confidence PII patterns. Phone detection is best-effort:\n * only well-structured (E.164) formats are matched. New {@link PiiDetectionType}\n * categories slot in here; a category may map to `undefined` to declare it\n * before a pattern exists, in which case it is excluded from detection.\n *\n * `overrides` swaps individual entries — used by `@n8n/agents` to layer its\n * Node-only Base58Check validator onto `crypto-wallet`.\n */\nexport function createPiiPatterns(\n\toverrides: Partial<Record<PiiDetectionType, RedactionPattern>> = {},\n): PiiPatternTable {\n\t/* eslint-disable @typescript-eslint/naming-convention -- category ids are the\n\t public `PiiDetectionType` vocabulary, which is kebab-case */\n\treturn {\n\t\temail: {\n\t\t\tcategory: 'email',\n\t\t\tregex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g,\n\t\t},\n\t\t'credit-card': {\n\t\t\tcategory: 'credit-card',\n\t\t\t// 13-19 digits, optionally grouped by single spaces or dashes.\n\t\t\tregex: /\\b\\d(?:[ -]?\\d){12,18}\\b/g,\n\t\t\tvalidate: passesLuhn,\n\t\t},\n\t\t'ssn-us': {\n\t\t\tcategory: 'ssn-us',\n\t\t\t// US Social Security Number, dashed form only (123-45-6789). Bare 9-digit\n\t\t\t// runs are intentionally not matched (too false-positive-prone). Per-country\n\t\t\t// national IDs each get their own `ssn-<cc>` category (e.g. a future `ssn-uk`).\n\t\t\tregex: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,\n\t\t},\n\t\tphone: {\n\t\t\tcategory: 'phone',\n\t\t\t// Best-effort, E.164 only: a leading `+` then 7–15 digits, tolerating\n\t\t\t// the spaces/parens/dots/dashes people write between groups\n\t\t\t// (e.g. `+1 (555) 123-4567`). Requiring the `+` keeps false positives\n\t\t\t// low — bare digit runs (IDs, dates, NANP without `+`) are not matched.\n\t\t\tregex: /\\+\\d(?:[\\s().-]*\\d){6,14}\\b/g,\n\t\t\tvalidate: passesE164,\n\t\t},\n\t\tiban: {\n\t\t\tcategory: 'iban',\n\t\t\t// Two forms: the compact (un-spaced) IBAN is matched case-insensitively so\n\t\t\t// lower/mixed-case IBANs are caught — with no internal spaces it can't bleed\n\t\t\t// into a following word. The spaced, group-of-4 form is matched upper-case\n\t\t\t// only: spaced IBANs are written upper-case by convention, and that keeps the\n\t\t\t// greedy body from swallowing following lower-case prose (which would fail the\n\t\t\t// checksum and suppress redaction, since the engine doesn't retry sub-matches).\n\t\t\t// `passesIbanChecksum` upper-cases, strips spaces, and verifies mod-97.\n\t\t\tregex: /\\b[A-Za-z]{2}\\d{2}[A-Za-z0-9]{11,30}\\b|\\b[A-Z]{2}\\d{2}(?: [A-Z0-9]{1,4}){2,8}\\b/g,\n\t\t\tvalidate: passesIbanChecksum,\n\t\t},\n\t\t'crypto-wallet': {\n\t\t\tcategory: 'crypto-wallet',\n\t\t\t// Ethereum `0x…40hex`, Bitcoin bech32 `bc1…`/`tb1…`, or Bitcoin Base58Check.\n\t\t\tregex:\n\t\t\t\t/\\b(?:0x[0-9a-fA-F]{40}|(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}|[13][1-9A-HJ-NP-Za-km-z]{25,34})\\b/g,\n\t\t\tvalidate: isCryptoWalletShape,\n\t\t},\n\t\t// `mac` is declared before `ip`: a MAC is colon-delimited hex and would also\n\t\t// match the IPv6 branch, so matching it as `mac` first keeps the category right.\n\t\tmac: {\n\t\t\tcategory: 'mac',\n\t\t\tregex: /\\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\\b/g,\n\t\t},\n\t\tip: {\n\t\t\tcategory: 'ip',\n\t\t\t// IPv4 (octets validated) or IPv6 (full and `::`-compressed forms).\n\t\t\tregex:\n\t\t\t\t/\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b|\\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}\\b|\\b(?:[A-Fa-f0-9]{1,4}:){1,7}:(?:[A-Fa-f0-9]{1,4})?\\b/g,\n\t\t\tvalidate: isIpAddress,\n\t\t},\n\t\turl: {\n\t\t\tcategory: 'url',\n\t\t\t// Whole http(s) URL. Stops at whitespace and common trailing delimiters.\n\t\t\tregex: /\\bhttps?:\\/\\/[^\\s<>\"')\\]}]+/g,\n\t\t},\n\t\t...overrides,\n\t};\n\t/* eslint-enable @typescript-eslint/naming-convention */\n}\n\n/** IPv4 with octets ≤ 255, or a colon-delimited IPv6 (shape already constrained by the regex). */\nfunction isIpAddress(match: string): boolean {\n\tif (match.includes(':')) return true;\n\tconst octets = match.split('.');\n\treturn octets.length === 4 && octets.every((o) => /^\\d{1,3}$/.test(o) && Number(o) <= 255);\n}\n\n/** Browser-safe default table. Node callers layer stricter validators on top. */\nexport const PII_PATTERNS = createPiiPatterns();\n\n/**\n * PII categories that actually have a detection pattern today — the source of\n * truth for what redaction can detect. Any {@link PiiDetectionType} mapped to\n * `undefined` in the table (declared but not yet implemented) is excluded here.\n */\nexport const SUPPORTED_PII_CATEGORIES: PiiDetectionType[] = (\n\tObject.keys(PII_PATTERNS) as PiiDetectionType[]\n).filter((type) => PII_PATTERNS[type] !== undefined);\n\n/** Resolve the active pattern set for the given options. */\nexport function resolvePatterns(\n\topts: {\n\t\tsecrets: boolean;\n\t\tdetect: readonly PiiDetectionType[];\n\t},\n\tpiiPatterns: PiiPatternTable = PII_PATTERNS,\n): RedactionPattern[] {\n\tconst patterns: RedactionPattern[] = [];\n\tif (opts.secrets) patterns.push(...SECRET_PATTERNS);\n\tfor (const type of opts.detect) {\n\t\tconst pattern = piiPatterns[type];\n\t\tif (pattern) patterns.push(pattern);\n\t}\n\treturn patterns;\n}\n"],"mappings":"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"}
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const require_redaction_pii_patterns = require("./pii-patterns.cjs");
//#region src/redaction/redact-text.ts
const DEFAULT_PLACEHOLDER = "[REDACTED]";
/**
* Redact secret/PII patterns from a complete string. Pure and idempotent —
* already-redacted placeholders are left untouched by the underlying patterns.
*/
function redactText(input, opts = {}) {
const placeholder = opts.placeholder ?? "[REDACTED]";
const patterns = require_redaction_pii_patterns.resolvePatterns({
secrets: opts.secrets ?? true,
detect: opts.detect ?? []
}, opts.piiPatterns);
const ordered = opts.preserveUrlStructure ? [...patterns.filter((pattern) => pattern.category === "url"), ...patterns.filter((pattern) => pattern.category !== "url")] : patterns;
const matches = [];
let text = input;
for (const pattern of ordered) text = text.replace(pattern.regex, (match) => {
if (pattern.validate && !pattern.validate(match)) return match;
if (opts.preserveUrlStructure && pattern.category === "url") {
const rebuilt = stripUrlSensitiveParts(match, placeholder);
if (rebuilt !== match) matches.push({ category: pattern.category });
return rebuilt;
}
matches.push({ category: pattern.category });
return placeholder;
});
return {
text,
matches
};
}
/** True for a path segment that looks like an embedded token — webhook-style
* services (Slack/Discord/Telegram, …) carry their secret as a path segment.
* Shape-based on purpose: per-service URL grammars don't scale across hundreds
* of integrations. Conservative: words, readable slugs and digit-only ids are
* kept. */
function isTokenLikeSegment(segment) {
if (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\d/.test(segment)) return true;
return segment.length >= 24 && /^[A-Za-z]+$/.test(segment);
}
/** Keep origin + path (token-like segments redacted) + query names; redact
* query values, drop userinfo and fragment. The replacement is URL-safe (no
* `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒
* fully redacted. */
function stripUrlSensitiveParts(match, placeholder) {
const urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, "") || "REDACTED";
try {
const url = new URL(match);
const pathname = url.pathname.split("/").map((segment) => isTokenLikeSegment(segment) ? urlPlaceholder : segment).join("/");
const names = [...url.searchParams.keys()];
const query = names.length > 0 ? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join("&")}` : "";
return `${url.origin}${pathname}${query}`;
} catch {
return placeholder;
}
}
/**
* Find the `[start, end)` ranges of every (validated) match in `input`. Used by
* the streaming redactor to avoid emitting through the middle of a complete
* match that contains internal whitespace (e.g. a spaced credit-card number).
*/
function findMatchRanges(input, opts = {}) {
const patterns = require_redaction_pii_patterns.resolvePatterns({
secrets: opts.secrets ?? true,
detect: opts.detect ?? []
}, opts.piiPatterns);
const ranges = [];
for (const pattern of patterns) {
const { regex } = pattern;
regex.lastIndex = 0;
let match;
while ((match = regex.exec(input)) !== null) {
if (match[0].length === 0) {
regex.lastIndex++;
continue;
}
if (pattern.validate && !pattern.validate(match[0])) continue;
ranges.push([match.index, match.index + match[0].length]);
}
}
return ranges;
}
const MAX_DEEP_DEPTH = 8;
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;
/**
* Recursively redact string values inside an arbitrary JSON-like value
* (tool results, structured payloads). Object keys are left intact; only
* string values are scanned. Recursion is depth-bounded as a cheap guard
* against pathological/cyclic structures.
*/
function redactDeep(value, opts = {}, depth = 0) {
return redactDeepValue(value, opts, depth);
}
function redactDeepValue(value, opts, depth, key) {
if (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) return {
value: opts.placeholder ?? "[REDACTED]",
matches: [{ category: "secret" }]
};
if (typeof value === "string") {
const { text, matches } = redactText(value, opts);
return {
value: text,
matches
};
}
if (depth >= MAX_DEEP_DEPTH) {
if (value !== null && typeof value === "object") return {
value: opts.placeholder ?? "[REDACTED]",
matches: [{ category: "secret" }]
};
return {
value,
matches: []
};
}
if (Array.isArray(value)) {
const matches = [];
return {
value: value.map((item) => {
const result = redactDeepValue(item, opts, depth + 1, key);
matches.push(...result.matches);
return result.value;
}),
matches
};
}
if (value !== null && typeof value === "object") {
const matches = [];
const next = {};
for (const [key, item] of Object.entries(value)) {
const result = redactDeepValue(item, opts, depth + 1, key);
matches.push(...result.matches);
next[key] = result.value;
}
return {
value: next,
matches
};
}
return {
value,
matches: []
};
}
//#endregion
exports.DEFAULT_PLACEHOLDER = DEFAULT_PLACEHOLDER;
exports.findMatchRanges = findMatchRanges;
exports.redactDeep = redactDeep;
exports.redactText = redactText;
//# sourceMappingURL=redact-text.cjs.map
{"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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"}
import { PiiDetectionType, PiiPatternTable, RedactionCategory } from "./pii-patterns.cjs";
//#region src/redaction/redact-text.d.ts
declare const DEFAULT_PLACEHOLDER = "[REDACTED]";
interface RedactionOptions {
secrets?: boolean;
detect?: readonly PiiDetectionType[];
placeholder?: string;
preserveUrlStructure?: boolean;
redactSensitiveKeys?: boolean;
piiPatterns?: PiiPatternTable;
}
interface RedactionResult {
text: string;
matches: Array<{
category: RedactionCategory;
}>;
}
declare function redactText(input: string, opts?: RedactionOptions): RedactionResult;
declare function findMatchRanges(input: string, opts?: RedactionOptions): Array<[number, number]>;
interface DeepRedactionResult {
value: unknown;
matches: Array<{
category: RedactionCategory;
}>;
}
declare function redactDeep(value: unknown, opts?: RedactionOptions, depth?: number): DeepRedactionResult;
//#endregion
export { DEFAULT_PLACEHOLDER, DeepRedactionResult, RedactionOptions, RedactionResult, findMatchRanges, redactDeep, redactText };
//# sourceMappingURL=redact-text.d.cts.map
import { PiiDetectionType, PiiPatternTable, RedactionCategory } from "./pii-patterns.mjs";
//#region src/redaction/redact-text.d.ts
declare const DEFAULT_PLACEHOLDER = "[REDACTED]";
interface RedactionOptions {
secrets?: boolean;
detect?: readonly PiiDetectionType[];
placeholder?: string;
preserveUrlStructure?: boolean;
redactSensitiveKeys?: boolean;
piiPatterns?: PiiPatternTable;
}
interface RedactionResult {
text: string;
matches: Array<{
category: RedactionCategory;
}>;
}
declare function redactText(input: string, opts?: RedactionOptions): RedactionResult;
declare function findMatchRanges(input: string, opts?: RedactionOptions): Array<[number, number]>;
interface DeepRedactionResult {
value: unknown;
matches: Array<{
category: RedactionCategory;
}>;
}
declare function redactDeep(value: unknown, opts?: RedactionOptions, depth?: number): DeepRedactionResult;
//#endregion
export { DEFAULT_PLACEHOLDER, DeepRedactionResult, RedactionOptions, RedactionResult, findMatchRanges, redactDeep, redactText };
//# sourceMappingURL=redact-text.d.mts.map
import { resolvePatterns } from "./pii-patterns.mjs";
//#region src/redaction/redact-text.ts
const DEFAULT_PLACEHOLDER = "[REDACTED]";
/**
* Redact secret/PII patterns from a complete string. Pure and idempotent —
* already-redacted placeholders are left untouched by the underlying patterns.
*/
function redactText(input, opts = {}) {
const placeholder = opts.placeholder ?? "[REDACTED]";
const patterns = resolvePatterns({
secrets: opts.secrets ?? true,
detect: opts.detect ?? []
}, opts.piiPatterns);
const ordered = opts.preserveUrlStructure ? [...patterns.filter((pattern) => pattern.category === "url"), ...patterns.filter((pattern) => pattern.category !== "url")] : patterns;
const matches = [];
let text = input;
for (const pattern of ordered) text = text.replace(pattern.regex, (match) => {
if (pattern.validate && !pattern.validate(match)) return match;
if (opts.preserveUrlStructure && pattern.category === "url") {
const rebuilt = stripUrlSensitiveParts(match, placeholder);
if (rebuilt !== match) matches.push({ category: pattern.category });
return rebuilt;
}
matches.push({ category: pattern.category });
return placeholder;
});
return {
text,
matches
};
}
/** True for a path segment that looks like an embedded token — webhook-style
* services (Slack/Discord/Telegram, …) carry their secret as a path segment.
* Shape-based on purpose: per-service URL grammars don't scale across hundreds
* of integrations. Conservative: words, readable slugs and digit-only ids are
* kept. */
function isTokenLikeSegment(segment) {
if (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\d/.test(segment)) return true;
return segment.length >= 24 && /^[A-Za-z]+$/.test(segment);
}
/** Keep origin + path (token-like segments redacted) + query names; redact
* query values, drop userinfo and fragment. The replacement is URL-safe (no
* `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒
* fully redacted. */
function stripUrlSensitiveParts(match, placeholder) {
const urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, "") || "REDACTED";
try {
const url = new URL(match);
const pathname = url.pathname.split("/").map((segment) => isTokenLikeSegment(segment) ? urlPlaceholder : segment).join("/");
const names = [...url.searchParams.keys()];
const query = names.length > 0 ? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join("&")}` : "";
return `${url.origin}${pathname}${query}`;
} catch {
return placeholder;
}
}
/**
* Find the `[start, end)` ranges of every (validated) match in `input`. Used by
* the streaming redactor to avoid emitting through the middle of a complete
* match that contains internal whitespace (e.g. a spaced credit-card number).
*/
function findMatchRanges(input, opts = {}) {
const patterns = resolvePatterns({
secrets: opts.secrets ?? true,
detect: opts.detect ?? []
}, opts.piiPatterns);
const ranges = [];
for (const pattern of patterns) {
const { regex } = pattern;
regex.lastIndex = 0;
let match;
while ((match = regex.exec(input)) !== null) {
if (match[0].length === 0) {
regex.lastIndex++;
continue;
}
if (pattern.validate && !pattern.validate(match[0])) continue;
ranges.push([match.index, match.index + match[0].length]);
}
}
return ranges;
}
const MAX_DEEP_DEPTH = 8;
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;
/**
* Recursively redact string values inside an arbitrary JSON-like value
* (tool results, structured payloads). Object keys are left intact; only
* string values are scanned. Recursion is depth-bounded as a cheap guard
* against pathological/cyclic structures.
*/
function redactDeep(value, opts = {}, depth = 0) {
return redactDeepValue(value, opts, depth);
}
function redactDeepValue(value, opts, depth, key) {
if (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) return {
value: opts.placeholder ?? "[REDACTED]",
matches: [{ category: "secret" }]
};
if (typeof value === "string") {
const { text, matches } = redactText(value, opts);
return {
value: text,
matches
};
}
if (depth >= MAX_DEEP_DEPTH) {
if (value !== null && typeof value === "object") return {
value: opts.placeholder ?? "[REDACTED]",
matches: [{ category: "secret" }]
};
return {
value,
matches: []
};
}
if (Array.isArray(value)) {
const matches = [];
return {
value: value.map((item) => {
const result = redactDeepValue(item, opts, depth + 1, key);
matches.push(...result.matches);
return result.value;
}),
matches
};
}
if (value !== null && typeof value === "object") {
const matches = [];
const next = {};
for (const [key, item] of Object.entries(value)) {
const result = redactDeepValue(item, opts, depth + 1, key);
matches.push(...result.matches);
next[key] = result.value;
}
return {
value: next,
matches
};
}
return {
value,
matches: []
};
}
//#endregion
export { DEFAULT_PLACEHOLDER, findMatchRanges, redactDeep, redactText };
//# sourceMappingURL=redact-text.mjs.map
{"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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"}
+3
-3
{
"name": "@n8n/utils",
"type": "module",
"version": "1.44.0",
"version": "1.45.0",
"files": [

@@ -31,3 +31,3 @@ "dist",

"nanoid": "3.3.18",
"@n8n/constants": "0.35.0"
"@n8n/constants": "0.36.0"
},

@@ -41,4 +41,4 @@ "devDependencies": {

"vitest": "^4.1.9",
"@n8n/eslint-config": "0.0.1",
"@n8n/typescript-config": "1.10.0",
"@n8n/eslint-config": "0.0.1",
"@n8n/vitest-config": "1.21.0"

@@ -45,0 +45,0 @@ },