@n8n/utils
Advanced tools
| 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 @@ }, |
340892
35.59%182
15.19%2899
35.66%+ Added
- Removed
Updated