@resq-systems/security 1.0.1 → 1.0.2

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.
package/README.md CHANGED
@@ -32,11 +32,12 @@ Peer dependency: `effect`. Uses Node.js `crypto` module for encryption.
32
32
  ## Quick Start
33
33
 
34
34
  ```ts
35
- import { encryptData, decryptData, isSafeInput, escapeHtml, redactPII } from "@resq-systems/security";
35
+ import { encryptData, decryptData, toEncryptionKey, isSafeInput, escapeHtml, redactPII } from "@resq-systems/security";
36
36
 
37
- // Encrypt/decrypt
38
- const encrypted = await encryptData("sensitive", "my-secret-key");
39
- const decrypted = await decryptData(encrypted, "my-secret-key");
37
+ // Encrypt/decrypt — the key is a branded `EncryptionKey`; mint it once at the boundary.
38
+ const key = toEncryptionKey(process.env.ENCRYPTION_KEY ?? "my-secret-key");
39
+ const encrypted = await encryptData("sensitive", key); // Ciphertext (branded)
40
+ const decrypted = await decryptData(encrypted, key);
40
41
 
41
42
  // Validate input
42
43
  if (!isSafeInput(userInput)) {
@@ -55,42 +56,40 @@ const clean = redactPII("Email: john@example.com, SSN: 123-45-6789");
55
56
 
56
57
  ### Encryption (`crypto.ts`)
57
58
 
58
- #### `encryptData(plaintext, encryptionKey): Promise<string>`
59
+ #### `encryptData(plaintext, encryptionKey): Promise<Ciphertext>`
59
60
 
60
61
  Encrypts data using AES-256-GCM with scrypt key derivation.
61
62
 
62
63
  - **plaintext** (`string`) -- data to encrypt.
63
- - **encryptionKey** (`string`) -- encryption key/password.
64
- - Returns base64 string containing `salt:iv:authTag:ciphertext`.
64
+ - **encryptionKey** (`EncryptionKey`) -- branded secret; mint with `toEncryptionKey`.
65
+ - Returns a branded `Ciphertext`: a base64 `salt | iv | authTag | ciphertext` envelope.
65
66
 
66
67
  #### `decryptData(encryptedData, encryptionKey): Promise<string>`
67
68
 
68
- Decrypts data produced by `encryptData`.
69
+ Decrypts data produced by `encryptData`. Verifies the GCM auth tag before returning -- tampered or wrong-key input throws.
69
70
 
70
- - **encryptedData** (`string`) -- base64-encoded encrypted data.
71
- - **encryptionKey** (`string`) -- same key used for encryption.
71
+ - **encryptedData** (`Ciphertext`) -- the branded envelope from `encryptData` (read one back from storage via `toCiphertext`).
72
+ - **encryptionKey** (`EncryptionKey`) -- same key used for encryption.
72
73
  - Returns the original plaintext string.
73
74
 
74
- #### `hashData(data): string`
75
+ #### `hashData(data): Sha256Hex`
75
76
 
76
- Hashes data using SHA-256. Non-reversible.
77
+ Hashes data using SHA-256. Non-reversible. Returns a branded 64-char lowercase hex digest. Not for password storage -- use a slow KDF (bcrypt/argon2/scrypt) for password-equivalent material.
77
78
 
78
- - Returns hex-encoded hash string.
79
-
80
- #### `generateSecureToken(length?): string`
79
+ #### `generateSecureToken(length?): SecureToken`
81
80
 
82
81
  Generates a cryptographically secure random token.
83
82
 
84
- - **length** (`number`, default `32`) -- byte length.
85
- - Returns hex string (2x the byte length).
83
+ - **length** (`PositiveInt`, default `toPositiveInt(32)`) -- byte length. Build non-default lengths with `toPositiveInt` from `@resq-systems/types`.
84
+ - Returns a branded `SecureToken`: hex string (2x the byte length).
86
85
 
87
- #### `maskPII(data): string`
86
+ #### `maskPII(data): Masked`
88
87
 
89
- Masks a string, showing first 2 and last 2 characters (e.g. `"Al****ce"`). Returns `"****"` for strings <= 4 chars.
88
+ Masks a string, showing first 2 and last 2 characters (e.g. `"Al****ce"`). Returns `"****"` for strings <= 4 chars. Result is a branded `Masked` string.
90
89
 
91
- #### `maskEmail(email): string`
90
+ #### `maskEmail(email): Masked`
92
91
 
93
- Masks email local part (e.g. `"j***n@example.com"`).
92
+ Masks the email local part while preserving the domain (e.g. `"j***n@example.com"`). Falls back to `maskPII` when the input is not a `local@domain` shape.
94
93
 
95
94
  #### `sanitizeForLogging(obj, sensitiveFields?): Partial<T>`
96
95
 
@@ -104,6 +103,23 @@ sanitizeForLogging({ password: "secret", email: "john@example.com", name: "John"
104
103
  // { password: "[REDACTED]", email: "j***n@example.com", name: "John" }
105
104
  ```
