@resq-systems/security 1.0.4 → 2.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.
Files changed (108) hide show
  1. package/README.md +236 -33
  2. package/lib/controls/address.d.mts +142 -0
  3. package/lib/controls/address.d.mts.map +1 -0
  4. package/lib/controls/address.mjs +533 -0
  5. package/lib/controls/address.mjs.map +1 -0
  6. package/lib/controls/csrf.d.mts +91 -0
  7. package/lib/controls/csrf.d.mts.map +1 -0
  8. package/lib/controls/csrf.mjs +200 -0
  9. package/lib/controls/csrf.mjs.map +1 -0
  10. package/lib/controls/index.d.mts +8 -0
  11. package/lib/controls/index.mjs +8 -0
  12. package/lib/controls/origin.d.mts +95 -0
  13. package/lib/controls/origin.d.mts.map +1 -0
  14. package/lib/controls/origin.mjs +156 -0
  15. package/lib/controls/origin.mjs.map +1 -0
  16. package/lib/controls/payload.d.mts +84 -0
  17. package/lib/controls/payload.d.mts.map +1 -0
  18. package/lib/controls/payload.mjs +147 -0
  19. package/lib/controls/payload.mjs.map +1 -0
  20. package/lib/controls/query.d.mts +157 -0
  21. package/lib/controls/query.d.mts.map +1 -0
  22. package/lib/controls/query.mjs +368 -0
  23. package/lib/controls/query.mjs.map +1 -0
  24. package/lib/controls/redirect.d.mts +92 -0
  25. package/lib/controls/redirect.d.mts.map +1 -0
  26. package/lib/controls/redirect.mjs +110 -0
  27. package/lib/controls/redirect.mjs.map +1 -0
  28. package/lib/controls/upload.d.mts +108 -0
  29. package/lib/controls/upload.d.mts.map +1 -0
  30. package/lib/controls/upload.mjs +374 -0
  31. package/lib/controls/upload.mjs.map +1 -0
  32. package/lib/crypto.d.mts +18 -5
  33. package/lib/crypto.d.mts.map +1 -1
  34. package/lib/crypto.mjs +35 -24
  35. package/lib/crypto.mjs.map +1 -1
  36. package/lib/hash.d.mts +83 -0
  37. package/lib/hash.d.mts.map +1 -0
  38. package/lib/hash.mjs +111 -0
  39. package/lib/hash.mjs.map +1 -0
  40. package/lib/index.d.mts +18 -2
  41. package/lib/index.mjs +20 -2
  42. package/lib/paths.d.mts +92 -0
  43. package/lib/paths.d.mts.map +1 -0
  44. package/lib/paths.mjs +140 -0
  45. package/lib/paths.mjs.map +1 -0
  46. package/lib/sanitize.d.mts +137 -35
  47. package/lib/sanitize.d.mts.map +1 -1
  48. package/lib/sanitize.mjs +170 -46
  49. package/lib/sanitize.mjs.map +1 -1
  50. package/lib/threats/capec.generated.d.mts +59 -0
  51. package/lib/threats/capec.generated.d.mts.map +1 -0
  52. package/lib/threats/capec.generated.mjs +644 -0
  53. package/lib/threats/capec.generated.mjs.map +1 -0
  54. package/lib/threats/engine.d.mts +94 -0
  55. package/lib/threats/engine.d.mts.map +1 -0
  56. package/lib/threats/engine.mjs +167 -0
  57. package/lib/threats/engine.mjs.map +1 -0
  58. package/lib/threats/index.d.mts +11 -0
  59. package/lib/threats/index.mjs +11 -0
  60. package/lib/threats/rules/datastore.d.mts +13 -0
  61. package/lib/threats/rules/datastore.d.mts.map +1 -0
  62. package/lib/threats/rules/datastore.mjs +366 -0
  63. package/lib/threats/rules/datastore.mjs.map +1 -0
  64. package/lib/threats/rules/index.d.mts +54 -0
  65. package/lib/threats/rules/index.d.mts.map +1 -0
  66. package/lib/threats/rules/index.mjs +121 -0
  67. package/lib/threats/rules/index.mjs.map +1 -0
  68. package/lib/threats/rules/markup.d.mts +28 -0
  69. package/lib/threats/rules/markup.d.mts.map +1 -0
  70. package/lib/threats/rules/markup.mjs +373 -0
  71. package/lib/threats/rules/markup.mjs.map +1 -0
  72. package/lib/threats/rules/protocol.d.mts +49 -0
  73. package/lib/threats/rules/protocol.d.mts.map +1 -0
  74. package/lib/threats/rules/protocol.mjs +175 -0
  75. package/lib/threats/rules/protocol.mjs.map +1 -0
  76. package/lib/threats/rules/system.d.mts +19 -0
  77. package/lib/threats/rules/system.d.mts.map +1 -0
  78. package/lib/threats/rules/system.mjs +455 -0
  79. package/lib/threats/rules/system.mjs.map +1 -0
  80. package/lib/threats/rules/web.d.mts +26 -0
  81. package/lib/threats/rules/web.d.mts.map +1 -0
  82. package/lib/threats/rules/web.mjs +412 -0
  83. package/lib/threats/rules/web.mjs.map +1 -0
  84. package/lib/threats/scoring.d.mts +59 -0
  85. package/lib/threats/scoring.d.mts.map +1 -0
  86. package/lib/threats/scoring.mjs +111 -0
  87. package/lib/threats/scoring.mjs.map +1 -0
  88. package/lib/threats/types.d.mts +245 -0
  89. package/lib/threats/types.d.mts.map +1 -0
  90. package/lib/threats/types.mjs +52 -0
  91. package/lib/threats/types.mjs.map +1 -0
  92. package/lib/threats/variants.d.mts +57 -0
  93. package/lib/threats/variants.d.mts.map +1 -0
  94. package/lib/threats/variants.mjs +144 -0
  95. package/lib/threats/variants.mjs.map +1 -0
  96. package/lib/unicode/confusables.d.mts +82 -0
  97. package/lib/unicode/confusables.d.mts.map +1 -0
  98. package/lib/unicode/confusables.mjs +954 -0
  99. package/lib/unicode/confusables.mjs.map +1 -0
  100. package/lib/unicode/index.d.mts +126 -0
  101. package/lib/unicode/index.d.mts.map +1 -0
  102. package/lib/unicode/index.mjs +288 -0
  103. package/lib/unicode/index.mjs.map +1 -0
  104. package/lib/validators.d.mts +341 -164
  105. package/lib/validators.d.mts.map +1 -1
  106. package/lib/validators.mjs +519 -338
  107. package/lib/validators.mjs.map +1 -1
  108. package/package.json +35 -8
