@resq-systems/security 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,529 @@
1
+ import { Exit, Option, Schema } from "effect";
2
+ import DOMPurify from "dompurify";
3
+ //#region src/sanitize.ts
4
+ /**
5
+ * Schema for URL protocol validation
6
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
7
+ */
8
+ const UrlProtocolSchema = Schema.Literals([
9
+ "http:",
10
+ "https:",
11
+ "mailto:",
12
+ "tel:",
13
+ "ftp:"
14
+ ]);
15
+ /**
16
+ * Schema for PII redaction options
17
+ * @compliance NIST 800-53 AU-3 (Content of Audit Records)
18
+ */
19
+ const PIIRedactionOptionsSchema = Schema.Struct({
20
+ redactEmails: Schema.optional(Schema.Boolean),
21
+ redactPhones: Schema.optional(Schema.Boolean),
22
+ redactSSN: Schema.optional(Schema.Boolean),
23
+ redactCreditCards: Schema.optional(Schema.Boolean),
24
+ redactIPs: Schema.optional(Schema.Boolean),
25
+ redactDates: Schema.optional(Schema.Boolean)
26
+ });
27
+ /**
28
+ * Schema for user input validation options
29
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
30
+ */
31
+ const UserInputOptionsSchema = Schema.Struct({
32
+ maxLength: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
33
+ allowHtml: Schema.optional(Schema.Boolean),
34
+ allowNewlines: Schema.optional(Schema.Boolean),
35
+ trimWhitespace: Schema.optional(Schema.Boolean)
36
+ });
37
+ /**
38
+ * Schema for safe URL - validates URL format and protocol
39
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
40
+ */
41
+ const SafeUrlSchema = Schema.String.check(Schema.makeFilter((url) => {
42
+ if (!url || url.trim() === "") return false;
43
+ if (url.startsWith("/") && !url.startsWith("//")) return true;
44
+ try {
45
+ const parsed = new URL(url);
46
+ return [
47
+ "http:",
48
+ "https:",
49
+ "mailto:"
50
+ ].includes(parsed.protocol);
51
+ } catch {
52
+ return /^[a-zA-Z0-9/_.-]+$/.test(url);
53
+ }
54
+ }, { message: "Invalid or unsafe URL" }));
55
+ /**
56
+ * Schema for sanitized HTML-safe string (validates as string; escaping done at runtime)
57
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
58
+ */
59
+ const SanitizedStringSchema = Schema.String;
60
+ /**
61
+ * Schema for email address validation
62
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
63
+ */
64
+ const EmailSchema = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/));
65
+ /**
66
+ * Schema for phone number validation (US format)
67
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
68
+ */
69
+ const PhoneNumberSchema = Schema.String.check(Schema.isPattern(/^(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/));
70
+ /**
71
+ * Schema for SSN validation (US format)
72
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
73
+ */
74
+ const SSNSchema = Schema.String.check(Schema.isPattern(/^\d{3}[-\s]?\d{2}[-\s]?\d{4}$/));
75
+ /**
76
+ * Schema for credit card number validation
77
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
78
+ */
79
+ const CreditCardSchema = Schema.String.check(Schema.isPattern(/^(?:\d{4}[-\s]?){3}\d{4}$|^\d{15,16}$/));
80
+ /**
81
+ * Schema for IPv4 address validation
82
+ */
83
+ const IPv4Schema = Schema.String.check(Schema.isPattern(/^(?:\d{1,3}\.){3}\d{1,3}$/));
84
+ /**
85
+ * Escapes special HTML characters in a string to their corresponding HTML entities,
86
+ * preventing direct injection of HTML and JavaScript when rendering untrusted content.
87
+ *
88
+ * @param text - The plain text to escape.
89
+ * @returns The escaped string safe for HTML rendering.
90
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * escapeHtml('<script>alert("xss")<\/script>');
95
+ * // "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"
96
+ * ```
97
+ */
98
+ const escapeHtml = (text) => {
99
+ if (!text || typeof text !== "string") return "";
100
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#039;");
101
+ };
102
+ /**
103
+ * Validates and sanitizes a user-supplied URL using Effect Schema.
104
+ * Returns an Exit with the sanitized URL or an error.
105
+ *
106
+ * @param url - The URL to be validated and sanitized.
107
+ * @param allowedProtocols - Array of allowed URL protocols.
108
+ * @returns Exit containing the sanitized URL or an error.
109
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * const result = sanitizeUrlEffect('https://example.com');
114
+ * // Exit.succeed('https://example.com')
115
+ *
116
+ * const invalid = sanitizeUrlEffect('javascript:alert(1)');
117
+ * // Exit.fail(...)
118
+ * ```
119
+ */
120
+ const sanitizeUrlEffect = (url, allowedProtocols = [
121
+ "http:",
122
+ "https:",
123
+ "mailto:"
124
+ ]) => {
125
+ const CustomSafeUrlSchema = Schema.String.check(Schema.makeFilter((u) => {
126
+ if (!u || u.trim() === "") return false;
127
+ const trimmed = u.trim();
128
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) return true;
129
+ try {
130
+ const parsed = new URL(trimmed);
131
+ if (!allowedProtocols.includes(parsed.protocol)) return false;
132
+ if (parsed.hostname.includes("javascript:") || parsed.hostname.includes("data:")) return false;
133
+ return true;
134
+ } catch {
135
+ return /^[a-zA-Z0-9/_.-]+$/.test(trimmed) && !trimmed.includes("javascript:") && !trimmed.includes("data:");
136
+ }
137
+ }, { message: "Invalid or unsafe URL" }));
138
+ return Schema.decodeUnknownExit(CustomSafeUrlSchema)(url);
139
+ };
140
+ /**
141
+ * Validates and sanitizes a user-supplied URL, ensuring it conforms to allowed protocols
142
+ * and is not a vector for injection attacks like `javascript:` or `data:`.
143
+ *
144
+ * @param url - The URL to be validated and sanitized.
145
+ * @param allowedProtocols - Array of allowed URL protocols.
146
+ * @returns The sanitized URL if valid, or an empty string if unsafe.
147
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * sanitizeUrl('https://example.com'); // 'https://example.com'
152
+ * sanitizeUrl('javascript:alert(1)'); // ''
153
+ * ```
154
+ */
155
+ const sanitizeUrl = (url, allowedProtocols = [
156
+ "http:",
157
+ "https:",
158
+ "mailto:"
159
+ ]) => {
160
+ const result = sanitizeUrlEffect(url, allowedProtocols);
161
+ return Exit.isSuccess(result) ? result.value : "";
162
+ };
163
+ let purifyInstance;
164
+ const getPurify = () => {
165
+ if (purifyInstance !== void 0) return purifyInstance;
166
+ if (typeof window !== "undefined") purifyInstance = DOMPurify;
167
+ else try {
168
+ const nodeModule = globalThis.process?.getBuiltinModule?.("module");
169
+ if (!nodeModule) {
170
+ purifyInstance = null;
171
+ return purifyInstance;
172
+ }
173
+ const { JSDOM } = nodeModule.createRequire(import.meta.url)("jsdom");
174
+ purifyInstance = DOMPurify(new JSDOM("").window);
175
+ } catch {
176
+ purifyInstance = null;
177
+ }
178
+ return purifyInstance;
179
+ };
180
+ /**
181
+ * Sanitizes HTML to prevent XSS attacks.
182
+ * Uses DOMPurify under the hood. If DOM is not available (e.g. server-side without JSDOM),
183
+ * it falls back to escaping all HTML characters for safety.
184
+ *
185
+ * NOTE: Server-side HTML sanitization requires `jsdom` to be installed in the consuming application
186
+ * environment; otherwise, it will fall back to escaping HTML characters.
187
+ *
188
+ * @param html - The HTML string to sanitize.
189
+ * @param options - Optional DOMPurify configuration.
190
+ * @returns The sanitized HTML string.
191
+ */
192
+ const sanitizeHtml = (html, options) => {
193
+ if (!html || typeof html !== "string") return "";
194
+ const purify = getPurify();
195
+ if (purify) return purify.sanitize(html, options);
196
+ return escapeHtml(html);
197
+ };
198
+ /**
199
+ * Validates user input using Effect Schema and returns an Exit.
200
+ *
201
+ * @param input - User input to validate and sanitize.
202
+ * @param options - Validation options.
203
+ * @returns Exit containing sanitized input or error.
204
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
205
+ */
206
+ const validateUserInputEffect = (input, options = {}) => {
207
+ const { maxLength = 500, allowHtml = false, allowNewlines = false, trimWhitespace = true } = options;
208
+ const parsed = Schema.decodeUnknownExit(Schema.String)(input);
209
+ if (Exit.isFailure(parsed)) return parsed;
210
+ let result = parsed.value;
211
+ if (trimWhitespace) result = result.trim();
212
+ if (!allowHtml) {
213
+ let prev;
214
+ do {
215
+ prev = result;
216
+ result = result.replaceAll(/<[^>]*>/g, "");
217
+ } while (result !== prev);
218
+ } else result = sanitizeHtml(result);
219
+ if (!allowNewlines) result = result.replaceAll(/[\r\n]+/g, " ");
220
+ result = result.replaceAll(/\s+/g, " ");
221
+ let prevScheme;
222
+ do {
223
+ prevScheme = result;
224
+ result = result.replaceAll(/javascript:/gi, "").replaceAll(/data:/gi, "").replaceAll(/vbscript:/gi, "").replaceAll(/on\w+=/gi, "");
225
+ } while (result !== prevScheme);
226
+ return Exit.succeed(result.slice(0, maxLength));
227
+ };
228
+ /**
229
+ * Validates and sanitizes generic user input by trimming, removing HTML tags (unless allowed),
230
+ * normalizing whitespace, and removing dangerous patterns to prevent XSS and basic injection flaws.
231
+ *
232
+ * @param input - User input to validate and sanitize.
233
+ * @param maxLength - Maximum allowed input length. Excess will be truncated.
234
+ * @param allowHtml - If true, HTML tags are preserved; otherwise, all tags are stripped.
235
+ * @returns Sanitized input string with length at most `maxLength`.
236
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * validateUserInput('<p>Hello!</p>', 50); // "Hello!"
241
+ * validateUserInput('<script>alert(1)<\/script>test', 100); // "test"
242
+ * ```
243
+ */
244
+ const validateUserInput = (input, maxLength = 500, allowHtml = false) => {
245
+ if (!input || typeof input !== "string") return "";
246
+ const result = validateUserInputEffect(input, {
247
+ maxLength,
248
+ allowHtml
249
+ });
250
+ return Exit.isSuccess(result) ? result.value : "";
251
+ };
252
+ /**
253
+ * Recursively removes dangerous prototype pollution keys from an object.
254
+ */
255
+ const sanitizeObject = (val, depth = 0) => {
256
+ if (depth > 50) return;
257
+ if (typeof val !== "object" || val === null) return;
258
+ if (Array.isArray(val)) {
259
+ for (const item of val) sanitizeObject(item, depth + 1);
260
+ return;
261
+ }
262
+ const dangerous = [
263
+ "__proto__",
264
+ "constructor",
265
+ "prototype"
266
+ ];
267
+ const obj = val;
268
+ for (const key of dangerous) if (key in obj) delete obj[key];
269
+ for (const key of Object.keys(obj)) sanitizeObject(obj[key], depth + 1);
270
+ };
271
+ /**
272
+ * Safely parses JSON with Effect Schema validation and prototype pollution protection.
273
+ *
274
+ * @template A - The expected schema type
275
+ * @param jsonString - The JSON string to parse.
276
+ * @param schema - Effect Schema to validate against.
277
+ * @returns Option containing the parsed and validated object.
278
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * const UserSchema = S.Struct({ name: S.String, age: S.Number });
283
+ * const result = parseJsonWithSchema('{"name":"John","age":30}', UserSchema);
284
+ * // Option.some({ name: 'John', age: 30 })
285
+ * ```
286
+ */
287
+ const parseJsonWithSchema = (jsonString, schema) => {
288
+ if (!jsonString || typeof jsonString !== "string") return Option.none();
289
+ try {
290
+ const sanitized = jsonString.replaceAll(/\)\s*\{/g, ") {}").replaceAll(/\]\s*\{/g, "] {}").replaceAll(/\}\s*\{/g, "} {}");
291
+ const parsed = JSON.parse(sanitized);
292
+ sanitizeObject(parsed);
293
+ const result = Schema.decodeUnknownExit(schema)(parsed);
294
+ return Exit.isSuccess(result) ? Option.some(result.value) : Option.none();
295
+ } catch {
296
+ return Option.none();
297
+ }
298
+ };
299
+ /**
300
+ * Sanitizes and safely parses a JSON string, removing suspicious syntax elements that could
301
+ * potentially result in JSON polyglot exploits or prototype pollution.
302
+ *
303
+ * The result is returned as `unknown` — this function performs **no** schema
304
+ * validation, so it cannot honestly promise any concrete shape for
305
+ * attacker-controlled input. Narrow the result yourself, or prefer
306
+ * {@link parseJsonWithSchema}, which validates against an Effect Schema and
307
+ * returns a typed `Option`.
308
+ *
309
+ * @param jsonString - The JSON string to sanitize and parse.
310
+ * @returns The parsed value (as `unknown`) if valid, or `null` if invalid.
311
+ * @compliance NIST 800-53 SI-10 (Information Input Validation)
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * const obj = sanitizeJson('{"foo":"bar"}');
316
+ * // obj: unknown — narrow before use, or use parseJsonWithSchema
317
+ * ```
318
+ */
319
+ const sanitizeJson = (jsonString) => {
320
+ if (!jsonString || typeof jsonString !== "string") return null;
321
+ try {
322
+ const sanitized = jsonString.replaceAll(/\)\s*\{/g, ") {}").replaceAll(/\]\s*\{/g, "] {}").replaceAll(/\}\s*\{/g, "} {}");
323
+ const parsed = JSON.parse(sanitized);
324
+ sanitizeObject(parsed);
325
+ return parsed;
326
+ } catch {
327
+ return null;
328
+ }
329
+ };
330
+ /**
331
+ * Strips ANSI escape codes from a string.
332
+ * Useful for cleaning terminal output before logging to files.
333
+ *
334
+ * @param text - The text potentially containing ANSI codes.
335
+ * @returns The text with ANSI codes removed.
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * stripAnsi('\x1b[31mRed text\x1b[0m'); // 'Red text'
340
+ * ```
341
+ */
342
+ const stripAnsi = (text) => {
343
+ if (!text || typeof text !== "string") return "";
344
+ return text.replaceAll(/\x1b\[[0-9;]*m/g, "");
345
+ };
346
+ /**
347
+ * PII pattern definitions with Effect Schema validation
348
+ * @compliance NIST 800-53 AU-3 (Content of Audit Records)
349
+ */
350
+ const PII_PATTERNS = {
351
+ ssn: {
352
+ pattern: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g,
353
+ marker: "[SSN]"
354
+ },
355
+ creditCard: {
356
+ pattern: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
357
+ marker: "[CREDIT_CARD]"
358
+ },
359
+ creditCardAlt: {
360
+ pattern: /\b\d{15,16}\b/g,
361
+ marker: "[CREDIT_CARD]"
362
+ },
363
+ email: {
364
+ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
365
+ marker: "[EMAIL]"
366
+ },
367
+ phone: {
368
+ pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
369
+ marker: "[PHONE]"
370
+ },
371
+ ipv4: {
372
+ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
373
+ marker: "[IP_ADDRESS]"
374
+ },
375
+ ipv6: {
376
+ pattern: /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/g,
377
+ marker: "[IP_ADDRESS]"
378
+ },
379
+ date: {
380
+ pattern: /\b(?:\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2})\b/g,
381
+ marker: "[DATE]"
382
+ }
383
+ };
384
+ /**
385
+ * Redacts PII from text using Effect Schema validated options.
386
+ *
387
+ * @param text - The text to redact PII from.
388
+ * @param options - Configuration options for redaction.
389
+ * @returns Exit containing redacted text or error.
390
+ * @compliance NIST 800-53 AU-3 (Content of Audit Records)
391
+ */
392
+ const redactPIIEffect = (text, options = {}) => {
393
+ const parsed = Schema.decodeUnknownExit(PIIRedactionOptionsSchema)(options);
394
+ if (Exit.isFailure(parsed)) return Exit.failCause(parsed.cause);
395
+ const { redactEmails = true, redactPhones = true, redactSSN = true, redactCreditCards = true, redactIPs = true, redactDates = false } = parsed.value;
396
+ let result = text;
397
+ if (redactSSN) result = result.replaceAll(PII_PATTERNS.ssn.pattern, PII_PATTERNS.ssn.marker);
398
+ if (redactCreditCards) {
399
+ result = result.replaceAll(PII_PATTERNS.creditCard.pattern, PII_PATTERNS.creditCard.marker);
400
+ result = result.replaceAll(PII_PATTERNS.creditCardAlt.pattern, PII_PATTERNS.creditCardAlt.marker);
401
+ }
402
+ if (redactEmails) result = result.replaceAll(PII_PATTERNS.email.pattern, PII_PATTERNS.email.marker);
403
+ if (redactPhones) result = result.replaceAll(PII_PATTERNS.phone.pattern, PII_PATTERNS.phone.marker);
404
+ if (redactIPs) {
405
+ result = result.replaceAll(PII_PATTERNS.ipv4.pattern, PII_PATTERNS.ipv4.marker);
406
+ result = result.replaceAll(PII_PATTERNS.ipv6.pattern, PII_PATTERNS.ipv6.marker);
407
+ }
408
+ if (redactDates) result = result.replaceAll(PII_PATTERNS.date.pattern, PII_PATTERNS.date.marker);
409
+ return Exit.succeed(result);
410
+ };
411
+ /**
412
+ * Redacts common PII patterns in a string for safe logging.
413
+ * Detects and masks SSNs, credit cards, emails, phone numbers, etc.
414
+ *
415
+ * @param text - The text to redact PII from.
416
+ * @param options - Configuration options for redaction.
417
+ * @returns The text with PII patterns replaced with redaction markers.
418
+ * @compliance NIST 800-53 AU-3 (Content of Audit Records)
419
+ *
420
+ * @example
421
+ * ```typescript
422
+ * redactPII('Contact john@example.com or call 555-123-4567');
423
+ * // 'Contact [EMAIL] or call [PHONE]'
424
+ *
425
+ * redactPII('SSN: 123-45-6789');
426
+ * // 'SSN: [SSN]'
427
+ * ```
428
+ */
429
+ const redactPII = (text, options = {}) => {
430
+ if (!text || typeof text !== "string") return "";
431
+ const { redactEmails = true, redactPhones = true, redactSSN = true, redactCreditCards = true, redactIPs = true, redactDates = false, customPatterns = [] } = options;
432
+ const result = redactPIIEffect(text, {
433
+ redactEmails,
434
+ redactPhones,
435
+ redactSSN,
436
+ redactCreditCards,
437
+ redactIPs,
438
+ redactDates
439
+ });
440
+ let output = Exit.isSuccess(result) ? result.value : text;
441
+ for (const { pattern, replacement } of customPatterns) output = output.replaceAll(pattern, replacement);
442
+ return output;
443
+ };
444
+ /**
445
+ * Creates a safe string representation of an object for logging,
446
+ * automatically redacting sensitive fields.
447
+ *
448
+ * @param obj - The object to stringify.
449
+ * @param sensitiveKeys - Array of key names to redact.
450
+ * @param indent - JSON indentation (default: 2).
451
+ * @returns A JSON string with sensitive values redacted.
452
+ * @compliance NIST 800-53 AU-3 (Content of Audit Records)
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * safeStringify({ user: 'john', password: 'secret123' }, ['password']);
457
+ * // '{\n "user": "john",\n "password": "[REDACTED]"\n}'
458
+ * ```
459
+ */
460
+ const safeStringify = (obj, sensitiveKeys = [
461
+ "password",
462
+ "token",
463
+ "apiKey",
464
+ "secret",
465
+ "authorization",
466
+ "cookie",
467
+ "ssn",
468
+ "creditCard"
469
+ ], indent = 2) => {
470
+ const sensitiveKeysLower = new Set(sensitiveKeys.map((k) => k.toLowerCase()));
471
+ const replacer = (_key, value) => {
472
+ if (_key && sensitiveKeysLower.has(_key.toLowerCase())) return "[REDACTED]";
473
+ return value;
474
+ };
475
+ try {
476
+ return JSON.stringify(obj, replacer, indent);
477
+ } catch {
478
+ return "[Unable to stringify object]";
479
+ }
480
+ };
481
+ /**
482
+ * Validates if a string is a valid email address using Effect Schema.
483
+ *
484
+ * Narrows the input to {@link Email} on success, so validated call sites
485
+ * carry the brand into downstream code.
486
+ *
487
+ * @param email - The string to validate.
488
+ * @returns true if valid email, false otherwise.
489
+ */
490
+ const isValidEmail = (email) => {
491
+ return Exit.isSuccess(Schema.decodeUnknownExit(EmailSchema)(email));
492
+ };
493
+ /**
494
+ * Validates if a string is a valid phone number using Effect Schema.
495
+ *
496
+ * Narrows the input to {@link PhoneNumber} on success.
497
+ *
498
+ * @param phone - The string to validate.
499
+ * @returns true if valid phone number, false otherwise.
500
+ */
501
+ const isValidPhone = (phone) => {
502
+ return Exit.isSuccess(Schema.decodeUnknownExit(PhoneNumberSchema)(phone));
503
+ };
504
+ /**
505
+ * Validates if a string is a valid SSN using Effect Schema.
506
+ *
507
+ * Narrows the input to {@link SSN} on success.
508
+ *
509
+ * @param ssn - The string to validate.
510
+ * @returns true if valid SSN, false otherwise.
511
+ */
512
+ const isValidSSN = (ssn) => {
513
+ return Exit.isSuccess(Schema.decodeUnknownExit(SSNSchema)(ssn));
514
+ };
515
+ /**
516
+ * Validates if a string is a safe URL using Effect Schema.
517
+ *
518
+ * Narrows the input to {@link SafeUrl} on success.
519
+ *
520
+ * @param url - The string to validate.
521
+ * @returns true if valid and safe URL, false otherwise.
522
+ */
523
+ const isValidUrl = (url) => {
524
+ return Exit.isSuccess(Schema.decodeUnknownExit(SafeUrlSchema)(url));
525
+ };
526
+ //#endregion
527
+ export { CreditCardSchema, EmailSchema, IPv4Schema, PIIRedactionOptionsSchema, PhoneNumberSchema, SSNSchema, SafeUrlSchema, SanitizedStringSchema, UrlProtocolSchema, UserInputOptionsSchema, escapeHtml, isValidEmail, isValidPhone, isValidSSN, isValidUrl, parseJsonWithSchema, redactPII, redactPIIEffect, safeStringify, sanitizeHtml, sanitizeJson, sanitizeUrl, sanitizeUrlEffect, stripAnsi, validateUserInput, validateUserInputEffect };
528
+
529
+ //# sourceMappingURL=sanitize.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.mjs","names":["S"],"sources":["../src/sanitize.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @file Input sanitization utilities for XSS prevention and data validation\n * @module utils/sanitize\n * @author ResQ\n * @description Provides type-safe input sanitization using Effect Schema for validation.\n * Includes utilities for HTML escaping, URL validation, PII redaction, and more.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\n\nimport type { Brand } from \"@resq-systems/types\";\nimport { Exit, Option, Schema as S } from \"effect\";\nimport DOMPurify from \"dompurify\";\nimport type { Config, WindowLike } from \"dompurify\";\n\n/**\n * A Schema with DecodingServices constrained to `never`, allowing synchronous decoding.\n */\ntype SyncSchema<T> = S.Codec<T, unknown, never>;\n\n// ============================================\n// Effect Schema Definitions\n// ============================================\n\n/**\n * Schema for URL protocol validation\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UrlProtocolSchema = S.Literals([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"ftp:\"]);\nexport type UrlProtocol = typeof UrlProtocolSchema.Type;\n\n/**\n * Schema for PII redaction options\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const PIIRedactionOptionsSchema = S.Struct({\n\tredactEmails: S.optional(S.Boolean),\n\tredactPhones: S.optional(S.Boolean),\n\tredactSSN: S.optional(S.Boolean),\n\tredactCreditCards: S.optional(S.Boolean),\n\tredactIPs: S.optional(S.Boolean),\n\tredactDates: S.optional(S.Boolean),\n});\nexport type PIIRedactionOptions = typeof PIIRedactionOptionsSchema.Type;\n\n/**\n * Schema for user input validation options\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UserInputOptionsSchema = S.Struct({\n\tmaxLength: S.optional(S.Int.check(S.isGreaterThan(0))),\n\tallowHtml: S.optional(S.Boolean),\n\tallowNewlines: S.optional(S.Boolean),\n\ttrimWhitespace: S.optional(S.Boolean),\n});\nexport type UserInputOptions = typeof UserInputOptionsSchema.Type;\n\n/**\n * Schema for safe URL - validates URL format and protocol\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SafeUrlSchema = S.String.check(\n\tS.makeFilter(\n\t\t(url: string) => {\n\t\t\tif (!url || url.trim() === \"\") return false;\n\t\t\tif (url.startsWith(\"/\") && !url.startsWith(\"//\")) return true;\n\t\t\ttry {\n\t\t\t\tconst parsed = new URL(url);\n\t\t\t\tconst safeProtocols = [\"http:\", \"https:\", \"mailto:\"];\n\t\t\t\treturn safeProtocols.includes(parsed.protocol);\n\t\t\t} catch {\n\t\t\t\treturn /^[a-zA-Z0-9/_.-]+$/.test(url);\n\t\t\t}\n\t\t},\n\t\t{ message: \"Invalid or unsafe URL\" },\n\t),\n);\n/** A string that has passed {@link isValidUrl} — a validated, injection-safe URL. */\nexport type SafeUrl = Brand<string, \"SafeUrl\">;\n\n/**\n * Schema for sanitized HTML-safe string (validates as string; escaping done at runtime)\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SanitizedStringSchema = S.String;\nexport type SanitizedString = typeof SanitizedStringSchema.Type;\n\n/**\n * Schema for email address validation\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const EmailSchema = S.String.check(\n\tS.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/),\n);\n/** A string that has passed {@link isValidEmail}. */\nexport type Email = Brand<string, \"Email\">;\n\n/**\n * Schema for phone number validation (US format)\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const PhoneNumberSchema = S.String.check(\n\tS.isPattern(/^(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}$/),\n);\n/** A string that has passed {@link isValidPhone} (US format). */\nexport type PhoneNumber = Brand<string, \"PhoneNumber\">;\n\n/**\n * Schema for SSN validation (US format)\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SSNSchema = S.String.check(S.isPattern(/^\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}$/));\n/** A string that has passed {@link isValidSSN} (US format). */\nexport type SSN = Brand<string, \"SSN\">;\n\n/**\n * Schema for credit card number validation\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const CreditCardSchema = S.String.check(\n\tS.isPattern(/^(?:\\d{4}[-\\s]?){3}\\d{4}$|^\\d{15,16}$/),\n);\n/** A string matching the {@link CreditCardSchema} pattern. */\nexport type CreditCard = Brand<string, \"CreditCard\">;\n\n/**\n * Schema for IPv4 address validation\n */\nexport const IPv4Schema = S.String.check(S.isPattern(/^(?:\\d{1,3}\\.){3}\\d{1,3}$/));\n/** A string matching the {@link IPv4Schema} dotted-quad pattern. */\nexport type IPv4 = Brand<string, \"IPv4\">;\n\n// ============================================\n// Sanitization Functions\n// ============================================\n\n/**\n * Escapes special HTML characters in a string to their corresponding HTML entities,\n * preventing direct injection of HTML and JavaScript when rendering untrusted content.\n *\n * @param text - The plain text to escape.\n * @returns The escaped string safe for HTML rendering.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * escapeHtml('<script>alert(\"xss\")</script>');\n * // \"&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;\"\n * ```\n */\nexport const escapeHtml = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\treturn text\n\t\t.replaceAll(\"&\", \"&amp;\")\n\t\t.replaceAll(\"<\", \"&lt;\")\n\t\t.replaceAll(\">\", \"&gt;\")\n\t\t.replaceAll('\"', \"&quot;\")\n\t\t.replaceAll(\"'\", \"&#039;\");\n};\n\n/**\n * Validates and sanitizes a user-supplied URL using Effect Schema.\n * Returns an Exit with the sanitized URL or an error.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Array of allowed URL protocols.\n * @returns Exit containing the sanitized URL or an error.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const result = sanitizeUrlEffect('https://example.com');\n * // Exit.succeed('https://example.com')\n *\n * const invalid = sanitizeUrlEffect('javascript:alert(1)');\n * // Exit.fail(...)\n * ```\n */\nexport const sanitizeUrlEffect = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): Exit.Exit<string, S.SchemaError> => {\n\tconst CustomSafeUrlSchema = S.String.check(\n\t\tS.makeFilter(\n\t\t\t(u: string) => {\n\t\t\t\tif (!u || u.trim() === \"\") return false;\n\t\t\t\tconst trimmed = u.trim();\n\t\t\t\tif (trimmed.startsWith(\"/\") && !trimmed.startsWith(\"//\")) return true;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = new URL(trimmed);\n\t\t\t\t\tif (!allowedProtocols.includes(parsed.protocol)) return false;\n\t\t\t\t\tif (parsed.hostname.includes(\"javascript:\") || parsed.hostname.includes(\"data:\")) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t} catch {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t/^[a-zA-Z0-9/_.-]+$/.test(trimmed) &&\n\t\t\t\t\t\t!trimmed.includes(\"javascript:\") &&\n\t\t\t\t\t\t!trimmed.includes(\"data:\")\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ message: \"Invalid or unsafe URL\" },\n\t\t),\n\t);\n\n\treturn S.decodeUnknownExit(CustomSafeUrlSchema)(url);\n};\n\n/**\n * Validates and sanitizes a user-supplied URL, ensuring it conforms to allowed protocols\n * and is not a vector for injection attacks like `javascript:` or `data:`.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Array of allowed URL protocols.\n * @returns The sanitized URL if valid, or an empty string if unsafe.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * sanitizeUrl('https://example.com'); // 'https://example.com'\n * sanitizeUrl('javascript:alert(1)'); // ''\n * ```\n */\nexport const sanitizeUrl = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): string => {\n\tconst result = sanitizeUrlEffect(url, allowedProtocols);\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\nlet purifyInstance: typeof DOMPurify | null | undefined;\n\nconst getPurify = (): typeof DOMPurify | null => {\n\tif (purifyInstance !== undefined) return purifyInstance;\n\n\tif (typeof window !== \"undefined\") {\n\t\tpurifyInstance = DOMPurify;\n\t} else {\n\t\ttry {\n\t\t\t// Resolve `node:module` at runtime (server-side only) via\n\t\t\t// process.getBuiltinModule so browser bundlers never see a static\n\t\t\t// `node:module` import. Available on Node >=20.16 and Bun; absent in\n\t\t\t// browsers, where the `window` branch above is taken instead. jsdom is an\n\t\t\t// optional peer dependency — install it for server-side HTML sanitization.\n\t\t\tconst proc = (globalThis as { process?: { getBuiltinModule?: (m: string) => unknown } })\n\t\t\t\t.process;\n\t\t\tconst nodeModule = proc?.getBuiltinModule?.(\"module\") as\n\t\t\t\t| { createRequire(path: string | URL): (id: string) => unknown }\n\t\t\t\t| undefined;\n\t\t\tif (!nodeModule) {\n\t\t\t\tpurifyInstance = null;\n\t\t\t\treturn purifyInstance;\n\t\t\t}\n\t\t\tconst req = nodeModule.createRequire(import.meta.url);\n\t\t\tconst { JSDOM } = req(\"jsdom\") as {\n\t\t\t\tJSDOM: new (\n\t\t\t\t\thtml?: string,\n\t\t\t\t) => {\n\t\t\t\t\twindow: WindowLike;\n\t\t\t\t};\n\t\t\t};\n\t\t\tconst dom = new JSDOM(\"\");\n\t\t\tpurifyInstance = DOMPurify(dom.window);\n\t\t} catch {\n\t\t\tpurifyInstance = null;\n\t\t}\n\t}\n\treturn purifyInstance;\n};\n\n/**\n * Sanitizes HTML to prevent XSS attacks.\n * Uses DOMPurify under the hood. If DOM is not available (e.g. server-side without JSDOM),\n * it falls back to escaping all HTML characters for safety.\n *\n * NOTE: Server-side HTML sanitization requires `jsdom` to be installed in the consuming application\n * environment; otherwise, it will fall back to escaping HTML characters.\n *\n * @param html - The HTML string to sanitize.\n * @param options - Optional DOMPurify configuration.\n * @returns The sanitized HTML string.\n */\nexport const sanitizeHtml = (html: string, options?: Config): string => {\n\tif (!html || typeof html !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst purify = getPurify();\n\tif (purify) {\n\t\treturn purify.sanitize(html, options) as string;\n\t}\n\n\treturn escapeHtml(html);\n};\n\n/**\n * Validates user input using Effect Schema and returns an Exit.\n *\n * @param input - User input to validate and sanitize.\n * @param options - Validation options.\n * @returns Exit containing sanitized input or error.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const validateUserInputEffect = (\n\tinput: string,\n\toptions: UserInputOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst {\n\t\tmaxLength = 500,\n\t\tallowHtml = false,\n\t\tallowNewlines = false,\n\t\ttrimWhitespace = true,\n\t} = options;\n\n\tconst parsed = S.decodeUnknownExit(S.String)(input);\n\tif (Exit.isFailure(parsed)) return parsed;\n\n\tlet result = parsed.value;\n\n\tif (trimWhitespace) {\n\t\tresult = result.trim();\n\t}\n\n\tif (!allowHtml) {\n\t\tlet prev: string;\n\t\tdo {\n\t\t\tprev = result;\n\t\t\tresult = result.replaceAll(/<[^>]*>/g, \"\");\n\t\t} while (result !== prev);\n\t} else {\n\t\tresult = sanitizeHtml(result);\n\t}\n\n\tif (!allowNewlines) {\n\t\tresult = result.replaceAll(/[\\r\\n]+/g, \" \");\n\t}\n\n\tresult = result.replaceAll(/\\s+/g, \" \");\n\n\t// Loop until stable to prevent bypass via nested patterns (e.g. \"javascrjavascript:ipt:\")\n\tlet prevScheme: string;\n\tdo {\n\t\tprevScheme = result;\n\t\tresult = result\n\t\t\t.replaceAll(/javascript:/gi, \"\")\n\t\t\t.replaceAll(/data:/gi, \"\")\n\t\t\t.replaceAll(/vbscript:/gi, \"\")\n\t\t\t.replaceAll(/on\\w+=/gi, \"\");\n\t} while (result !== prevScheme);\n\n\treturn Exit.succeed(result.slice(0, maxLength));\n};\n\n/**\n * Validates and sanitizes generic user input by trimming, removing HTML tags (unless allowed),\n * normalizing whitespace, and removing dangerous patterns to prevent XSS and basic injection flaws.\n *\n * @param input - User input to validate and sanitize.\n * @param maxLength - Maximum allowed input length. Excess will be truncated.\n * @param allowHtml - If true, HTML tags are preserved; otherwise, all tags are stripped.\n * @returns Sanitized input string with length at most `maxLength`.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * validateUserInput('<p>Hello!</p>', 50); // \"Hello!\"\n * validateUserInput('<script>alert(1)</script>test', 100); // \"test\"\n * ```\n */\nexport const validateUserInput = (input: string, maxLength = 500, allowHtml = false): string => {\n\tif (!input || typeof input !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst result = validateUserInputEffect(input, { maxLength, allowHtml });\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\n/**\n * Recursively removes dangerous prototype pollution keys from an object.\n */\nconst sanitizeObject = (val: unknown, depth = 0): void => {\n\tif (depth > 50) {\n\t\treturn;\n\t}\n\tif (typeof val !== \"object\" || val === null) {\n\t\treturn;\n\t}\n\tif (Array.isArray(val)) {\n\t\tfor (const item of val) {\n\t\t\tsanitizeObject(item, depth + 1);\n\t\t}\n\t\treturn;\n\t}\n\tconst dangerous = [\"__proto__\", \"constructor\", \"prototype\"];\n\tconst obj = val as Record<string, unknown>;\n\tfor (const key of dangerous) {\n\t\tif (key in obj) {\n\t\t\tdelete obj[key];\n\t\t}\n\t}\n\tfor (const key of Object.keys(obj)) {\n\t\tsanitizeObject(obj[key], depth + 1);\n\t}\n};\n\n/**\n * Safely parses JSON with Effect Schema validation and prototype pollution protection.\n *\n * @template A - The expected schema type\n * @param jsonString - The JSON string to parse.\n * @param schema - Effect Schema to validate against.\n * @returns Option containing the parsed and validated object.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const UserSchema = S.Struct({ name: S.String, age: S.Number });\n * const result = parseJsonWithSchema('{\"name\":\"John\",\"age\":30}', UserSchema);\n * // Option.some({ name: 'John', age: 30 })\n * ```\n */\nexport const parseJsonWithSchema = <A>(\n\tjsonString: string,\n\tschema: SyncSchema<A>,\n): Option.Option<A> => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn Option.none();\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\tconst result = S.decodeUnknownExit(schema)(parsed);\n\t\treturn Exit.isSuccess(result) ? Option.some(result.value as A) : Option.none();\n\t} catch {\n\t\treturn Option.none();\n\t}\n};\n\n/**\n * Sanitizes and safely parses a JSON string, removing suspicious syntax elements that could\n * potentially result in JSON polyglot exploits or prototype pollution.\n *\n * The result is returned as `unknown` — this function performs **no** schema\n * validation, so it cannot honestly promise any concrete shape for\n * attacker-controlled input. Narrow the result yourself, or prefer\n * {@link parseJsonWithSchema}, which validates against an Effect Schema and\n * returns a typed `Option`.\n *\n * @param jsonString - The JSON string to sanitize and parse.\n * @returns The parsed value (as `unknown`) if valid, or `null` if invalid.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const obj = sanitizeJson('{\"foo\":\"bar\"}');\n * // obj: unknown — narrow before use, or use parseJsonWithSchema\n * ```\n */\nexport const sanitizeJson = (jsonString: string): unknown => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed: unknown = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/**\n * Strips ANSI escape codes from a string.\n * Useful for cleaning terminal output before logging to files.\n *\n * @param text - The text potentially containing ANSI codes.\n * @returns The text with ANSI codes removed.\n *\n * @example\n * ```typescript\n * stripAnsi('\\x1b[31mRed text\\x1b[0m'); // 'Red text'\n * ```\n */\nexport const stripAnsi = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\t// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI codes require control characters\n\treturn text.replaceAll(/\\x1b\\[[0-9;]*m/g, \"\");\n};\n\n// ============================================\n// PII Redaction Functions\n// ============================================\n\n/**\n * PII pattern definitions with Effect Schema validation\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nconst PII_PATTERNS = {\n\tssn: { pattern: /\\b\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}\\b/g, marker: \"[SSN]\" },\n\tcreditCard: { pattern: /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g, marker: \"[CREDIT_CARD]\" },\n\tcreditCardAlt: { pattern: /\\b\\d{15,16}\\b/g, marker: \"[CREDIT_CARD]\" },\n\temail: { pattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b/g, marker: \"[EMAIL]\" },\n\tphone: { pattern: /\\b(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}\\b/g, marker: \"[PHONE]\" },\n\tipv4: { pattern: /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tipv6: { pattern: /\\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tdate: {\n\t\tpattern: /\\b(?:\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4}|\\d{4}[/-]\\d{1,2}[/-]\\d{1,2})\\b/g,\n\t\tmarker: \"[DATE]\",\n\t},\n} as const;\n\n/**\n * Redacts PII from text using Effect Schema validated options.\n *\n * @param text - The text to redact PII from.\n * @param options - Configuration options for redaction.\n * @returns Exit containing redacted text or error.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const redactPIIEffect = (\n\ttext: string,\n\toptions: PIIRedactionOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst parsed = S.decodeUnknownExit(PIIRedactionOptionsSchema)(options);\n\tif (Exit.isFailure(parsed)) return Exit.failCause(parsed.cause);\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t} = parsed.value;\n\n\tlet result = text;\n\n\tif (redactSSN) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ssn.pattern, PII_PATTERNS.ssn.marker);\n\t}\n\n\tif (redactCreditCards) {\n\t\tresult = result.replaceAll(PII_PATTERNS.creditCard.pattern, PII_PATTERNS.creditCard.marker);\n\t\tresult = result.replaceAll(\n\t\t\tPII_PATTERNS.creditCardAlt.pattern,\n\t\t\tPII_PATTERNS.creditCardAlt.marker,\n\t\t);\n\t}\n\n\tif (redactEmails) {\n\t\tresult = result.replaceAll(PII_PATTERNS.email.pattern, PII_PATTERNS.email.marker);\n\t}\n\n\tif (redactPhones) {\n\t\tresult = result.replaceAll(PII_PATTERNS.phone.pattern, PII_PATTERNS.phone.marker);\n\t}\n\n\tif (redactIPs) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv4.pattern, PII_PATTERNS.ipv4.marker);\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv6.pattern, PII_PATTERNS.ipv6.marker);\n\t}\n\n\tif (redactDates) {\n\t\tresult = result.replaceAll(PII_PATTERNS.date.pattern, PII_PATTERNS.date.marker);\n\t}\n\n\treturn Exit.succeed(result);\n};\n\n/**\n * Redacts common PII patterns in a string for safe logging.\n * Detects and masks SSNs, credit cards, emails, phone numbers, etc.\n *\n * @param text - The text to redact PII from.\n * @param options - Configuration options for redaction.\n * @returns The text with PII patterns replaced with redaction markers.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * redactPII('Contact john@example.com or call 555-123-4567');\n * // 'Contact [EMAIL] or call [PHONE]'\n *\n * redactPII('SSN: 123-45-6789');\n * // 'SSN: [SSN]'\n * ```\n */\nexport const redactPII = (\n\ttext: string,\n\toptions: PIIRedactionOptions & {\n\t\tcustomPatterns?: Array<{ pattern: RegExp; replacement: string }>;\n\t} = {},\n): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t\tcustomPatterns = [],\n\t} = options;\n\n\tconst result = redactPIIEffect(text, {\n\t\tredactEmails,\n\t\tredactPhones,\n\t\tredactSSN,\n\t\tredactCreditCards,\n\t\tredactIPs,\n\t\tredactDates,\n\t});\n\n\tlet output = Exit.isSuccess(result) ? result.value : text;\n\n\tfor (const { pattern, replacement } of customPatterns) {\n\t\toutput = output.replaceAll(pattern, replacement);\n\t}\n\n\treturn output;\n};\n\n/**\n * Creates a safe string representation of an object for logging,\n * automatically redacting sensitive fields.\n *\n * @param obj - The object to stringify.\n * @param sensitiveKeys - Array of key names to redact.\n * @param indent - JSON indentation (default: 2).\n * @returns A JSON string with sensitive values redacted.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * safeStringify({ user: 'john', password: 'secret123' }, ['password']);\n * // '{\\n \"user\": \"john\",\\n \"password\": \"[REDACTED]\"\\n}'\n * ```\n */\nexport const safeStringify = (\n\tobj: unknown,\n\tsensitiveKeys: string[] = [\n\t\t\"password\",\n\t\t\"token\",\n\t\t\"apiKey\",\n\t\t\"secret\",\n\t\t\"authorization\",\n\t\t\"cookie\",\n\t\t\"ssn\",\n\t\t\"creditCard\",\n\t],\n\tindent = 2,\n): string => {\n\tconst sensitiveKeysLower = new Set(sensitiveKeys.map((k) => k.toLowerCase()));\n\n\tconst replacer = (_key: string, value: unknown): unknown => {\n\t\tif (_key && sensitiveKeysLower.has(_key.toLowerCase())) {\n\t\t\treturn \"[REDACTED]\";\n\t\t}\n\t\treturn value;\n\t};\n\n\ttry {\n\t\treturn JSON.stringify(obj, replacer, indent);\n\t} catch {\n\t\treturn \"[Unable to stringify object]\";\n\t}\n};\n\n// ============================================\n// Validation Helpers\n// ============================================\n\n/**\n * Validates if a string is a valid email address using Effect Schema.\n *\n * Narrows the input to {@link Email} on success, so validated call sites\n * carry the brand into downstream code.\n *\n * @param email - The string to validate.\n * @returns true if valid email, false otherwise.\n */\nexport const isValidEmail = (email: string): email is Email => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(EmailSchema)(email));\n};\n\n/**\n * Validates if a string is a valid phone number using Effect Schema.\n *\n * Narrows the input to {@link PhoneNumber} on success.\n *\n * @param phone - The string to validate.\n * @returns true if valid phone number, false otherwise.\n */\nexport const isValidPhone = (phone: string): phone is PhoneNumber => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(PhoneNumberSchema)(phone));\n};\n\n/**\n * Validates if a string is a valid SSN using Effect Schema.\n *\n * Narrows the input to {@link SSN} on success.\n *\n * @param ssn - The string to validate.\n * @returns true if valid SSN, false otherwise.\n */\nexport const isValidSSN = (ssn: string): ssn is SSN => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SSNSchema)(ssn));\n};\n\n/**\n * Validates if a string is a safe URL using Effect Schema.\n *\n * Narrows the input to {@link SafeUrl} on success.\n *\n * @param url - The string to validate.\n * @returns true if valid and safe URL, false otherwise.\n */\nexport const isValidUrl = (url: string): url is SafeUrl => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SafeUrlSchema)(url));\n};\n"],"mappings":"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"}