106
105
 
106
+ #### Branded Crypto Types
107
+
108
+ The crypto surface uses nominal (branded) types so a plain `string` cannot be passed where a validated secret or ciphertext is expected. Exported brands: `Ciphertext`, `EncryptionKey`, `SecureToken`, `Sha256Hex`, `Masked`.
109
+
110
+ `EncryptionKey` and `Ciphertext` ship smart constructors (backed by `@resq-systems/types`):
111
+
112
+ | Function | Brand | Behavior |
113
+ |----------|-------|----------|
114
+ | `toEncryptionKey(value)` | `EncryptionKey` | Assert non-empty, brand, or throw |
115
+ | `coerceEncryptionKey(value)` | `EncryptionKey \| null` | Brand, or `null` when empty |
116
+ | `isEncryptionKey(value)` | type guard | `true` when `value` is a usable key |
117
+ | `unsafeEncryptionKey(value)` | `EncryptionKey` | Brand without checking |
118
+ | `toCiphertext(value)` | `Ciphertext` | Assert well-formed base64 envelope, or throw |
119
+ | `coerceCiphertext(value)` | `Ciphertext \| null` | Brand, or `null` when malformed |
120
+ | `isCiphertext(value)` | type guard | `true` for a well-formed envelope |
121
+ | `unsafeCiphertext(value)` | `Ciphertext` | Brand without checking |
122
+
107
123
  ### Threat Detection (`validators.ts`)
108
124
 
109
125
  #### `detectThreatPatterns(input, config?): ThreatDetectionResult`
@@ -172,6 +188,10 @@ Escapes `&`, `<`, `>`, `"`, `'` to HTML entities.
172
188
  escapeHtml('<img onerror="alert(1)">'); // "&lt;img onerror=&quot;alert(1)&quot;&gt;"