@@ -1,180 +1,165 @@
1
+ import { ThreatFinding, ThreatType } from "./threats/types.mjs";
1
2
  //#region src/validators.d.ts
2
- /**
3
- * Copyright 2026 ResQ Systems, Inc.
4
- *
5
- * Licensed under the Apache License, Version 2.0 (the "License");
6
- * you may not use this file except in compliance with the License.
7
- * You may obtain a copy of the License at
8
- *
9
- * http://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in writing, software
12
- * distributed under the License is distributed on an "AS IS" BASIS,
13
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- * See the License for the specific language governing permissions and
15
- * limitations under the License.
16
- */
17
3
  /**
18
4
  * Outcome of {@link detectThreatPatterns}.
19
5
  *
20
- * `isSafe` is the boolean shortcut; `threats` is the full list of
21
- * findings (one per detector that fired). Use
22
- * {@link getThreatErrorMessage} to render a user-facing message for
23
- * the first finding.
6
+ * `isSafe` is the boolean shortcut; `threats` carries the findings. Prefer
7
+ * {@link scanForThreats}, whose result adds a numeric score and an allow/review/block
8
+ * verdict instead of collapsing everything into one boolean.
24
9
  */
25
10
  interface ThreatDetectionResult {
26
- /** `true` when no detectors fired. Equivalent to `threats.length === 0`. */
11
+ /** `true` when no detector fired. Equivalent to `threats.length === 0`. */
27
12
  isSafe: boolean;
28
- /** All findings produced by enabled detectors, in detector order. */
13
+ /** Findings from the enabled detectors, at most one per weakness category. */
29
14
  threats: ThreatFinding[];
30
15
  }
31
16
  /**
32
- * A single detector hit. Detectors that fire return at most one
33
- * finding per call (one example is enough to reject the input).
17
+ * Minimal shape {@link getThreatErrorMessage} needs.
18
+ *
19
+ * Deliberately narrower than {@link ThreatFinding} so callers can pass a hand-built
20
+ * summary — or a finding from an older version of this package — without having to
21
+ * populate the full record.
34
22
  */
