@n8n/utils 1.44.0 → 1.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/errors/error-chain.ts
3
+ /** Any non-null object. Unlike is-record.ts this accepts arrays, so a wrapped array stays in the chain. */
4
+ const isObjectLike = (value) => typeof value === "object" && value !== null;
5
+ const MAX_CHAIN_DEPTH = 5;
6
+ /** The keys errors wrap each other under. */
7
+ const WRAPPING_KEYS = [
8
+ "cause",
9
+ "errorResponse",
10
+ "reason"
11
+ ];
12
+ /** The error and the errors it wraps, shallowest first, each visited once. */
13
+ function errorChain(error) {
14
+ if (!isObjectLike(error)) return [];
15
+ const seen = /* @__PURE__ */ new Set([error]);
16
+ const chain = [error];
17
+ let generation = [error];
18
+ for (let depth = 0; depth < MAX_CHAIN_DEPTH && generation.length > 0; depth++) {
19
+ const next = [];
20
+ for (const level of generation) for (const key of WRAPPING_KEYS) {
21
+ const wrapped = level[key];
22
+ if (isObjectLike(wrapped) && !seen.has(wrapped)) {
23
+ seen.add(wrapped);
24
+ next.push(wrapped);
25
+ }
26
+ }
27
+ chain.push(...next);
28
+ generation = next;
29
+ }
30
+ return chain;
31
+ }
32
+ //#endregion
33
+ exports.errorChain = errorChain;
34
+ exports.isObjectLike = isObjectLike;
35
+
36
+ //# sourceMappingURL=error-chain.cjs.map
@@ -0,0 +1 @@
1
+ {"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"}
@@ -0,0 +1,7 @@
1
+ //#region src/errors/error-chain.d.ts
2
+ type UnknownRecord = Readonly<Record<string, unknown>>;
3
+ declare const isObjectLike: (value: unknown) => value is UnknownRecord;
4
+ declare function errorChain(error: unknown): UnknownRecord[];
5
+ //#endregion
6
+ export { UnknownRecord, errorChain, isObjectLike };
7
+ //# sourceMappingURL=error-chain.d.cts.map
@@ -0,0 +1,7 @@
1
+ //#region src/errors/error-chain.d.ts
2
+ type UnknownRecord = Readonly<Record<string, unknown>>;
3
+ declare const isObjectLike: (value: unknown) => value is UnknownRecord;
4
+ declare function errorChain(error: unknown): UnknownRecord[];
5
+ //#endregion
6
+ export { UnknownRecord, errorChain, isObjectLike };
7
+ //# sourceMappingURL=error-chain.d.mts.map
@@ -0,0 +1,34 @@
1
+ //#region src/errors/error-chain.ts
2
+ /** Any non-null object. Unlike is-record.ts this accepts arrays, so a wrapped array stays in the chain. */
3
+ const isObjectLike = (value) => typeof value === "object" && value !== null;
4
+ const MAX_CHAIN_DEPTH = 5;
5
+ /** The keys errors wrap each other under. */
6
+ const WRAPPING_KEYS = [
7
+ "cause",
8
+ "errorResponse",
9
+ "reason"
10
+ ];
11
+ /** The error and the errors it wraps, shallowest first, each visited once. */
12
+ function errorChain(error) {
13
+ if (!isObjectLike(error)) return [];
14
+ const seen = /* @__PURE__ */ new Set([error]);
15
+ const chain = [error];
16
+ let generation = [error];
17
+ for (let depth = 0; depth < MAX_CHAIN_DEPTH && generation.length > 0; depth++) {
18
+ const next = [];
19
+ for (const level of generation) for (const key of WRAPPING_KEYS) {
20
+ const wrapped = level[key];
21
+ if (isObjectLike(wrapped) && !seen.has(wrapped)) {
22
+ seen.add(wrapped);
23
+ next.push(wrapped);
24
+ }
25
+ }
26
+ chain.push(...next);
27
+ generation = next;
28
+ }
29
+ return chain;
30
+ }
31
+ //#endregion
32
+ export { errorChain, isObjectLike };
33
+
34
+ //# sourceMappingURL=error-chain.mjs.map
@@ -0,0 +1 @@
1
+ {"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"}
@@ -0,0 +1,21 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/number/bytes.ts
3
+ /**
4
+ * Convert bytes to megabytes (rounded to nearest integer)
5
+ */
6
+ function toMb(sizeInBytes) {
7
+ return Math.round(sizeInBytes / (1024 * 1024));
8
+ }
9
+ /**
10
+ * Format bytes to human-readable size with appropriate unit (B, KB, or MB)
11
+ */
12
+ function formatBytes(sizeInBytes) {
13
+ if (sizeInBytes < 1024) return `${sizeInBytes}B`;
14
+ else if (sizeInBytes < 1024 * 1024) return `${Math.round(sizeInBytes / 1024)}KB`;
15
+ else return `${Math.round(sizeInBytes / (1024 * 1024))}MB`;
16
+ }
17
+ //#endregion
18
+ exports.formatBytes = formatBytes;
19
+ exports.toMb = toMb;
20
+
21
+ //# sourceMappingURL=bytes.cjs.map
@@ -0,0 +1 @@
1
+ {"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"}
@@ -0,0 +1,6 @@
1
+ //#region src/number/bytes.d.ts
2
+ declare function toMb(sizeInBytes: number): number;
3
+ declare function formatBytes(sizeInBytes: number): string;
4
+ //#endregion
5
+ export { formatBytes, toMb };
6
+ //# sourceMappingURL=bytes.d.cts.map
@@ -0,0 +1,6 @@
1
+ //#region src/number/bytes.d.ts
2
+ declare function toMb(sizeInBytes: number): number;
3
+ declare function formatBytes(sizeInBytes: number): string;
4
+ //#endregion
5
+ export { formatBytes, toMb };
6
+ //# sourceMappingURL=bytes.d.mts.map
@@ -0,0 +1,19 @@
1
+ //#region src/number/bytes.ts
2
+ /**
3
+ * Convert bytes to megabytes (rounded to nearest integer)
4
+ */
5
+ function toMb(sizeInBytes) {
6
+ return Math.round(sizeInBytes / (1024 * 1024));
7
+ }
8
+ /**
9
+ * Format bytes to human-readable size with appropriate unit (B, KB, or MB)
10
+ */
11
+ function formatBytes(sizeInBytes) {
12
+ if (sizeInBytes < 1024) return `${sizeInBytes}B`;
13
+ else if (sizeInBytes < 1024 * 1024) return `${Math.round(sizeInBytes / 1024)}KB`;
14
+ else return `${Math.round(sizeInBytes / (1024 * 1024))}MB`;
15
+ }
16
+ //#endregion
17
+ export { formatBytes, toMb };
18
+
19
+ //# sourceMappingURL=bytes.mjs.map
@@ -0,0 +1 @@
1
+ {"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"}
@@ -0,0 +1,188 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_scrub_secrets = require("../scrub-secrets.cjs");
3
+ //#region src/redaction/pii-patterns.ts
4
+ /** Compile a global regex once, adding the `g` flag if the source omits it. */
5
+ function globalRegex(source, flags = "") {
6
+ return new RegExp(source, flags.includes("g") ? flags : `${flags}g`);
7
+ }
8
+ /**
9
+ * Secret/credential patterns, sourced from {@link SECRET_VALUE_PATTERNS} so
10
+ * there is a single place that defines what a credential looks like.
11
+ */
12
+ const SECRET_PATTERNS = require_scrub_secrets.SECRET_VALUE_PATTERNS.map((re) => ({
13
+ category: "secret",
14
+ regex: globalRegex(re.source, re.flags)
15
+ }));
16
+ /** Luhn checksum — used to keep credit-card redaction from firing on any long digit run. */
17
+ function passesLuhn(candidate) {
18
+ const digits = candidate.replace(/\D/g, "");
19
+ if (digits.length < 13 || digits.length > 19) return false;
20
+ let sum = 0;
21
+ let double = false;
22
+ for (let i = digits.length - 1; i >= 0; i--) {
23
+ let digit = digits.charCodeAt(i) - 48;
24
+ if (double) {
25
+ digit *= 2;
26
+ if (digit > 9) digit -= 9;
27
+ }
28
+ sum += digit;
29
+ double = !double;
30
+ }
31
+ return sum % 10 === 0;
32
+ }
33
+ /**
34
+ * Confidence gate for phone candidates, encoding the **E.164** standard: a
35
+ * leading `+`, a non-zero country code, and 7–15 digits total. Runs on the
36
+ * digit/`+`-only normalized form (separators stripped).
37
+ */
38
+ function passesE164(candidate) {
39
+ return /^\+[1-9]\d{6,14}$/.test(candidate.replace(/[^\d+]/g, ""));
40
+ }
41
+ /**
42
+ * IBAN mod-97 checksum (ISO 13616): drop spaces, move the first 4 chars to the
43
+ * end, map letters A–Z → 10–35, and confirm the big-integer value mod 97 === 1.
44
+ */
45
+ function passesIbanChecksum(candidate) {
46
+ const compact = candidate.replace(/\s/g, "").toUpperCase();
47
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(compact)) return false;
48
+ const rearranged = compact.slice(4) + compact.slice(0, 4);
49
+ let remainder = 0;
50
+ for (let i = 0; i < rearranged.length; i++) {
51
+ const code = rearranged.charCodeAt(i);
52
+ const value = code >= 65 ? code - 55 : code - 48;
53
+ remainder = value > 9 ? (remainder * 100 + value) % 97 : (remainder * 10 + value) % 97;
54
+ }
55
+ return remainder === 1;
56
+ }
57
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
58
+ function base58Decode(input) {
59
+ const bytes = [];
60
+ for (let i = 0; i < input.length; i++) {
61
+ let carry = BASE58_ALPHABET.indexOf(input[i]);
62
+ if (carry === -1) return void 0;
63
+ for (let j = 0; j < bytes.length; j++) {
64
+ carry += bytes[j] * 58;
65
+ bytes[j] = carry & 255;
66
+ carry >>= 8;
67
+ }
68
+ while (carry > 0) {
69
+ bytes.push(carry & 255);
70
+ carry >>= 8;
71
+ }
72
+ }
73
+ for (let i = 0; i < input.length && input[i] === "1"; i++) bytes.push(0);
74
+ return Uint8Array.from(bytes.reverse());
75
+ }
76
+ /**
77
+ * Ethereum (`0x`+40 hex) or Bitcoin bech32 (`bc1`/`tb1`) — both distinctive
78
+ * enough to accept on shape alone.
79
+ */
80
+ function isDistinctiveWalletShape(match) {
81
+ if (/^0x[0-9a-fA-F]{40}$/.test(match)) return true;
82
+ return /^(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}$/.test(match);
83
+ }
84
+ /**
85
+ * Default legacy-address gate: a Base58Check payload decodes to exactly 25
86
+ * bytes (1 version + 20 hash + 4 checksum). Verifying the checksum itself needs
87
+ * SHA-256, which has no synchronous cross-platform primitive — Node callers
88
+ * inject the stricter check via `createPiiPatterns`. Erring toward redaction is
89
+ * the safe direction: an unvalidated Base58 blob of that length is far more
90
+ * likely to be a credential than prose.
91
+ */
92
+ function isLegacyWalletShape(match) {
93
+ return base58Decode(match)?.length === 25;
94
+ }
95
+ /** Ethereum, Bitcoin bech32, or a legacy Base58 address of plausible length. */
96
+ function isCryptoWalletShape(match) {
97
+ return isDistinctiveWalletShape(match) || isLegacyWalletShape(match);
98
+ }
99
+ /**
100
+ * Conservative, high-confidence PII patterns. Phone detection is best-effort:
101
+ * only well-structured (E.164) formats are matched. New {@link PiiDetectionType}
102
+ * categories slot in here; a category may map to `undefined` to declare it
103
+ * before a pattern exists, in which case it is excluded from detection.
104
+ *
105
+ * `overrides` swaps individual entries — used by `@n8n/agents` to layer its
106
+ * Node-only Base58Check validator onto `crypto-wallet`.
107
+ */
108
+ function createPiiPatterns(overrides = {}) {
109
+ return {
110
+ email: {
111
+ category: "email",
112
+ regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g
113
+ },
114
+ "credit-card": {
115
+ category: "credit-card",
116
+ regex: /\b\d(?:[ -]?\d){12,18}\b/g,
117
+ validate: passesLuhn
118
+ },
119
+ "ssn-us": {
120
+ category: "ssn-us",
121
+ regex: /\b\d{3}-\d{2}-\d{4}\b/g
122
+ },
123
+ phone: {
124
+ category: "phone",
125
+ regex: /\+\d(?:[\s().-]*\d){6,14}\b/g,
126
+ validate: passesE164
127
+ },
128
+ iban: {
129
+ category: "iban",
130
+ 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,
131
+ validate: passesIbanChecksum
132
+ },
133
+ "crypto-wallet": {
134
+ category: "crypto-wallet",
135
+ 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,
136
+ validate: isCryptoWalletShape
137
+ },
138
+ mac: {
139
+ category: "mac",
140
+ regex: /\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g
141
+ },
142
+ ip: {
143
+ category: "ip",
144
+ 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,
145
+ validate: isIpAddress
146
+ },
147
+ url: {
148
+ category: "url",
149
+ regex: /\bhttps?:\/\/[^\s<>"')\]}]+/g
150
+ },
151
+ ...overrides
152
+ };
153
+ }
154
+ /** IPv4 with octets ≤ 255, or a colon-delimited IPv6 (shape already constrained by the regex). */
155
+ function isIpAddress(match) {
156
+ if (match.includes(":")) return true;
157
+ const octets = match.split(".");
158
+ return octets.length === 4 && octets.every((o) => /^\d{1,3}$/.test(o) && Number(o) <= 255);
159
+ }
160
+ /** Browser-safe default table. Node callers layer stricter validators on top. */
161
+ const PII_PATTERNS = createPiiPatterns();
162
+ /**
163
+ * PII categories that actually have a detection pattern today — the source of
164
+ * truth for what redaction can detect. Any {@link PiiDetectionType} mapped to
165
+ * `undefined` in the table (declared but not yet implemented) is excluded here.
166
+ */
167
+ const SUPPORTED_PII_CATEGORIES = Object.keys(PII_PATTERNS).filter((type) => PII_PATTERNS[type] !== void 0);
168
+ /** Resolve the active pattern set for the given options. */
169
+ function resolvePatterns(opts, piiPatterns = PII_PATTERNS) {
170
+ const patterns = [];
171
+ if (opts.secrets) patterns.push(...SECRET_PATTERNS);
172
+ for (const type of opts.detect) {
173
+ const pattern = piiPatterns[type];
174
+ if (pattern) patterns.push(pattern);
175
+ }
176
+ return patterns;
177
+ }
178
+ //#endregion
179
+ exports.PII_PATTERNS = PII_PATTERNS;
180
+ exports.SUPPORTED_PII_CATEGORIES = SUPPORTED_PII_CATEGORIES;
181
+ exports.base58Decode = base58Decode;
182
+ exports.createPiiPatterns = createPiiPatterns;
183
+ exports.isCryptoWalletShape = isCryptoWalletShape;
184
+ exports.passesIbanChecksum = passesIbanChecksum;
185
+ exports.passesLuhn = passesLuhn;
186
+ exports.resolvePatterns = resolvePatterns;
187
+
188
+ //# sourceMappingURL=pii-patterns.cjs.map
@@ -0,0 +1 @@
1
+ {"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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"}
@@ -0,0 +1,23 @@
1
+ //#region src/redaction/pii-patterns.d.ts
2
+ type PiiDetectionType = 'email' | 'phone' | 'credit-card' | 'ssn-us' | 'iban' | 'crypto-wallet' | 'ip' | 'mac' | 'url';
3
+ type RedactionCategory = 'secret' | PiiDetectionType;
4
+ interface RedactionPattern {
5
+ readonly category: RedactionCategory;
6
+ readonly regex: RegExp;
7
+ readonly validate?: (match: string) => boolean;
8
+ }
9
+ type PiiPatternTable = Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;
10
+ declare function passesLuhn(candidate: string): boolean;
11
+ declare function passesIbanChecksum(candidate: string): boolean;
12
+ declare function base58Decode(input: string): Uint8Array | undefined;
13
+ declare function isCryptoWalletShape(match: string): boolean;
14
+ declare function createPiiPatterns(overrides?: Partial<Record<PiiDetectionType, RedactionPattern>>): PiiPatternTable;
15
+ declare const PII_PATTERNS: Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;
16
+ declare const SUPPORTED_PII_CATEGORIES: PiiDetectionType[];
17
+ declare function resolvePatterns(opts: {
18
+ secrets: boolean;
19
+ detect: readonly PiiDetectionType[];
20
+ }, piiPatterns?: PiiPatternTable): RedactionPattern[];
21
+ //#endregion
22
+ export { PII_PATTERNS, PiiDetectionType, PiiPatternTable, RedactionCategory, RedactionPattern, SUPPORTED_PII_CATEGORIES, base58Decode, createPiiPatterns, isCryptoWalletShape, passesIbanChecksum, passesLuhn, resolvePatterns };
23
+ //# sourceMappingURL=pii-patterns.d.cts.map
@@ -0,0 +1,23 @@
1
+ //#region src/redaction/pii-patterns.d.ts
2
+ type PiiDetectionType = 'email' | 'phone' | 'credit-card' | 'ssn-us' | 'iban' | 'crypto-wallet' | 'ip' | 'mac' | 'url';
3
+ type RedactionCategory = 'secret' | PiiDetectionType;
4
+ interface RedactionPattern {
5
+ readonly category: RedactionCategory;
6
+ readonly regex: RegExp;
7
+ readonly validate?: (match: string) => boolean;
8
+ }
9
+ type PiiPatternTable = Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;
10
+ declare function passesLuhn(candidate: string): boolean;
11
+ declare function passesIbanChecksum(candidate: string): boolean;
12
+ declare function base58Decode(input: string): Uint8Array | undefined;
13
+ declare function isCryptoWalletShape(match: string): boolean;
14
+ declare function createPiiPatterns(overrides?: Partial<Record<PiiDetectionType, RedactionPattern>>): PiiPatternTable;
15
+ declare const PII_PATTERNS: Readonly<Record<PiiDetectionType, RedactionPattern | undefined>>;
16
+ declare const SUPPORTED_PII_CATEGORIES: PiiDetectionType[];
17
+ declare function resolvePatterns(opts: {
18
+ secrets: boolean;
19
+ detect: readonly PiiDetectionType[];
20
+ }, piiPatterns?: PiiPatternTable): RedactionPattern[];
21
+ //#endregion
22
+ export { PII_PATTERNS, PiiDetectionType, PiiPatternTable, RedactionCategory, RedactionPattern, SUPPORTED_PII_CATEGORIES, base58Decode, createPiiPatterns, isCryptoWalletShape, passesIbanChecksum, passesLuhn, resolvePatterns };
23
+ //# sourceMappingURL=pii-patterns.d.mts.map
@@ -0,0 +1,180 @@
1
+ import { SECRET_VALUE_PATTERNS } from "../scrub-secrets.mjs";
2
+ //#region src/redaction/pii-patterns.ts
3
+ /** Compile a global regex once, adding the `g` flag if the source omits it. */
4
+ function globalRegex(source, flags = "") {
5
+ return new RegExp(source, flags.includes("g") ? flags : `${flags}g`);
6
+ }
7
+ /**
8
+ * Secret/credential patterns, sourced from {@link SECRET_VALUE_PATTERNS} so
9
+ * there is a single place that defines what a credential looks like.
10
+ */
11
+ const SECRET_PATTERNS = SECRET_VALUE_PATTERNS.map((re) => ({
12
+ category: "secret",
13
+ regex: globalRegex(re.source, re.flags)
14
+ }));
15
+ /** Luhn checksum — used to keep credit-card redaction from firing on any long digit run. */
16
+ function passesLuhn(candidate) {
17
+ const digits = candidate.replace(/\D/g, "");
18
+ if (digits.length < 13 || digits.length > 19) return false;
19
+ let sum = 0;
20
+ let double = false;
21
+ for (let i = digits.length - 1; i >= 0; i--) {
22
+ let digit = digits.charCodeAt(i) - 48;
23
+ if (double) {
24
+ digit *= 2;
25
+ if (digit > 9) digit -= 9;
26
+ }
27
+ sum += digit;
28
+ double = !double;
29
+ }
30
+ return sum % 10 === 0;
31
+ }
32
+ /**
33
+ * Confidence gate for phone candidates, encoding the **E.164** standard: a
34
+ * leading `+`, a non-zero country code, and 7–15 digits total. Runs on the
35
+ * digit/`+`-only normalized form (separators stripped).
36
+ */
37
+ function passesE164(candidate) {
38
+ return /^\+[1-9]\d{6,14}$/.test(candidate.replace(/[^\d+]/g, ""));
39
+ }
40
+ /**
41
+ * IBAN mod-97 checksum (ISO 13616): drop spaces, move the first 4 chars to the
42
+ * end, map letters A–Z → 10–35, and confirm the big-integer value mod 97 === 1.
43
+ */
44
+ function passesIbanChecksum(candidate) {
45
+ const compact = candidate.replace(/\s/g, "").toUpperCase();
46
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(compact)) return false;
47
+ const rearranged = compact.slice(4) + compact.slice(0, 4);
48
+ let remainder = 0;
49
+ for (let i = 0; i < rearranged.length; i++) {
50
+ const code = rearranged.charCodeAt(i);
51
+ const value = code >= 65 ? code - 55 : code - 48;
52
+ remainder = value > 9 ? (remainder * 100 + value) % 97 : (remainder * 10 + value) % 97;
53
+ }
54
+ return remainder === 1;
55
+ }
56
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
57
+ function base58Decode(input) {
58
+ const bytes = [];
59
+ for (let i = 0; i < input.length; i++) {
60
+ let carry = BASE58_ALPHABET.indexOf(input[i]);
61
+ if (carry === -1) return void 0;
62
+ for (let j = 0; j < bytes.length; j++) {
63
+ carry += bytes[j] * 58;
64
+ bytes[j] = carry & 255;
65
+ carry >>= 8;
66
+ }
67
+ while (carry > 0) {
68
+ bytes.push(carry & 255);
69
+ carry >>= 8;
70
+ }
71
+ }
72
+ for (let i = 0; i < input.length && input[i] === "1"; i++) bytes.push(0);
73
+ return Uint8Array.from(bytes.reverse());
74
+ }
75
+ /**
76
+ * Ethereum (`0x`+40 hex) or Bitcoin bech32 (`bc1`/`tb1`) — both distinctive
77
+ * enough to accept on shape alone.
78
+ */
79
+ function isDistinctiveWalletShape(match) {
80
+ if (/^0x[0-9a-fA-F]{40}$/.test(match)) return true;
81
+ return /^(?:bc1|tb1)[023456789acdefghjklmnpqrstuvwxyz]{11,71}$/.test(match);
82
+ }
83
+ /**
84
+ * Default legacy-address gate: a Base58Check payload decodes to exactly 25
85
+ * bytes (1 version + 20 hash + 4 checksum). Verifying the checksum itself needs
86
+ * SHA-256, which has no synchronous cross-platform primitive — Node callers
87
+ * inject the stricter check via `createPiiPatterns`. Erring toward redaction is
88
+ * the safe direction: an unvalidated Base58 blob of that length is far more
89
+ * likely to be a credential than prose.
90
+ */
91
+ function isLegacyWalletShape(match) {
92
+ return base58Decode(match)?.length === 25;
93
+ }
94
+ /** Ethereum, Bitcoin bech32, or a legacy Base58 address of plausible length. */
95
+ function isCryptoWalletShape(match) {
96
+ return isDistinctiveWalletShape(match) || isLegacyWalletShape(match);
97
+ }
98
+ /**
99
+ * Conservative, high-confidence PII patterns. Phone detection is best-effort:
100
+ * only well-structured (E.164) formats are matched. New {@link PiiDetectionType}
101
+ * categories slot in here; a category may map to `undefined` to declare it
102
+ * before a pattern exists, in which case it is excluded from detection.
103
+ *
104
+ * `overrides` swaps individual entries — used by `@n8n/agents` to layer its
105
+ * Node-only Base58Check validator onto `crypto-wallet`.
106
+ */
107
+ function createPiiPatterns(overrides = {}) {
108
+ return {
109
+ email: {
110
+ category: "email",
111
+ regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g
112
+ },
113
+ "credit-card": {
114
+ category: "credit-card",
115
+ regex: /\b\d(?:[ -]?\d){12,18}\b/g,
116
+ validate: passesLuhn
117
+ },
118
+ "ssn-us": {
119
+ category: "ssn-us",
120
+ regex: /\b\d{3}-\d{2}-\d{4}\b/g
121
+ },
122
+ phone: {
123
+ category: "phone",
124
+ regex: /\+\d(?:[\s().-]*\d){6,14}\b/g,
125
+ validate: passesE164
126
+ },
127
+ iban: {
128
+ category: "iban",
129
+ 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,
130
+ validate: passesIbanChecksum
131
+ },
132
+ "crypto-wallet": {
133
+ category: "crypto-wallet",
134
+ 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,
135
+ validate: isCryptoWalletShape
136
+ },
137
+ mac: {
138
+ category: "mac",
139
+ regex: /\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g
140
+ },
141
+ ip: {
142
+ category: "ip",
143
+ 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,
144
+ validate: isIpAddress
145
+ },
146
+ url: {
147
+ category: "url",
148
+ regex: /\bhttps?:\/\/[^\s<>"')\]}]+/g
149
+ },
150
+ ...overrides
151
+ };
152
+ }
153
+ /** IPv4 with octets ≤ 255, or a colon-delimited IPv6 (shape already constrained by the regex). */
154
+ function isIpAddress(match) {
155
+ if (match.includes(":")) return true;
156
+ const octets = match.split(".");
157
+ return octets.length === 4 && octets.every((o) => /^\d{1,3}$/.test(o) && Number(o) <= 255);
158
+ }
159
+ /** Browser-safe default table. Node callers layer stricter validators on top. */
160
+ const PII_PATTERNS = createPiiPatterns();
161
+ /**
162
+ * PII categories that actually have a detection pattern today — the source of
163
+ * truth for what redaction can detect. Any {@link PiiDetectionType} mapped to
164
+ * `undefined` in the table (declared but not yet implemented) is excluded here.
165
+ */
166
+ const SUPPORTED_PII_CATEGORIES = Object.keys(PII_PATTERNS).filter((type) => PII_PATTERNS[type] !== void 0);
167
+ /** Resolve the active pattern set for the given options. */
168
+ function resolvePatterns(opts, piiPatterns = PII_PATTERNS) {
169
+ const patterns = [];
170
+ if (opts.secrets) patterns.push(...SECRET_PATTERNS);
171
+ for (const type of opts.detect) {
172
+ const pattern = piiPatterns[type];
173
+ if (pattern) patterns.push(pattern);
174
+ }
175
+ return patterns;
176
+ }
177
+ //#endregion
178
+ export { PII_PATTERNS, SUPPORTED_PII_CATEGORIES, base58Decode, createPiiPatterns, isCryptoWalletShape, passesIbanChecksum, passesLuhn, resolvePatterns };
179
+
180
+ //# sourceMappingURL=pii-patterns.mjs.map
@@ -0,0 +1 @@
1
+ {"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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"}
@@ -0,0 +1,152 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_redaction_pii_patterns = require("./pii-patterns.cjs");
3
+ //#region src/redaction/redact-text.ts
4
+ const DEFAULT_PLACEHOLDER = "[REDACTED]";
5
+ /**
6
+ * Redact secret/PII patterns from a complete string. Pure and idempotent —
7
+ * already-redacted placeholders are left untouched by the underlying patterns.
8
+ */
9
+ function redactText(input, opts = {}) {
10
+ const placeholder = opts.placeholder ?? "[REDACTED]";
11
+ const patterns = require_redaction_pii_patterns.resolvePatterns({
12
+ secrets: opts.secrets ?? true,
13
+ detect: opts.detect ?? []
14
+ }, opts.piiPatterns);
15
+ const ordered = opts.preserveUrlStructure ? [...patterns.filter((pattern) => pattern.category === "url"), ...patterns.filter((pattern) => pattern.category !== "url")] : patterns;
16
+ const matches = [];
17
+ let text = input;
18
+ for (const pattern of ordered) text = text.replace(pattern.regex, (match) => {
19
+ if (pattern.validate && !pattern.validate(match)) return match;
20
+ if (opts.preserveUrlStructure && pattern.category === "url") {
21
+ const rebuilt = stripUrlSensitiveParts(match, placeholder);
22
+ if (rebuilt !== match) matches.push({ category: pattern.category });
23
+ return rebuilt;
24
+ }
25
+ matches.push({ category: pattern.category });
26
+ return placeholder;
27
+ });
28
+ return {
29
+ text,
30
+ matches
31
+ };
32
+ }
33
+ /** True for a path segment that looks like an embedded token — webhook-style
34
+ * services (Slack/Discord/Telegram, …) carry their secret as a path segment.
35
+ * Shape-based on purpose: per-service URL grammars don't scale across hundreds
36
+ * of integrations. Conservative: words, readable slugs and digit-only ids are
37
+ * kept. */
38
+ function isTokenLikeSegment(segment) {
39
+ if (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\d/.test(segment)) return true;
40
+ return segment.length >= 24 && /^[A-Za-z]+$/.test(segment);
41
+ }
42
+ /** Keep origin + path (token-like segments redacted) + query names; redact
43
+ * query values, drop userinfo and fragment. The replacement is URL-safe (no
44
+ * `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒
45
+ * fully redacted. */
46
+ function stripUrlSensitiveParts(match, placeholder) {
47
+ const urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, "") || "REDACTED";
48
+ try {
49
+ const url = new URL(match);
50
+ const pathname = url.pathname.split("/").map((segment) => isTokenLikeSegment(segment) ? urlPlaceholder : segment).join("/");
51
+ const names = [...url.searchParams.keys()];
52
+ const query = names.length > 0 ? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join("&")}` : "";
53
+ return `${url.origin}${pathname}${query}`;
54
+ } catch {
55
+ return placeholder;
56
+ }
57
+ }
58
+ /**
59
+ * Find the `[start, end)` ranges of every (validated) match in `input`. Used by
60
+ * the streaming redactor to avoid emitting through the middle of a complete
61
+ * match that contains internal whitespace (e.g. a spaced credit-card number).
62
+ */
63
+ function findMatchRanges(input, opts = {}) {
64
+ const patterns = require_redaction_pii_patterns.resolvePatterns({
65
+ secrets: opts.secrets ?? true,
66
+ detect: opts.detect ?? []
67
+ }, opts.piiPatterns);
68
+ const ranges = [];
69
+ for (const pattern of patterns) {
70
+ const { regex } = pattern;
71
+ regex.lastIndex = 0;
72
+ let match;
73
+ while ((match = regex.exec(input)) !== null) {
74
+ if (match[0].length === 0) {
75
+ regex.lastIndex++;
76
+ continue;
77
+ }
78
+ if (pattern.validate && !pattern.validate(match[0])) continue;
79
+ ranges.push([match.index, match.index + match[0].length]);
80
+ }
81
+ }
82
+ return ranges;
83
+ }
84
+ const MAX_DEEP_DEPTH = 8;
85
+ const SENSITIVE_KEY_PATTERN = /(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[._-])token$)/i;
86
+ /**
87
+ * Recursively redact string values inside an arbitrary JSON-like value
88
+ * (tool results, structured payloads). Object keys are left intact; only
89
+ * string values are scanned. Recursion is depth-bounded as a cheap guard
90
+ * against pathological/cyclic structures.
91
+ */
92
+ function redactDeep(value, opts = {}, depth = 0) {
93
+ return redactDeepValue(value, opts, depth);
94
+ }
95
+ function redactDeepValue(value, opts, depth, key) {
96
+ if (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) return {
97
+ value: opts.placeholder ?? "[REDACTED]",
98
+ matches: [{ category: "secret" }]
99
+ };
100
+ if (typeof value === "string") {
101
+ const { text, matches } = redactText(value, opts);
102
+ return {
103
+ value: text,
104
+ matches
105
+ };
106
+ }
107
+ if (depth >= MAX_DEEP_DEPTH) {
108
+ if (value !== null && typeof value === "object") return {
109
+ value: opts.placeholder ?? "[REDACTED]",
110
+ matches: [{ category: "secret" }]
111
+ };
112
+ return {
113
+ value,
114
+ matches: []
115
+ };
116
+ }
117
+ if (Array.isArray(value)) {
118
+ const matches = [];
119
+ return {
120
+ value: value.map((item) => {
121
+ const result = redactDeepValue(item, opts, depth + 1, key);
122
+ matches.push(...result.matches);
123
+ return result.value;
124
+ }),
125
+ matches
126
+ };
127
+ }
128
+ if (value !== null && typeof value === "object") {
129
+ const matches = [];
130
+ const next = {};
131
+ for (const [key, item] of Object.entries(value)) {
132
+ const result = redactDeepValue(item, opts, depth + 1, key);
133
+ matches.push(...result.matches);
134
+ next[key] = result.value;
135
+ }
136
+ return {
137
+ value: next,
138
+ matches
139
+ };
140
+ }
141
+ return {
142
+ value,
143
+ matches: []
144
+ };
145
+ }
146
+ //#endregion
147
+ exports.DEFAULT_PLACEHOLDER = DEFAULT_PLACEHOLDER;
148
+ exports.findMatchRanges = findMatchRanges;
149
+ exports.redactDeep = redactDeep;
150
+ exports.redactText = redactText;
151
+
152
+ //# sourceMappingURL=redact-text.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact-text.cjs","names":["resolvePatterns"],"sources":["../../src/redaction/redact-text.ts"],"sourcesContent":["import type { PiiDetectionType, PiiPatternTable, RedactionCategory } from './pii-patterns';\nimport { resolvePatterns } from './pii-patterns';\n\nexport const DEFAULT_PLACEHOLDER = '[REDACTED]';\n\nexport interface RedactionOptions {\n\t/** Scan for credential/secret patterns. Defaults to `true`. */\n\tsecrets?: boolean;\n\t/** PII categories to scan for. Defaults to none. */\n\tdetect?: readonly PiiDetectionType[];\n\t/** Replacement text for a match. Defaults to `[REDACTED]`. */\n\tplaceholder?: string;\n\t/** For `url` matches, keep origin + path + query names and redact the\n\t * value-bearing parts: query values, token-like path segments (webhook\n\t * secrets), userinfo, fragment. Off by default so guardrail behavior is\n\t * unchanged; telemetry/trace scrubbing opts in. */\n\tpreserveUrlStructure?: boolean;\n\t/** Replace values under secret-shaped object keys. */\n\tredactSensitiveKeys?: boolean;\n\t/**\n\t * Detection table to resolve PII categories against. Defaults to the\n\t * browser-safe {@link PII_PATTERNS}; `@n8n/agents` passes a table whose\n\t * `crypto-wallet` entry carries the Node-only Base58Check validator.\n\t */\n\tpiiPatterns?: PiiPatternTable;\n}\n\nexport interface RedactionResult {\n\t/** The input with every detected match replaced by the placeholder. */\n\ttext: string;\n\t/** One entry per replaced match (category only — never the value). */\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Redact secret/PII patterns from a complete string. Pure and idempotent —\n * already-redacted placeholders are left untouched by the underlying patterns.\n */\nexport function redactText(input: string, opts: RedactionOptions = {}): RedactionResult {\n\tconst placeholder = opts.placeholder ?? DEFAULT_PLACEHOLDER;\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\t// In preserve mode the url pass runs FIRST: it rewrites URLs with a URL-safe\n\t// placeholder before other patterns can plant one containing `]` mid-URL —\n\t// `]` stops the url regex, which would hide the URL's tail (and any secrets\n\t// in it) from this pass entirely.\n\tconst ordered = opts.preserveUrlStructure\n\t\t? [\n\t\t\t\t...patterns.filter((pattern) => pattern.category === 'url'),\n\t\t\t\t...patterns.filter((pattern) => pattern.category !== 'url'),\n\t\t\t]\n\t\t: patterns;\n\n\tconst matches: Array<{ category: RedactionCategory }> = [];\n\tlet text = input;\n\n\tfor (const pattern of ordered) {\n\t\t// `replace` with a global regex scans from 0 and resets lastIndex, so the\n\t\t// shared precompiled regex is safe to reuse across calls.\n\t\ttext = text.replace(pattern.regex, (match) => {\n\t\t\tif (pattern.validate && !pattern.validate(match)) return match;\n\t\t\tif (opts.preserveUrlStructure && pattern.category === 'url') {\n\t\t\t\tconst rebuilt = stripUrlSensitiveParts(match, placeholder);\n\t\t\t\tif (rebuilt !== match) matches.push({ category: pattern.category });\n\t\t\t\treturn rebuilt;\n\t\t\t}\n\t\t\tmatches.push({ category: pattern.category });\n\t\t\treturn placeholder;\n\t\t});\n\t}\n\n\treturn { text, matches };\n}\n\n/** True for a path segment that looks like an embedded token — webhook-style\n * services (Slack/Discord/Telegram, …) carry their secret as a path segment.\n * Shape-based on purpose: per-service URL grammars don't scale across hundreds\n * of integrations. Conservative: words, readable slugs and digit-only ids are\n * kept. */\nfunction isTokenLikeSegment(segment: string): boolean {\n\tif (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\\d/.test(segment)) return true;\n\t// Long single-class opaque blob (e.g. a letters-only token) — real words stay\n\t// shorter and readable slugs contain separators.\n\treturn segment.length >= 24 && /^[A-Za-z]+$/.test(segment);\n}\n\n/** Keep origin + path (token-like segments redacted) + query names; redact\n * query values, drop userinfo and fragment. The replacement is URL-safe (no\n * `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒\n * fully redacted. */\nfunction stripUrlSensitiveParts(match: string, placeholder: string): string {\n\tconst urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, '') || 'REDACTED';\n\ttry {\n\t\tconst url = new URL(match);\n\t\tconst pathname = url.pathname\n\t\t\t.split('/')\n\t\t\t.map((segment) => (isTokenLikeSegment(segment) ? urlPlaceholder : segment))\n\t\t\t.join('/');\n\t\tconst names = [...url.searchParams.keys()];\n\t\tconst query =\n\t\t\tnames.length > 0\n\t\t\t\t? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join('&')}`\n\t\t\t\t: '';\n\t\treturn `${url.origin}${pathname}${query}`;\n\t} catch {\n\t\treturn placeholder;\n\t}\n}\n\n/**\n * Find the `[start, end)` ranges of every (validated) match in `input`. Used by\n * the streaming redactor to avoid emitting through the middle of a complete\n * match that contains internal whitespace (e.g. a spaced credit-card number).\n */\nexport function findMatchRanges(\n\tinput: string,\n\topts: RedactionOptions = {},\n): Array<[number, number]> {\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\n\tconst ranges: Array<[number, number]> = [];\n\tfor (const pattern of patterns) {\n\t\tconst { regex } = pattern;\n\t\t// Reset before the scan loop; reusing the shared global regex is safe\n\t\t// because usage is synchronous and the loop always runs to completion.\n\t\tregex.lastIndex = 0;\n\t\tlet match: RegExpExecArray | null;\n\t\twhile ((match = regex.exec(input)) !== null) {\n\t\t\tif (match[0].length === 0) {\n\t\t\t\tregex.lastIndex++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pattern.validate && !pattern.validate(match[0])) continue;\n\t\t\tranges.push([match.index, match.index + match[0].length]);\n\t\t}\n\t}\n\treturn ranges;\n}\n\nconst MAX_DEEP_DEPTH = 8;\nconst SENSITIVE_KEY_PATTERN =\n\t/(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[._-])token$)/i;\n\nexport interface DeepRedactionResult {\n\tvalue: unknown;\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Recursively redact string values inside an arbitrary JSON-like value\n * (tool results, structured payloads). Object keys are left intact; only\n * string values are scanned. Recursion is depth-bounded as a cheap guard\n * against pathological/cyclic structures.\n */\nexport function redactDeep(\n\tvalue: unknown,\n\topts: RedactionOptions = {},\n\tdepth = 0,\n): DeepRedactionResult {\n\treturn redactDeepValue(value, opts, depth);\n}\n\nfunction redactDeepValue(\n\tvalue: unknown,\n\topts: RedactionOptions,\n\tdepth: number,\n\tkey?: string,\n): DeepRedactionResult {\n\tif (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) {\n\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t}\n\n\tif (typeof value === 'string') {\n\t\tconst { text, matches } = redactText(value, opts);\n\t\treturn { value: text, matches };\n\t}\n\n\t// Fail closed at the recursion bound: a subtree we refuse to walk is withheld\n\t// rather than passed through unscanned. Real payloads don't nest this deep,\n\t// so the only things reaching here are pathological or cyclic.\n\tif (depth >= MAX_DEEP_DEPTH) {\n\t\tif (value !== null && typeof value === 'object') {\n\t\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t\t}\n\t\treturn { value, matches: [] };\n\t}\n\n\tif (Array.isArray(value)) {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next = value.map((item) => {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\treturn result.value;\n\t\t});\n\t\treturn { value: next, matches };\n\t}\n\n\tif (value !== null && typeof value === 'object') {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next: Record<string, unknown> = {};\n\t\tfor (const [key, item] of Object.entries(value)) {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\tnext[key] = result.value;\n\t\t}\n\t\treturn { value: next, matches };\n\t}\n\n\treturn { value, matches: [] };\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { PiiDetectionType, PiiPatternTable, RedactionCategory } from "./pii-patterns.cjs";
2
+ //#region src/redaction/redact-text.d.ts
3
+ declare const DEFAULT_PLACEHOLDER = "[REDACTED]";
4
+ interface RedactionOptions {
5
+ secrets?: boolean;
6
+ detect?: readonly PiiDetectionType[];
7
+ placeholder?: string;
8
+ preserveUrlStructure?: boolean;
9
+ redactSensitiveKeys?: boolean;
10
+ piiPatterns?: PiiPatternTable;
11
+ }
12
+ interface RedactionResult {
13
+ text: string;
14
+ matches: Array<{
15
+ category: RedactionCategory;
16
+ }>;
17
+ }
18
+ declare function redactText(input: string, opts?: RedactionOptions): RedactionResult;
19
+ declare function findMatchRanges(input: string, opts?: RedactionOptions): Array<[number, number]>;
20
+ interface DeepRedactionResult {
21
+ value: unknown;
22
+ matches: Array<{
23
+ category: RedactionCategory;
24
+ }>;
25
+ }
26
+ declare function redactDeep(value: unknown, opts?: RedactionOptions, depth?: number): DeepRedactionResult;
27
+ //#endregion
28
+ export { DEFAULT_PLACEHOLDER, DeepRedactionResult, RedactionOptions, RedactionResult, findMatchRanges, redactDeep, redactText };
29
+ //# sourceMappingURL=redact-text.d.cts.map
@@ -0,0 +1,29 @@
1
+ import { PiiDetectionType, PiiPatternTable, RedactionCategory } from "./pii-patterns.mjs";
2
+ //#region src/redaction/redact-text.d.ts
3
+ declare const DEFAULT_PLACEHOLDER = "[REDACTED]";
4
+ interface RedactionOptions {
5
+ secrets?: boolean;
6
+ detect?: readonly PiiDetectionType[];
7
+ placeholder?: string;
8
+ preserveUrlStructure?: boolean;
9
+ redactSensitiveKeys?: boolean;
10
+ piiPatterns?: PiiPatternTable;
11
+ }
12
+ interface RedactionResult {
13
+ text: string;
14
+ matches: Array<{
15
+ category: RedactionCategory;
16
+ }>;
17
+ }
18
+ declare function redactText(input: string, opts?: RedactionOptions): RedactionResult;
19
+ declare function findMatchRanges(input: string, opts?: RedactionOptions): Array<[number, number]>;
20
+ interface DeepRedactionResult {
21
+ value: unknown;
22
+ matches: Array<{
23
+ category: RedactionCategory;
24
+ }>;
25
+ }
26
+ declare function redactDeep(value: unknown, opts?: RedactionOptions, depth?: number): DeepRedactionResult;
27
+ //#endregion
28
+ export { DEFAULT_PLACEHOLDER, DeepRedactionResult, RedactionOptions, RedactionResult, findMatchRanges, redactDeep, redactText };
29
+ //# sourceMappingURL=redact-text.d.mts.map
@@ -0,0 +1,148 @@
1
+ import { resolvePatterns } from "./pii-patterns.mjs";
2
+ //#region src/redaction/redact-text.ts
3
+ const DEFAULT_PLACEHOLDER = "[REDACTED]";
4
+ /**
5
+ * Redact secret/PII patterns from a complete string. Pure and idempotent —
6
+ * already-redacted placeholders are left untouched by the underlying patterns.
7
+ */
8
+ function redactText(input, opts = {}) {
9
+ const placeholder = opts.placeholder ?? "[REDACTED]";
10
+ const patterns = resolvePatterns({
11
+ secrets: opts.secrets ?? true,
12
+ detect: opts.detect ?? []
13
+ }, opts.piiPatterns);
14
+ const ordered = opts.preserveUrlStructure ? [...patterns.filter((pattern) => pattern.category === "url"), ...patterns.filter((pattern) => pattern.category !== "url")] : patterns;
15
+ const matches = [];
16
+ let text = input;
17
+ for (const pattern of ordered) text = text.replace(pattern.regex, (match) => {
18
+ if (pattern.validate && !pattern.validate(match)) return match;
19
+ if (opts.preserveUrlStructure && pattern.category === "url") {
20
+ const rebuilt = stripUrlSensitiveParts(match, placeholder);
21
+ if (rebuilt !== match) matches.push({ category: pattern.category });
22
+ return rebuilt;
23
+ }
24
+ matches.push({ category: pattern.category });
25
+ return placeholder;
26
+ });
27
+ return {
28
+ text,
29
+ matches
30
+ };
31
+ }
32
+ /** True for a path segment that looks like an embedded token — webhook-style
33
+ * services (Slack/Discord/Telegram, …) carry their secret as a path segment.
34
+ * Shape-based on purpose: per-service URL grammars don't scale across hundreds
35
+ * of integrations. Conservative: words, readable slugs and digit-only ids are
36
+ * kept. */
37
+ function isTokenLikeSegment(segment) {
38
+ if (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\d/.test(segment)) return true;
39
+ return segment.length >= 24 && /^[A-Za-z]+$/.test(segment);
40
+ }
41
+ /** Keep origin + path (token-like segments redacted) + query names; redact
42
+ * query values, drop userinfo and fragment. The replacement is URL-safe (no
43
+ * `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒
44
+ * fully redacted. */
45
+ function stripUrlSensitiveParts(match, placeholder) {
46
+ const urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, "") || "REDACTED";
47
+ try {
48
+ const url = new URL(match);
49
+ const pathname = url.pathname.split("/").map((segment) => isTokenLikeSegment(segment) ? urlPlaceholder : segment).join("/");
50
+ const names = [...url.searchParams.keys()];
51
+ const query = names.length > 0 ? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join("&")}` : "";
52
+ return `${url.origin}${pathname}${query}`;
53
+ } catch {
54
+ return placeholder;
55
+ }
56
+ }
57
+ /**
58
+ * Find the `[start, end)` ranges of every (validated) match in `input`. Used by
59
+ * the streaming redactor to avoid emitting through the middle of a complete
60
+ * match that contains internal whitespace (e.g. a spaced credit-card number).
61
+ */
62
+ function findMatchRanges(input, opts = {}) {
63
+ const patterns = resolvePatterns({
64
+ secrets: opts.secrets ?? true,
65
+ detect: opts.detect ?? []
66
+ }, opts.piiPatterns);
67
+ const ranges = [];
68
+ for (const pattern of patterns) {
69
+ const { regex } = pattern;
70
+ regex.lastIndex = 0;
71
+ let match;
72
+ while ((match = regex.exec(input)) !== null) {
73
+ if (match[0].length === 0) {
74
+ regex.lastIndex++;
75
+ continue;
76
+ }
77
+ if (pattern.validate && !pattern.validate(match[0])) continue;
78
+ ranges.push([match.index, match.index + match[0].length]);
79
+ }
80
+ }
81
+ return ranges;
82
+ }
83
+ const MAX_DEEP_DEPTH = 8;
84
+ const SENSITIVE_KEY_PATTERN = /(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[._-])token$)/i;
85
+ /**
86
+ * Recursively redact string values inside an arbitrary JSON-like value
87
+ * (tool results, structured payloads). Object keys are left intact; only
88
+ * string values are scanned. Recursion is depth-bounded as a cheap guard
89
+ * against pathological/cyclic structures.
90
+ */
91
+ function redactDeep(value, opts = {}, depth = 0) {
92
+ return redactDeepValue(value, opts, depth);
93
+ }
94
+ function redactDeepValue(value, opts, depth, key) {
95
+ if (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) return {
96
+ value: opts.placeholder ?? "[REDACTED]",
97
+ matches: [{ category: "secret" }]
98
+ };
99
+ if (typeof value === "string") {
100
+ const { text, matches } = redactText(value, opts);
101
+ return {
102
+ value: text,
103
+ matches
104
+ };
105
+ }
106
+ if (depth >= MAX_DEEP_DEPTH) {
107
+ if (value !== null && typeof value === "object") return {
108
+ value: opts.placeholder ?? "[REDACTED]",
109
+ matches: [{ category: "secret" }]
110
+ };
111
+ return {
112
+ value,
113
+ matches: []
114
+ };
115
+ }
116
+ if (Array.isArray(value)) {
117
+ const matches = [];
118
+ return {
119
+ value: value.map((item) => {
120
+ const result = redactDeepValue(item, opts, depth + 1, key);
121
+ matches.push(...result.matches);
122
+ return result.value;
123
+ }),
124
+ matches
125
+ };
126
+ }
127
+ if (value !== null && typeof value === "object") {
128
+ const matches = [];
129
+ const next = {};
130
+ for (const [key, item] of Object.entries(value)) {
131
+ const result = redactDeepValue(item, opts, depth + 1, key);
132
+ matches.push(...result.matches);
133
+ next[key] = result.value;
134
+ }
135
+ return {
136
+ value: next,
137
+ matches
138
+ };
139
+ }
140
+ return {
141
+ value,
142
+ matches: []
143
+ };
144
+ }
145
+ //#endregion
146
+ export { DEFAULT_PLACEHOLDER, findMatchRanges, redactDeep, redactText };
147
+
148
+ //# sourceMappingURL=redact-text.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact-text.mjs","names":[],"sources":["../../src/redaction/redact-text.ts"],"sourcesContent":["import type { PiiDetectionType, PiiPatternTable, RedactionCategory } from './pii-patterns';\nimport { resolvePatterns } from './pii-patterns';\n\nexport const DEFAULT_PLACEHOLDER = '[REDACTED]';\n\nexport interface RedactionOptions {\n\t/** Scan for credential/secret patterns. Defaults to `true`. */\n\tsecrets?: boolean;\n\t/** PII categories to scan for. Defaults to none. */\n\tdetect?: readonly PiiDetectionType[];\n\t/** Replacement text for a match. Defaults to `[REDACTED]`. */\n\tplaceholder?: string;\n\t/** For `url` matches, keep origin + path + query names and redact the\n\t * value-bearing parts: query values, token-like path segments (webhook\n\t * secrets), userinfo, fragment. Off by default so guardrail behavior is\n\t * unchanged; telemetry/trace scrubbing opts in. */\n\tpreserveUrlStructure?: boolean;\n\t/** Replace values under secret-shaped object keys. */\n\tredactSensitiveKeys?: boolean;\n\t/**\n\t * Detection table to resolve PII categories against. Defaults to the\n\t * browser-safe {@link PII_PATTERNS}; `@n8n/agents` passes a table whose\n\t * `crypto-wallet` entry carries the Node-only Base58Check validator.\n\t */\n\tpiiPatterns?: PiiPatternTable;\n}\n\nexport interface RedactionResult {\n\t/** The input with every detected match replaced by the placeholder. */\n\ttext: string;\n\t/** One entry per replaced match (category only — never the value). */\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Redact secret/PII patterns from a complete string. Pure and idempotent —\n * already-redacted placeholders are left untouched by the underlying patterns.\n */\nexport function redactText(input: string, opts: RedactionOptions = {}): RedactionResult {\n\tconst placeholder = opts.placeholder ?? DEFAULT_PLACEHOLDER;\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\t// In preserve mode the url pass runs FIRST: it rewrites URLs with a URL-safe\n\t// placeholder before other patterns can plant one containing `]` mid-URL —\n\t// `]` stops the url regex, which would hide the URL's tail (and any secrets\n\t// in it) from this pass entirely.\n\tconst ordered = opts.preserveUrlStructure\n\t\t? [\n\t\t\t\t...patterns.filter((pattern) => pattern.category === 'url'),\n\t\t\t\t...patterns.filter((pattern) => pattern.category !== 'url'),\n\t\t\t]\n\t\t: patterns;\n\n\tconst matches: Array<{ category: RedactionCategory }> = [];\n\tlet text = input;\n\n\tfor (const pattern of ordered) {\n\t\t// `replace` with a global regex scans from 0 and resets lastIndex, so the\n\t\t// shared precompiled regex is safe to reuse across calls.\n\t\ttext = text.replace(pattern.regex, (match) => {\n\t\t\tif (pattern.validate && !pattern.validate(match)) return match;\n\t\t\tif (opts.preserveUrlStructure && pattern.category === 'url') {\n\t\t\t\tconst rebuilt = stripUrlSensitiveParts(match, placeholder);\n\t\t\t\tif (rebuilt !== match) matches.push({ category: pattern.category });\n\t\t\t\treturn rebuilt;\n\t\t\t}\n\t\t\tmatches.push({ category: pattern.category });\n\t\t\treturn placeholder;\n\t\t});\n\t}\n\n\treturn { text, matches };\n}\n\n/** True for a path segment that looks like an embedded token — webhook-style\n * services (Slack/Discord/Telegram, …) carry their secret as a path segment.\n * Shape-based on purpose: per-service URL grammars don't scale across hundreds\n * of integrations. Conservative: words, readable slugs and digit-only ids are\n * kept. */\nfunction isTokenLikeSegment(segment: string): boolean {\n\tif (segment.length >= 16 && /[A-Za-z]/.test(segment) && /\\d/.test(segment)) return true;\n\t// Long single-class opaque blob (e.g. a letters-only token) — real words stay\n\t// shorter and readable slugs contain separators.\n\treturn segment.length >= 24 && /^[A-Za-z]+$/.test(segment);\n}\n\n/** Keep origin + path (token-like segments redacted) + query names; redact\n * query values, drop userinfo and fragment. The replacement is URL-safe (no\n * `]`, which the url regex stops at) so re-scrubbing is stable. Unparseable ⇒\n * fully redacted. */\nfunction stripUrlSensitiveParts(match: string, placeholder: string): string {\n\tconst urlPlaceholder = placeholder.replace(/[^A-Za-z0-9_.~-]/g, '') || 'REDACTED';\n\ttry {\n\t\tconst url = new URL(match);\n\t\tconst pathname = url.pathname\n\t\t\t.split('/')\n\t\t\t.map((segment) => (isTokenLikeSegment(segment) ? urlPlaceholder : segment))\n\t\t\t.join('/');\n\t\tconst names = [...url.searchParams.keys()];\n\t\tconst query =\n\t\t\tnames.length > 0\n\t\t\t\t? `?${names.map((name) => `${encodeURIComponent(name)}=${urlPlaceholder}`).join('&')}`\n\t\t\t\t: '';\n\t\treturn `${url.origin}${pathname}${query}`;\n\t} catch {\n\t\treturn placeholder;\n\t}\n}\n\n/**\n * Find the `[start, end)` ranges of every (validated) match in `input`. Used by\n * the streaming redactor to avoid emitting through the middle of a complete\n * match that contains internal whitespace (e.g. a spaced credit-card number).\n */\nexport function findMatchRanges(\n\tinput: string,\n\topts: RedactionOptions = {},\n): Array<[number, number]> {\n\tconst patterns = resolvePatterns(\n\t\t{\n\t\t\tsecrets: opts.secrets ?? true,\n\t\t\tdetect: opts.detect ?? [],\n\t\t},\n\t\topts.piiPatterns,\n\t);\n\n\tconst ranges: Array<[number, number]> = [];\n\tfor (const pattern of patterns) {\n\t\tconst { regex } = pattern;\n\t\t// Reset before the scan loop; reusing the shared global regex is safe\n\t\t// because usage is synchronous and the loop always runs to completion.\n\t\tregex.lastIndex = 0;\n\t\tlet match: RegExpExecArray | null;\n\t\twhile ((match = regex.exec(input)) !== null) {\n\t\t\tif (match[0].length === 0) {\n\t\t\t\tregex.lastIndex++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pattern.validate && !pattern.validate(match[0])) continue;\n\t\t\tranges.push([match.index, match.index + match[0].length]);\n\t\t}\n\t}\n\treturn ranges;\n}\n\nconst MAX_DEEP_DEPTH = 8;\nconst SENSITIVE_KEY_PATTERN =\n\t/(api[_-]?key|private[_-]?key|authorization|bearer|cookie|credentials?|password|secret|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|auth[_-]?token|(?:^|[._-])token$)/i;\n\nexport interface DeepRedactionResult {\n\tvalue: unknown;\n\tmatches: Array<{ category: RedactionCategory }>;\n}\n\n/**\n * Recursively redact string values inside an arbitrary JSON-like value\n * (tool results, structured payloads). Object keys are left intact; only\n * string values are scanned. Recursion is depth-bounded as a cheap guard\n * against pathological/cyclic structures.\n */\nexport function redactDeep(\n\tvalue: unknown,\n\topts: RedactionOptions = {},\n\tdepth = 0,\n): DeepRedactionResult {\n\treturn redactDeepValue(value, opts, depth);\n}\n\nfunction redactDeepValue(\n\tvalue: unknown,\n\topts: RedactionOptions,\n\tdepth: number,\n\tkey?: string,\n): DeepRedactionResult {\n\tif (opts.redactSensitiveKeys && key && SENSITIVE_KEY_PATTERN.test(key)) {\n\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t}\n\n\tif (typeof value === 'string') {\n\t\tconst { text, matches } = redactText(value, opts);\n\t\treturn { value: text, matches };\n\t}\n\n\t// Fail closed at the recursion bound: a subtree we refuse to walk is withheld\n\t// rather than passed through unscanned. Real payloads don't nest this deep,\n\t// so the only things reaching here are pathological or cyclic.\n\tif (depth >= MAX_DEEP_DEPTH) {\n\t\tif (value !== null && typeof value === 'object') {\n\t\t\treturn { value: opts.placeholder ?? DEFAULT_PLACEHOLDER, matches: [{ category: 'secret' }] };\n\t\t}\n\t\treturn { value, matches: [] };\n\t}\n\n\tif (Array.isArray(value)) {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next = value.map((item) => {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\treturn result.value;\n\t\t});\n\t\treturn { value: next, matches };\n\t}\n\n\tif (value !== null && typeof value === 'object') {\n\t\tconst matches: Array<{ category: RedactionCategory }> = [];\n\t\tconst next: Record<string, unknown> = {};\n\t\tfor (const [key, item] of Object.entries(value)) {\n\t\t\tconst result = redactDeepValue(item, opts, depth + 1, key);\n\t\t\tmatches.push(...result.matches);\n\t\t\tnext[key] = result.value;\n\t\t}\n\t\treturn { value: next, matches };\n\t}\n\n\treturn { value, matches: [] };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@n8n/utils",
3
3
  "type": "module",
4
- "version": "1.44.0",
4
+ "version": "1.45.1",
5
5
  "files": [
6
6
  "dist",
7
- "LICENSE.md",
8
- "LICENSE_EE.md"
7
+ "LICENSE_EE.md",
8
+ "LICENSE.md"
9
9
  ],
10
10
  "exports": {
11
11
  "./dist/*": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "nanoid": "3.3.18",
31
- "@n8n/constants": "0.35.0"
31
+ "@n8n/constants": "0.36.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@testing-library/jest-dom": "^6.6.3",
@@ -38,8 +38,8 @@
38
38
  "vite": "^8.0.2",
39
39
  "vitest": "^4.1.9",
40
40
  "@n8n/typescript-config": "1.10.0",
41
- "@n8n/eslint-config": "0.0.1",
42
- "@n8n/vitest-config": "1.21.0"
41
+ "@n8n/vitest-config": "1.21.0",
42
+ "@n8n/eslint-config": "0.0.1"
43
43
  },
44
44
  "license": "SEE LICENSE IN LICENSE.md",
45
45
  "homepage": "https://n8n.io",