173
189
  ```
174
190
 
191
+ #### `sanitizeHtml(html, options?): string`
192
+
193
+ Sanitizes HTML to prevent XSS using DOMPurify. Accepts an optional DOMPurify `Config`. When no DOM is available (server-side without `jsdom`), it falls back to escaping all HTML characters. `jsdom` is an optional peer dependency required only for server-side HTML sanitization.
194
+
175
195
  #### `sanitizeUrl(url, allowedProtocols?): string`
176
196
 
177
197
  Validates and returns a safe URL; returns empty string if unsafe.
@@ -229,9 +249,14 @@ Replaces PII patterns with redaction markers.
229
249
  | `redactPhones` | `boolean` | `true` | `[PHONE]` |
230
250
  | `redactSSN` | `boolean` | `true` | `[SSN]` |
231
251
  | `redactCreditCards` | `boolean` | `true` | `[CREDIT_CARD]` |
232
- | `redactIPs` | `boolean` | `true` | `[IP_ADDRESS]` |
252
+ | `redactIPs` | `boolean` | `true` | `[IP_ADDRESS]` (IPv4 and IPv6) |
253
+ | `redactDates` | `boolean` | `false` | `[DATE]` |
233
254
  | `customPatterns` | `Array<{ pattern, replacement }>` | `[]` | custom |
234
255
 
256
+ #### `redactPIIEffect(text, options?): Exit<string, SchemaError>`
257
+
258
+ Effect-based variant of `redactPII` that validates the options against `PIIRedactionOptionsSchema` and returns an `Exit`. Does not apply `customPatterns`.
259
+
235
260
  #### `safeStringify(obj, sensitiveKeys?, indent?): string`
236
261
 
237
262
  JSON.stringify with automatic redaction of sensitive keys.
@@ -240,20 +265,24 @@ JSON.stringify with automatic redaction of sensitive keys.
240
265
 
241
266
  ### Validation Helpers
242
267
 
243
- | Function | Description |
244
- |----------|-------------|
245
- | `isValidEmail(email)` | Validates email format |
246
- | `isValidPhone(phone)` | Validates US phone format |
247
- | `isValidSSN(ssn)` | Validates US SSN format |
248
- | `isValidUrl(url)` | Validates safe URL |
268
+ Each is a type guard that narrows its input to the matching brand on success.
269
+
270
+ | Function | Narrows to | Description |
271
+ |----------|------------|-------------|
272
+ | `isValidEmail(email)` | `Email` | Validates email format; accepts alphabetic and Punycode/IDN (`xn--…`) TLDs |
273
+ | `isValidPhone(phone)` | `PhoneNumber` | Validates US phone format |
274
+ | `isValidSSN(ssn)` | `SSN` | Validates US SSN format |
275
+ | `isValidUrl(url)` | `SafeUrl` | Validates safe URL |
249
276
 
250
277
  ### Effect Schemas
251
278
 
252
279
  Exported for runtime validation: `SafeUrlSchema`, `EmailSchema`, `PhoneNumberSchema`, `SSNSchema`, `CreditCardSchema`, `IPv4Schema`, `SanitizedStringSchema`, `UrlProtocolSchema`, `PIIRedactionOptionsSchema`, `UserInputOptionsSchema`.
253
280
 
281
+ `EmailSchema` accepts a 2+ character alphabetic TLD **or** a Punycode/IDN `xn--…` TLD (e.g. `.xn--p1ai` for `.рф`), so internationalized domains are not rejected. It is kept in sync with the `EmailAddress` brand in `@resq-systems/email-templates`.
282
+
254
283
  ### Types
255
284
 
256
- Exported types: `ThreatDetectionResult`, `ThreatFinding`, `ThreatType`, `ThreatDetectionConfig`, `PIIRedactionOptions`, `UserInputOptions`, `SafeUrl`, `Email`, `PhoneNumber`, `SSN`, `CreditCard`, `IPv4`, `UrlProtocol`.
285
+ Exported types: `ThreatDetectionResult`, `ThreatFinding`, `ThreatType`, `ThreatDetectionConfig`, `PIIRedactionOptions`, `UserInputOptions`, `SanitizedString`, `SafeUrl`, `Email`, `PhoneNumber`, `SSN`, `CreditCard`, `IPv4`, `UrlProtocol`. Crypto brands: `Ciphertext`, `EncryptionKey`, `SecureToken`, `Sha256Hex`, `Masked`.
257
286
 
258
287
  ## Prerequisites
259
288
 
@@ -262,7 +291,7 @@ Exported types: `ThreatDetectionResult`, `ThreatFinding`, `ThreatType`, `ThreatD
262
291
 
263
292
  ## Configuration
264
293
 