35
- interface ThreatFinding {
36
- /** Which detector matched. */
37
- type: ThreatType;
38
- /** Human-readable description suitable for log lines (not for end users — use {@link getThreatErrorMessage} instead). */
39
- description: string;
40
- /** First 50 chars of the matching substring, for diagnostics. Truncated to prevent leaking large payloads in logs. */
41
- matchedPattern?: string;
23
+ interface ThreatSummary {
24
+ /** Weakness category. The only field the message depends on. */
25
+ readonly type: ThreatType;
26
+ /** Operator-facing description, if available. */
27
+ readonly description?: string;
28
+ /** Matched excerpt, if available. */
29
+ readonly matchedPattern?: string;
42
30
  }
43
31
  /**
44
- * The closed set of threat categories the validators recognize. Add
45
- * new categories here when adding a new detector.
32
+ * Per-detector toggles for {@link detectThreatPatterns}.
33
+ *
34
+ * @deprecated Prefer {@link scanForThreats} with an explicit `contexts` list. These
35
+ * booleans conflate "which weakness am I looking for" with "where is this value
36
+ * going", and the second question is the one that decides whether a signature is
37
+ * evidence or noise. Each flag maps onto a context: `checkXSS` → `html`,
38
+ * `checkSQLInjection` → `sql`, `checkNoSQLInjection` → `nosql`,
39
+ * `checkCommandInjection` → `shell`, `checkPathTraversal` → `filesystem`;
40
+ * `checkHomoglyphs` runs UTS #39 identifier analysis.
46
41
  */
47
- type ThreatType = "xss" | "sql_injection" | "nosql_injection" | "command_injection" | "path_traversal" | "homoglyph";
42
+ interface ThreatDetectionConfig {
43
+ /** Default `true`. Maps to the `html` context. */
44
+ checkXSS?: boolean;
45
+ /** Default `true`. Maps to the `sql` context. */
46
+ checkSQLInjection?: boolean;
47
+ /** Default `true`. Maps to the `nosql` context. */
48
+ checkNoSQLInjection?: boolean;
49
+ /** Default `false` — opt in only when input reaches a shell. Maps to `shell`. */
50
+ checkCommandInjection?: boolean;
51
+ /** Default `true`. Maps to the `filesystem` context. */
52
+ checkPathTraversal?: boolean;
53
+ /** Default `true`. Runs UTS #39 identifier analysis rather than a pattern list. */
54
+ checkHomoglyphs?: boolean;
55
+ }
48
56
  /**
49
- * Detect XSS-style payloads (script tags, event handlers, dangerous
50
- * URI schemes, prototype pollution, …) in a UTF-8 input.
57
+ * Detect XSS payloads — script tags, inline event handlers, dangerous URI schemes,
58
+ * markup sinks — in a value bound for an HTML context.
51
59
  *
52
- * Inputs longer than 100 000 characters are truncated before scanning
53
- * to bound regex evaluation cost and prevent ReDoS on crafted
54
- * payloads. Returns at most one finding — the regex catalog is
55
- * exhaustive enough that the first hit is sufficient for a
56
- * reject-or-sanitize decision.
60
+ * @param input - String to scan. Truncated at 100 000 characters.
61
+ * @returns Empty array, or a single finding of type `"xss"`.
57
62
  *
58
- * @param input - String to scan.
59
- * @returns Empty array when nothing matches, or a single
60
- * {@link ThreatFinding} of type `"xss"`.
63
+ * @remarks
64
+ * Prototype-pollution patterns (`__proto__`, `constructor[`) no longer surface here.
65
+ * They are a distinct weakness class with distinct controls and now report as
66
+ * `prototype_pollution` — see {@link containsPrototypePollution}.
61
67
  *
62
68
  * @example
63
69
  * ```ts
64
70
  * containsXSSPatterns(`<img src=x onerror="alert(1)">`);
65
- * // → [{ type: "xss", description: "...", matchedPattern: "onerror=" }]
71
+ * // → [{ ruleId: "XSS-EVENT-HANDLER-001", type: "xss", severity: "high", … }]
66
72
  * ```
67
73
  */
68
74
  declare function containsXSSPatterns(input: string): ThreatFinding[];
69
75
  /**
70
- * Detect SQL-injection patterns (UNION SELECT, DROP TABLE,
71
- * comment-based bypasses, always-true tautologies, stacked queries)
72
- * in input.
76
+ * Detect prototype-pollution payloads — `__proto__`, `constructor.prototype`, and the
77
+ * nested-object forms that arrive through a JSON body or query-string expansion.
73
78
  *
74
- * **Not a replacement for parameterised queries.** Use this as a
75
- * defense-in-depth signal in addition to a properly bound prepared
76
- * statement, never as the only barrier.
79
+ * **Not the control.** Reject unknown keys with schema validation, build lookup
80
+ * objects with `Object.create(null)`, and use a merge that skips `__proto__`,
81
+ * `constructor`, and `prototype`.
82
+ *
83
+ * @param input - String to scan.
84
+ * @returns Empty array, or a single finding of type `"prototype_pollution"`.
85
+ */
86
+ declare function containsPrototypePollution(input: string): ThreatFinding[];
87
+ /**
88
+ * Detect SQL-injection patterns in a value bound for a query.
89
+ *
90
+ * **Not a replacement for parameterized queries.** A bound parameter is safe whatever
91
+ * keywords it contains; an interpolated one is unsafe however many signatures it
92
+ * dodges. Use this for telemetry alongside binding, never instead of it.
77
93
  *
78
94
  * @param input - String to scan. Truncated at 100 000 characters.
79
95
  * @returns Empty array, or one finding of type `"sql_injection"`.
80
96
  */
81
97
  declare function containsSQLInjection(input: string): ThreatFinding[];
82
98
  /**
83
- * Detect NoSQL-injection patterns — Mongo-style operator injection
84
- * (`$where`, `$ne`, `$regex`), JavaScript-in-query payloads, and
85
- * structural manipulators that can bypass auth filters in document
86
- * stores.
99
+ * Detect NoSQL operator injection — `$where`, `$ne`, `$regex`, and the object and
100
+ * array forms that bypass authentication filters in document stores.
87
101
  *
88
102
  * @param input - String to scan.
89
103
  * @returns Empty array, or one finding of type `"nosql_injection"`.
90
104
  */
91
105
  declare function containsNoSQLInjection(input: string): ThreatFinding[];
92
106
  /**
93
- * Detect shell command-injection patterns: command substitution
94
- * (`$(...)`, backticks), chained dangerous commands (`; rm`, `; curl`,
95
- * …) and shell-piped exec (`| sh`, `| bash`).
107
+ * Detect shell command-injection patterns — command substitution, chained commands,
108
+ * pipes into an interpreter.
96
109
  *
97
- * **Off by default in {@link detectThreatPatterns}** — these patterns
98
- * occasionally fire on legitimate user content. Enable explicitly
99
- * (`checkCommandInjection: true`) only when input flows into a child
100
- * process or shell.
110
+ * **Off by default in {@link detectThreatPatterns}**, because these patterns fire on
111
+ * ordinary prose. Enable only when the value reaches a child process, and prefer
112
+ * spawning with an argv array and `shell: false`, which makes the category moot.
101
113
  *
102
114
  * @param input - String to scan. Truncated at 100 000 characters.
103
115
  * @returns Empty array, or one finding of type `"command_injection"`.
104
116
  */
105
117
  declare function containsCommandInjection(input: string): ThreatFinding[];
106
118
  /**
107
- * Detect path-traversal payloads — `../`, encoded dots, raw absolute
108
- * paths trying to escape a base directory. Pair with `path.resolve()`
109
- * + a `startsWith()` containment check on the canonicalised path
110
- * before reading or writing the file.
119
+ * Detect path-traversal payloads — `../`, its percent-encoded and double-encoded
120
+ * forms, NUL truncation, and references to sensitive system paths.
121
+ *
122
+ * **Not the control.** Use `resolveContainedPath` from
123
+ * `@resq-systems/security/paths`, which resolves the candidate against a base
124
+ * directory and verifies containment — a check that also catches absolute paths and
125
+ * separator tricks no signature enumerates.
111
126
  *
112
127
  * @param input - String to scan.
113
128
  * @returns Empty array, or one finding of type `"path_traversal"`.
114
129
  */
115
130
  declare function containsPathTraversal(input: string): ThreatFinding[];
116
131
  /**
117
- * Detect lookalike Unicode characters (Cyrillic / Greek glyphs that
118
- * render identically to common ASCII letters). The classic phishing
119
- * trick is `paypaӏ.com` (`ӏ` instead of `l`); this detector catches
120
- * the building blocks.
132
+ * Detect visually confusable characters in a **protected identifier**.
121
133
  *
122
- * Use {@link normalizeUnicode} to *replace* homoglyphs with their
123
- * ASCII equivalents — this function only flags their presence.
134
+ * Backed by UTS #39 script analysis rather than a hand-written lookalike table, so it
135
+ * reports the actual signal — a Latin/Cyrillic mix in `pаypal` — instead of flagging
136
+ * every non-ASCII character. Single-script values are not confusable with anything, so
137
+ * `Ольга Иванова` and `東京タワー` pass where the previous implementation rejected both.
124
138
  *
125
- * @param input - String to scan.
126
- * @returns Empty array, or one finding of type `"homoglyph"` (the
127
- * first matched lookalike).
128
- */
129
- declare function containsHomoglyphs(input: string): ThreatFinding[];
130
- /**
131
- * Per-detector toggles for {@link detectThreatPatterns}.
139
+ * Scope this to usernames, domains, org names, and package names. Do **not** run it on
140
+ * prose or on people's names — see {@link validatePersonName}.
132
141
  *
133
- * Defaults: XSS, SQL, NoSQL, path-traversal, and homoglyph detectors
134
- * are **on**; command injection is **off** (false-positive prone).
135
- * Pass `false` to disable a detector or `true` to force-enable
136
- * `checkCommandInjection`.
142
+ * @param input - Identifier to scan.
143
+ * @returns Empty array, or a single finding of type `"homoglyph"`.
137
144
  */
138
- interface ThreatDetectionConfig {
139
- /** Default `true`. */
140
- checkXSS?: boolean;
141
- /** Default `true`. */
142
- checkSQLInjection?: boolean;
143
- /** Default `true`. */
144
- checkNoSQLInjection?: boolean;
145
- /** Default `false` — opt in only when input reaches a shell. */
146
- checkCommandInjection?: boolean;
147
- /** Default `true`. */
148
- checkPathTraversal?: boolean;
149
- /** Default `true`. */
150
- checkHomoglyphs?: boolean;
151
- }
145
+ declare function containsHomoglyphs(input: string): ThreatFinding[];
152
146
  /**
153
- * Run every enabled detector against `input` and aggregate findings.
147
+ * Run the enabled detectors against `input` and aggregate findings.
154
148
  *
155
- * Returns early-but-not-immediately: each individual detector still
156
- * runs to completion, but each detector returns at most one finding,
157
- * so the aggregate threats array is small (≤ 6 entries).
149
+ * @deprecated Prefer {@link scanForThreats}, which takes explicit contexts and returns
150
+ * a score and verdict rather than one boolean. This wrapper maps the legacy toggles
151
+ * onto contexts and keeps the one-finding-per-category shape.
158
152
  *
159
- * Non-string inputs (`null`, `undefined`, numbers, …) are treated as
160
- * safe — wrap caller-side validation around this if you want to
161
- * reject non-strings.
153
+ * Non-string input (`null`, `undefined`, a number) is reported safe — wrap your own
154
+ * type validation around this if you need to reject those.
162
155
  *
163
156
  * @param input - The candidate string.
164
- * @param config - Detector toggles. Defaults turn on everything
165
- * except command-injection.
157
+ * @param config - Detector toggles. Everything except command injection defaults on.
166
158
  * @returns `{ isSafe, threats }`.
167
- *
168
- * @example
169
- * ```ts
170
- * const result = detectThreatPatterns(req.body.query);
171
- * if (!result.isSafe) return new Response(getThreatErrorMessage(result), { status: 400 });
172
- * ```
173
159
  */
174
160
  declare function detectThreatPatterns(input: string, config?: ThreatDetectionConfig): ThreatDetectionResult;
175
161
  /**
176
- * Boolean shortcut over {@link detectThreatPatterns} — discards the
177
- * findings list when you only need a yes/no decision.
162
+ * Boolean shortcut over {@link detectThreatPatterns}.
178
163
  *
179
164
  * @param input - String to test.
180
165
  * @param config - Optional detector toggles.
@@ -182,88 +167,280 @@ declare function detectThreatPatterns(input: string, config?: ThreatDetectionCon
182
167
  */
183
168
  declare function isSafeInput(input: string, config?: ThreatDetectionConfig): boolean;
184
169
  /**
185
- * HTML-entity escape `&`, `<`, `>`, `"`, `'`, and `/` for safe
186
- * insertion into HTML text and attribute contexts.
170
+ * HTML-entity-escape a value being inserted as **element text**.
171
+ *
172
+ * Escapes `&`, `<`, `>`, `"`, `'`, and `/`, which covers text nodes and fully quoted
173
+ * attribute values.
174
+ *
175
+ * **Output encoding is context-dependent.** HTML text, quoted attributes, unquoted
176
+ * attributes, URLs, JavaScript string literals, and CSS each have different rules, and
177
+ * no single function is correct for all of them. This one is correct for text; use
178
+ * {@link escapeHtmlAttribute} for attribute values, `sanitizeUrl` for URLs, and
179
+ * `sanitizeHtml` (DOMPurify) when the value is meant to *be* markup.
180
+ *
181
+ * There is deliberately no CSS-context escaper here, and no general JavaScript-string
182
+ * escaper — hand-rolled versions of those are reliably wrong, and the fix is to stop
183
+ * interpolating untrusted values into style and script *source*. Embedding untrusted
184
+ * *data* in a script element is the one tractable case, because `JSON.stringify` fixes
185
+ * the string boundaries first; {@link encodeJsonForScript} covers that and nothing else.
186
+ *
187
+ * @param input - Untrusted string. Non-string or empty input yields `""`.
188
+ * @returns Entity-escaped output safe to interpolate into HTML text.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * escapeHtmlText('<script>alert("xss")</script>');
193
+ * // "&lt;script&gt;alert(&quot;xss&quot;)&lt;&#x2F;script&gt;"
194
+ * ```
195
+ */
196
+ declare function escapeHtmlText(input: string): string;
197
+ /**
198
+ * HTML-entity-escape a value being inserted as an **attribute value**.
187
199
  *
188
- * **Limited scope.** This is appropriate for plain text destined for
189
- * `textContent` or attribute values, not for unfiltered HTML
190
- * rendering. For rich-text use a vetted sanitizer (DOMPurify on the
191
- * client, sanitize-html or similar on the server).
200
+ * Everything {@link escapeHtmlText} escapes, plus backtick, equals, and whitespace —
201
+ * the characters that let a payload break out of an *unquoted* attribute. That case is
202
+ * precisely what generic "escape for display" helpers get wrong.
192
203
  *
193
- * Returns `""` for non-string or empty input.
204
+ * The ceiling on the unquoted case is injection of a valueless boolean attribute —
205
+ * `autofocus`, `disabled`, `formnovalidate` — not script execution: an injected
206
+ * `onmouseover=…` arrives with its `=` already escaped, so it lands as an attribute
207
+ * whose *name* is the escaped text, with no handler bound.
208
+ *
209
+ * Quote your attributes anyway. This makes an unquoted attribute survivable; it does
210
+ * not make it correct.
211
+ *
212
+ * @param input - Untrusted string. Non-string or empty input yields `""`.
213
+ * @returns Output safe to interpolate into a quoted or unquoted attribute value.
214
+ */
215
+ declare function escapeHtmlAttribute(input: string): string;
216
+ /**
217
+ * HTML-entity-escape a value for display.
218
+ *
219
+ * @deprecated Renamed to {@link escapeHtmlText}, which says what it actually does. The
220
+ * old name suggested a general-purpose "make this safe to display" operation, and
221
+ * callers reasonably read it as attribute-safe — which entity escaping alone is not,
222
+ * for *unquoted* attributes. Behaviour is unchanged; only the name is.
194
223
  *
195
224
  * @param input - Untrusted string.
196
- * @returns Entity-escaped output safe to interpolate into HTML.
225
+ * @returns Entity-escaped output.
197
226
  */
198
227
  declare function sanitizeForDisplay(input: string): string;
199
228
  /**
200
- * Canonicalise a string for safe equality checks against ASCII.
229
+ * Escape a value for inclusion in a log record.
230
+ *
231
+ * This is the control named by the log-injection rules. A log line is a *sink*: a value
232
+ * carrying a newline forges an entry (CWE-117), one carrying a terminal escape rewrites
233
+ * what an operator sees, and one carrying a bidirectional override reorders the line
234
+ * without altering a byte of it.
235
+ *
236
+ * Escaping rather than stripping is deliberate. The record is evidence, so the encoded
237
+ * form is reversible and nothing is silently discarded — contrast `stripAnsi`, which
238
+ * deletes. Structured logging is still the better answer, because it removes the
239
+ * ambiguity this function can only make visible; use both.
201
240
  *
202
- * Two-pass:
203
- * 1. Normalize to NFC (composed form) so combining-character
204
- * sequences don't compare differently from their pre-composed
205
- * counterparts.
206
- * 2. Replace known homoglyphs (Cyrillic `А`, Greek `Ε`, …) with their
207
- * ASCII equivalents (`A`, `E`, …).
241
+ * @param value - Untrusted field value. Non-string or empty input yields `""`.
242
+ * @param options - Optional bounds.
243
+ * @param options.maxLength - Characters read from `value`. Defaults to 2048. Truncation
244
+ * is announced in the output rather than applied silently, and the returned string may
245
+ * exceed this length, because escaping expands.
246
+ * @returns A single-line, control-free rendering of `value`.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * encodeLogValue("alice\nINFO user promoted to admin");
251
+ * // "alice\\nINFO user promoted to admin" — one line, no forged entry
252
+ * ```
253
+ */
254
+ declare function encodeLogValue(value: string, options?: {
255
+ readonly maxLength?: number;
256
+ }): string;
257
+ /**
258
+ * Escape one cell for CSV export.
259
+ *
260
+ * This is the control named by the formula-injection rules. A CSV file is not inert: a
261
+ * cell beginning `=`, `+`, `-`, `@`, tab or CR is evaluated as a formula by Excel,
262
+ * Sheets and LibreOffice when the recipient opens it, so the payload executes on *their*
263
+ * machine, outside the exporting application entirely (CWE-1236).
264
+ *
265
+ * Two separate jobs, in order: neutralise the formula trigger with a leading apostrophe,
266
+ * then apply RFC 4180 quoting so the field cannot break the row.
267
+ *
268
+ * **Only strings are prefixed.** A `number` or `boolean` came from the application's own
269
+ * types and cannot carry a formula, so `-1234` exports as a negative number while
270
+ * `"-1234"` exports as text. Pass numeric columns as numbers, or every negative value in
271
+ * the sheet becomes a string.
272
+ *
273
+ * Three things worth knowing before relying on it:
274
+ * - The leading apostrophe is an Excel convention, **not** an RFC 4180 construct. Readers
275
+ * that do not implement it surface it as a literal character in the data.
276
+ * - NUL is removed rather than escaped, so it does not round-trip.
277
+ * - Scanning the output with `scanForThreats` still reports a finding, by design:
278
+ * `CSV-FORMULA-LEAD-001` sees through the apostrophe and `CSV-DDE-001` is
279
+ * position-independent. The rules describe the *value*; this function protects the
280
+ * *file*. A clean scan is the wrong acceptance test.
281
+ *
282
+ * @param value - Cell value. `null` and `undefined` become `""`.
283
+ * @param options - Optional dialect settings.
284
+ * @param options.delimiter - Field separator the row will be joined with. Defaults to `","`.
285
+ * @returns The escaped field, ready to join into a row.
208
286
  *
209
- * Use before storing user-controlled identifiers (usernames, domain
210
- * names) and before comparing them to a denylist or to each other.
287
+ * @example
288
+ * ```ts
289
+ * escapeCsvField("=WEBSERVICE(\"https://evil.example\")");
290
+ * // quoted, and inert on open
291
+ * escapeCsvField(-1234); // "-1234" — a number, not a formula
292
+ * ```
293
+ */
294
+ declare function escapeCsvField(value: unknown, options?: {
295
+ readonly delimiter?: string;
296
+ }): string;
297
+ /**
298
+ * Escape and join one row for CSV export.
211
299
  *
212
- * Returns `""` for non-string or empty input.
300
+ * @param values - Cell values, in column order.
301
+ * @param options - Optional dialect settings.
302
+ * @param options.delimiter - Field separator. Defaults to `","`.
303
+ * @returns The joined row, without a line terminator.
213
304
  *
214
- * @param input - Raw string from an untrusted source.
215
- * @returns ASCII-normalized, NFC-composed string.
305
+ * @example
306
+ * ```ts
307
+ * toCsvRow(["Ada Lovelace", "=1+1", 42]);
308
+ * ```
309
+ */
310
+ declare function toCsvRow(values: readonly unknown[], options?: {
311
+ readonly delimiter?: string;
312
+ }): string;
313
+ /**
314
+ * Serialise a value for embedding inside a `<script>` element.
315
+ *
316
+ * `JSON.stringify` alone is not safe here. Its output may contain `</script>`, which
317
+ * closes the element from *inside a string literal* — the HTML tokenizer never looks at
318
+ * JavaScript syntax — so the remainder of the payload becomes markup.
319
+ *
320
+ * **Script element content only.** The output contains unescaped `"`, so it must never be
321
+ * placed in an attribute; use {@link escapeHtmlAttribute} there. It is also not a general
322
+ * JavaScript-string escaper — it is safe precisely because `JSON.stringify` has already
323
+ * decided where the string boundaries are.
324
+ *
325
+ * Using `<script type="application/json">` with `JSON.parse(el.textContent)` does **not**
326
+ * remove the need for this: a raw `</script>` in the data closes that element too.
327
+ *
328
+ * @param value - Any JSON-serialisable value.
329
+ * @returns JSON text safe to place between `<script>` tags.
330
+ * @throws {TypeError} If `value` cannot be represented as JSON — `undefined`, a function
331
+ * or a symbol at the top level (for which `JSON.stringify` returns `undefined` rather
332
+ * than a string), a circular structure, or a `BigInt`. Failing loudly is deliberate: a
333
+ * sentinel string would emit a syntax error into the page instead.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * const json = encodeJsonForScript({ name: userName });
338
+ * const html = "<script>window.__DATA__ = " + json + ";</script>";
339
+ * ```
340
+ */
341
+ declare function encodeJsonForScript(value: unknown): string;
342
+ /**
343
+ * Fold non-ASCII lookalike characters onto ASCII and compose to NFC.
344
+ *
345
+ * @deprecated Prefer `getSkeleton` and `analyzeIdentifier` from
346
+ * `@resq-systems/security/unicode`. Rewriting a user's identifier into a different
347
+ * string loses information and only *looks* safe — the durable pattern is to store
348
+ * what they typed, index its skeleton, and compare skeletons for collisions.
349
+ *
350
+ * Now backed by the UTS #39 confusable tables rather than the previous 14-entry map,
351
+ * so coverage is far wider. Combining marks are preserved (`e` + U+0301 still composes
352
+ * to `é`) and ASCII characters are never rewritten.
353
+ *
354
+ * @param input - Raw string from an untrusted source. Non-string input yields `""`.
355
+ * @returns NFC-composed string with non-ASCII confusables folded to ASCII.
216
356
  */
217
357
  declare function normalizeUnicode(input: string): string;
218
358
  /**
219
- * Generic user-facing fallback message. Render this verbatim when a
220
- * detector fires but you don't want to expose which one. Prefer
221
- * {@link getThreatErrorMessage} for category-specific messages.
359
+ * Generic user-facing fallback message. Render verbatim when a detector fires and you
360
+ * do not want to reveal which one.
222
361
  */
223
362
  declare const THREAT_DETECTED_MESSAGE = "Input contains potentially unsafe content";
224
363
  /**
225
- * Boolean refinement helper for use with `zod.string().refine(...)`,
226
- * `effect/Schema.filter(...)`, or any predicate-based validator.
364
+ * Refinement helper for `zod.string().refine(...)`, `effect/Schema.filter(...)`, or
365
+ * any predicate-based validator. Equivalent to {@link isSafeInput} with defaults.
227
366
  *
228
- * Equivalent to `isSafeInput(input)` with default config.
367
+ * @param input - String to test.
368
+ * @returns `true` when no detector fires.
229
369
  */
230
370
  declare function validateSafeText(input: string): boolean;
231
371
  /**
232
- * Refinement for human name fields. More permissive than
233
- * {@link validateSafeText} — allows international letters,
234
- * combining marks, hyphens, apostrophes, and spaces — but still
235
- * rejects HTML/SQL/NoSQL injection patterns and homoglyph forgeries.
372
+ * Validate a human name field.
373
+ *
374
+ * The policy is an allowlist of what a name is made of — letters in any script,
375
+ * combining marks, apostrophes, hyphens, periods, spaces — plus a length bound and a
376
+ * bidirectional-control check. Nothing that passes it can carry an injection payload,
377
+ * because `<`, `(`, `;`, `$`, `=`, and every digit are already excluded.
236
378
  *
237
- * Suitable for first/last/full-name inputs in registration forms.
379
+ * It deliberately does **not** run SQL, path-traversal, or confusable detectors. A
380
+ * name is not a query, a path, or a protected identifier, and subjecting one to those
381
+ * checks rejects real people: the previous implementation ran the homoglyph detector
382
+ * here, which failed any name containing а, е, о, р, с, or х — that is, most Russian,
383
+ * Ukrainian, Bulgarian, Serbian, and Greek names.
238
384
  *
239
- * @returns `true` when the name passes both the threat detectors and
240
- * the name-shape regex.
385
+ * Encode the value at whatever sink it eventually reaches. That is what makes it safe;
386
+ * this function only establishes that it is a name.
387
+ *
388
+ * @param input - Candidate name.
389
+ * @returns `true` when the value is a plausible name.
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * validatePersonName("O'Brien"); // true
394
+ * validatePersonName("José García"); // true
395
+ * validatePersonName("Ольга Иванова"); // true
396
+ * validatePersonName("John123"); // false
397
+ * validatePersonName("<script>x</script>"); // false
398
+ * ```
399
+ */
400
+ declare function validatePersonName(input: string): boolean;
401
+ /**
402
+ * Validate a human name field.
403
+ *
404
+ * @deprecated Renamed to {@link validatePersonName}. The old name implied a general
405
+ * "safe name" check and was implemented as one, running injection and homoglyph
406
+ * detectors against people's names. Behaviour now matches
407
+ * {@link validatePersonName}.
408
+ *
409
+ * @param input - Candidate name.
410
+ * @returns `true` when the value is a plausible name.
241
411
  */
242
412
  declare function validateSafeName(input: string): boolean;
243
413
  /**
244
- * Refinement for email fields. Combines:
414
+ * Validate an email address.
245
415
  *
246
- * 1. RFC-style format check (length-bounded to ≤ 254 chars to
247
- * prevent ReDoS).
248
- * 2. XSS / SQL / NoSQL / homoglyph detectors — emails are extremely
249
- * constrained and should never legitimately contain HTML or query
250
- * operators.
416
+ * Two checks: an RFC-shaped format match (length-bounded first, so the pattern never
417
+ * sees an unbounded string), and UTS #39 identifier analysis of the **domain**, where
418
+ * a mixed-script host is the IDN homograph attack — `аpple.com` with a Cyrillic `а`
419
+ * resolves somewhere else entirely.
251
420
  *
252
- * @returns `true` when both checks pass.
421
+ * The local part is not confusable-checked: it is not a routable identifier, and
422
+ * flagging it would reject legitimate internationalized mailboxes.
423
+ *
424
+ * @param input - Candidate address.
425
+ * @returns `true` when the format is valid and the domain is not a script mix.
253
426
  */
254
427
  declare function validateSafeEmail(input: string): boolean;
255
428
  /**
256
- * Map a {@link ThreatDetectionResult} into a user-facing error
257
- * message string suitable for an HTTP 400 response or form
258
- * validation error. Returns `""` when the result is safe (so
259
- * `error || undefined` works).
260
- *
261
- * Uses only the **first** finding for the message — exposing every
262
- * threat type to the user can leak information about the detection
263
- * rules. For full diagnostics, log `result.threats` server-side
264
- * rather than returning them.
429
+ * Render a user-facing error message for a detection result.
430
+ *
431
+ * Uses only the **first** finding: enumerating every category that fired leaks the
432
+ * shape of the rule set to whoever is probing it. Log `result.threats` server-side for
433
+ * diagnostics and return this to the client.
434
+ *
435
+ * @param result - A {@link ThreatDetectionResult}, a `ThreatScanResult`-shaped object,
436
+ * or any `{ isSafe, threats }` pair.
437
+ * @returns A message, or `""` when the result is safe — so `message || undefined`
438
+ * works at a call site.
265
439
  */
266
- declare function getThreatErrorMessage(result: ThreatDetectionResult): string;
440
+ declare function getThreatErrorMessage(result: {
441
+ readonly isSafe: boolean;
442
+ readonly threats: readonly ThreatSummary[];
443
+ }): string;
267
444
  //#endregion
268
- export { THREAT_DETECTED_MESSAGE, ThreatDetectionConfig, ThreatDetectionResult, ThreatFinding, ThreatType, containsCommandInjection, containsHomoglyphs, containsNoSQLInjection, containsPathTraversal, containsSQLInjection, containsXSSPatterns, detectThreatPatterns, getThreatErrorMessage, isSafeInput, normalizeUnicode, sanitizeForDisplay, validateSafeEmail, validateSafeName, validateSafeText };
445
+ export { THREAT_DETECTED_MESSAGE, ThreatDetectionConfig, ThreatDetectionResult, type ThreatFinding, ThreatSummary, type ThreatType, containsCommandInjection, containsHomoglyphs, containsNoSQLInjection, containsPathTraversal, containsPrototypePollution, containsSQLInjection, containsXSSPatterns, detectThreatPatterns, encodeJsonForScript, encodeLogValue, escapeCsvField, escapeHtmlAttribute, escapeHtmlText, getThreatErrorMessage, isSafeInput, normalizeUnicode, sanitizeForDisplay, toCsvRow, validatePersonName, validateSafeEmail, validateSafeName, validateSafeText };
269
446
  //# sourceMappingURL=validators.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validators.d.mts","names":[],"sources":["../src/validators.ts"],"mappings":";;AAyKA;;;;;;;;;AAWA;;;;;;;;;;AAaA;;;UAxBiB,qBAAA;EAwBK;EAtBrB,MAAA;EAkDkC;EAhDlC,OAAA,EAAS,aAAA;AAAA;;AAgFV;;;UAzEiB,aAAA;EAyEiD;EAvEjE,IAAA,EAAM,UAAA;EAmG+B;EAjGrC,WAAA;EAiGsC;EA/FtC,cAAA;AAAA;;;;;KAOW,UAAA;;;;;AA2LZ;;;;;AA+BA;;;;;;;;;;;iBA9LgB,mBAAA,CAAoB,KAAA,WAAgB,aAAA;AA4OpD;;;;;;;;;;AAgDA;;AAhDA,iBA5MgB,oBAAA,CAAqB,KAAA,WAAgB,aAAA;;;;;;;AA8QrD;;;iBAlPgB,sBAAA,CAAuB,KAAA,WAAgB,aAAA;;AAgRvD;;;;;AAyBA;;;;;AAQA;;iBAlRgB,wBAAA,CAAyB,KAAA,WAAgB,aAAA;;;AAiSzD;;;;;AA0BA;;iBAtRgB,qBAAA,CAAsB,KAAA,WAAgB,aAAA;;;AAwTtD;;;;;;;;;;;iBAzRgB,kBAAA,CAAmB,KAAA,WAAgB,aAAA;;;;;;;;;UA+BlC,qBAAA;;EAEhB,QAAA;;EAEA,iBAAA;;EAEA,mBAAA;;EAEA,qBAAA;;EAEA,kBAAA;;EAEA,eAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;iBAkCe,oBAAA,CACf,KAAA,UACA,MAAA,GAAQ,qBAAA,GACN,qBAAA;;;;;;;;;iBA6Ca,WAAA,CAAY,KAAA,UAAe,MAAA,GAAS,qBAAA;;;;;;;;;;;;;;;iBAkBpC,kBAAA,CAAmB,KAAA;;;;;;;;;;;;;;;;;;;iBA8BnB,gBAAA,CAAiB,KAAA;;;;;;cAyBpB,uBAAA;;;;;;;iBAQG,gBAAA,CAAiB,KAAA;;;;;;;;;;;;iBAejB,gBAAA,CAAiB,KAAA;;;;;;;;;;;;iBA0BjB,iBAAA,CAAkB,KAAA;;;;;;;;;;;;iBAkClB,qBAAA,CAAsB,MAAA,EAAQ,qBAAA"}
1
+ {"version":3,"file":"validators.d.mts","names":[],"sources":["../src/validators.ts"],"mappings":";;;;;;;;;UAgDiB;;EAEhB;;EAEA,SAAS;;;;;;;;;UAUO;;WAEP,MAAM;;WAEN;;WAEA;;;;;;;;;;;;;UAkBO;;EAEhB;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;iBAkDe,oBAAoB,gBAAgB;;;;;;;;;;;;iBAepC,2BAA2B,gBAAgB;;;;;;;;;;;iBAc3C,qBAAqB,gBAAgB;;;;;;;;iBAWrC,uBAAuB,gBAAgB;;;;;;;;;;;;iBAevC,yBAAyB,gBAAgB;;;;;;;;;;;;;iBAgBzC,sBAAsB,gBAAgB;;;;;;;;;;;;;;;iBA2CtC,mBAAmB,gBAAgB;;;;;;;;;;;;;;;iBA0CnC,qBACf,eACA,SAAQ,wBACN;;;;;;;;iBA8Ba,YAAY,eAAe,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCpC,eAAe;;;;;;;;;;;;;;;;;;;iBA4Cf,oBAAoB;;;;;;;;;;;;iBAwBpB,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwDnB,eACf,eACA;WAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqEL,eACf,gBACA;WAAoB;;;;;;;;;;;;;;;iBA+BL,SACf,4BACA;WAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyDL,oBAAoB;;;;;;;;;;;;;;;;iBAuCpB,iBAAiB;;;;;cAYpB;;;;;;;;iBASG,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoDjB,mBAAmB;;;;;;;;;;;;iBAyBnB,iBAAiB;;;;;;;;;;;;;;;iBAyBjB,kBAAkB;;;;;;;;;;;;;iBA2BlB,sBAAsB;WAC5B;WACA,kBAAkB"}