265
- - **Crypto Key**: Ensure `ENCRYPTION_KEY` is set for cryptographic modules (must be a valid hex string of proper bit length).
294
+ - **Crypto Key**: Ensure `ENCRYPTION_KEY` is set for cryptographic modules. Any non-empty secret works -- scrypt stretches it into a 256-bit AES key -- but a high-entropy value (>= 32 bytes) is strongly preferred. Brand it once with `toEncryptionKey` before passing it to `encryptData`/`decryptData`.
266
295
 
267
296
  ## Testing
268
297
 
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.d.mts","names":[],"sources":["../src/sanitize.ts"],"mappings":";;;;;;;;KAiCK,UAAA,MAAgB,MAAA,CAAE,KAAA,CAAM,CAAA;;;;;cAUhB,iBAAA,EAAiB,MAAA,CAAA,QAAA;AAAA,KAClB,WAAA,UAAqB,iBAAA,CAAkB,IAAA;;;;;cAMtC,yBAAA,EAAyB,MAAA,CAAA,MAAA;EAAA;;;;;;;KAQ1B,mBAAA,UAA6B,yBAAA,CAA0B,IAAA;;;;;cAMtD,sBAAA,EAAsB,MAAA,CAAA,MAAA;EAAA;;;;;KAMvB,gBAAA,UAA0B,sBAAA,CAAuB,IAAA;;;;;cAMhD,aAAA,EAAa,MAAA,CAAA,MAAA;;KAiBd,OAAA,GAAU,KAAA;;;;;cAMT,qBAAA,EAAqB,MAAA,CAAA,MAAA;AAAA,KACtB,eAAA,UAAyB,qBAAA,CAAsB,IAAA;;;;;AA1C3D;;;;cAoDa,WAAA,EAAW,MAAA,CAAA,MAAA;AA9CxB;AAAA,KAkDY,KAAA,GAAQ,KAAA;;;;;cAMP,iBAAA,EAAiB,MAAA,CAAA,MAAA;;KAIlB,WAAA,GAAc,KAAA;;;;;cAMb,SAAA,EAAS,MAAA,CAAA,MAAA;;KAEV,GAAA,GAAM,KAAA;;;;;cAML,gBAAA,EAAgB,MAAA,CAAA,MAAA;;KAIjB,UAAA,GAAa,KAAA;;;;cAKZ,UAAA,EAAU,MAAA,CAAA,MAAA;;KAEX,IAAA,GAAO,KAAA;;;;;;;;;;AA/EnB;;;;;cAmGa,UAAA,GAAc,IAAA;;;;;AA5E3B;;;;;AAMA;;;;;AACA;;;;cAoGa,iBAAA,GACZ,GAAA,UACA,gBAAA,YAA2B,WAAA,OACzB,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;AA7FvB;;;;;AAIA;;;;;AAMA;;;;;AAVA,cAyIa,WAAA,GACZ,GAAA,UACA,gBAAA,YAA2B,WAAA;;;;;AAvH5B;;;;;AAEA;;;cA+Ka,YAAA,GAAgB,IAAA,UAAc,OAAA,GAAU,MAAA;;AAzKrD;;;;;AAIA;;cA0La,uBAAA,GACZ,KAAA,UACA,OAAA,GAAS,gBAAA,KACP,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;;;AAxLvB;;;;;AAEA;;;;;AAoBA;;;;cAiOa,iBAAA,GAAqB,KAAA,UAAe,SAAA,WAAiB,SAAA;AAlMlE;;;;;;;;;;;;;;;;AAAA,cAuPa,mBAAA,MACZ,UAAA,UACA,MAAA,EAAQ,UAAA,CAAW,CAAA,MACjB,MAAA,CAAO,MAAA,CAAO,CAAA;AA3MjB;;;;;;;;;AA4DA;;;;;;;;;AAqBA;;AAjFA,cAqPa,YAAA,GAAgB,UAAA;;;;;;;;;;;;;cAiChB,SAAA,GAAa,IAAA;;AAnI1B;;;;;;;cAyKa,eAAA,GACZ,IAAA,UACA,OAAA,GAAS,mBAAA,KACP,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;;AAvHvB;;;;;;;;;;;;;;;;;cAwLa,SAAA,GACZ,IAAA,UACA,OAAA,GAAS,mBAAA;EACR,cAAA,GAAiB,KAAA;IAAQ,OAAA,EAAS,MAAA;IAAQ,WAAA;EAAA;AAAA;;AA7G5C;;;;;AAsCA;;;;;;;;;;cA0Ha,aAAA,GACZ,GAAA,WACA,aAAA,aAUA,MAAA;;;;;;;AAlED;;;cAiGa,YAAA,GAAgB,KAAA,aAAgB,KAAA,IAAS,KAAA;;;;;;;;;cAYzC,YAAA,GAAgB,KAAA,aAAgB,KAAA,IAAS,WAAA;;;;;;AAvDtD;;;cAmEa,UAAA,GAAc,GAAA,aAAc,GAAA,IAAO,GAAA;;;;;;AAxBhD;;;cAoCa,UAAA,GAAc,GAAA,aAAc,GAAA,IAAO,OAAA"}
1
+ {"version":3,"file":"sanitize.d.mts","names":[],"sources":["../src/sanitize.ts"],"mappings":";;;;;;;;KAiCK,UAAA,MAAgB,MAAA,CAAE,KAAA,CAAM,CAAA;;;;;cAUhB,iBAAA,EAAiB,MAAA,CAAA,QAAA;AAAA,KAClB,WAAA,UAAqB,iBAAA,CAAkB,IAAA;;;;;cAMtC,yBAAA,EAAyB,MAAA,CAAA,MAAA;EAAA;;;;;;;KAQ1B,mBAAA,UAA6B,yBAAA,CAA0B,IAAA;;;;;cAMtD,sBAAA,EAAsB,MAAA,CAAA,MAAA;EAAA;;;;;KAMvB,gBAAA,UAA0B,sBAAA,CAAuB,IAAA;;;;;cAMhD,aAAA,EAAa,MAAA,CAAA,MAAA;;KAiBd,OAAA,GAAU,KAAA;;;;;cAMT,qBAAA,EAAqB,MAAA,CAAA,MAAA;AAAA,KACtB,eAAA,UAAyB,qBAAA,CAAsB,IAAA;;;;;AA1C3D;;;;cAoDa,WAAA,EAAW,MAAA,CAAA,MAAA;AA9CxB;AAAA,KAkDY,KAAA,GAAQ,KAAA;;;;;cAMP,iBAAA,EAAiB,MAAA,CAAA,MAAA;;KAIlB,WAAA,GAAc,KAAA;;;;;cAMb,SAAA,EAAS,MAAA,CAAA,MAAA;;KAEV,GAAA,GAAM,KAAA;;;;;cAML,gBAAA,EAAgB,MAAA,CAAA,MAAA;;KAIjB,UAAA,GAAa,KAAA;;;;cAKZ,UAAA,EAAU,MAAA,CAAA,MAAA;;KAEX,IAAA,GAAO,KAAA;;;;;;;;;;AA/EnB;;;;;cAmGa,UAAA,GAAc,IAAA;;;;;AA5E3B;;;;;AAMA;;;;;AACA;;;;cAoGa,iBAAA,GACZ,GAAA,UACA,gBAAA,YAA2B,WAAA,OACzB,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;AA7FvB;;;;;AAIA;;;;;AAMA;;;;;AAVA,cAyIa,WAAA,GACZ,GAAA,UACA,gBAAA,YAA2B,WAAA;;;;;AAvH5B;;;;;AAEA;;;cA+Ka,YAAA,GAAgB,IAAA,UAAc,OAAA,GAAU,MAAA;;AAzKrD;;;;;AAIA;;cA0La,uBAAA,GACZ,KAAA,UACA,OAAA,GAAS,gBAAA,KACP,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;;;AAxLvB;;;;;AAEA;;;;;AAoBA;;;;cAiOa,iBAAA,GAAqB,KAAA,UAAe,SAAA,WAAiB,SAAA;AAlMlE;;;;;;;;;;;;;;;;AAAA,cAuPa,mBAAA,MACZ,UAAA,UACA,MAAA,EAAQ,UAAA,CAAW,CAAA,MACjB,MAAA,CAAO,MAAA,CAAO,CAAA;AA3MjB;;;;;;;;;AA4DA;;;;;;;;;AAqBA;;AAjFA,cAqPa,YAAA,GAAgB,UAAA;;;;;;;;;;;;;cAiChB,SAAA,GAAa,IAAA;;AAnI1B;;;;;;;cAkLa,eAAA,GACZ,IAAA,UACA,OAAA,GAAS,mBAAA,KACP,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;;AAhIvB;;;;;;;;;;;;;;;;;cAiMa,SAAA,GACZ,IAAA,UACA,OAAA,GAAS,mBAAA;EACR,cAAA,GAAiB,KAAA;IAAQ,OAAA,EAAS,MAAA;IAAQ,WAAA;EAAA;AAAA;;AAtH5C;;;;;AA+CA;;;;;;;;;;cA0Ha,aAAA,GACZ,GAAA,WACA,aAAA,aAUA,MAAA;;;;;;;AAlED;;;cAiGa,YAAA,GAAgB,KAAA,aAAgB,KAAA,IAAS,KAAA;;;;;;;;;cAYzC,YAAA,GAAgB,KAAA,aAAgB,KAAA,IAAS,WAAA;;;;;;AAvDtD;;;cAmEa,UAAA,GAAc,GAAA,aAAc,GAAA,IAAO,GAAA;;;;;;AAxBhD;;;cAoCa,UAAA,GAAc,GAAA,aAAc,GAAA,IAAO,OAAA"}
package/lib/sanitize.mjs CHANGED
@@ -365,7 +365,7 @@ const PII_PATTERNS = {
365
365
  marker: "[CREDIT_CARD]"
366
366
  },
367
367
  email: {
368
- pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
368
+ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.(?:xn--[A-Za-z0-9-]+|[A-Za-z]{2,})\b/g,
369
369
  marker: "[EMAIL]"
370
370
  },
371
371
  phone: {
@@ -1 +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 *\n * Accepts a 2+ character alphabetic TLD or a Punycode/IDN `xn--…` TLD (e.g.\n * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Kept in\n * sync with `@resq-systems/email-templates`'s `EmailAddress` brand.\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,}|xn--[A-Za-z0-9-]+)$/),\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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"}
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 *\n * Accepts a 2+ character alphabetic TLD or a Punycode/IDN `xn--…` TLD (e.g.\n * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Kept in\n * sync with `@resq-systems/email-templates`'s `EmailAddress` brand.\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,}|xn--[A-Za-z0-9-]+)$/),\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\t// TLD alternation mirrors `EmailSchema` so IDN/Punycode addresses\n\t// (e.g. `user@example.xn--p1ai`) are redacted, not leaked. The `xn--` branch\n\t// is tried first: unlike the anchored (`$`) validators, this pattern ends in\n\t// `\\b`, so `[A-Za-z]{2,}` would otherwise match just `xn` and stop at the\n\t// hyphen, leaving `--p1ai` unredacted. (Also drops the stray `|` from the\n\t// former `[A-Z|a-z]` class.)\n\temail: {\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:xn--[A-Za-z0-9-]+|[A-Za-z]{2,})\\b/g,\n\t\tmarker: \"[EMAIL]\",\n\t},\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resq-systems/security",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Security utilities: encryption, input validation, schemas, and PII sanitization",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",