disarm 0.0.0 → 0.11.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Richard Quinn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,8 +1,87 @@
1
- # disarm (placeholder)
1
+ # disarm (Node.js)
2
2
 
3
- Name reserved for a forthcoming library: secure text transliteration, Unicode
4
- normalization, and safe key/slug generation (formerly `translit`).
3
+ Unicode confusable/text-security building blocks for Node.js — TR39 *visual*
4
+ homoglyph folding, deobfuscation (bidi / zalgo / zero-width / invisible / emoji),
5
+ and standards-based *phonetic* transliteration. Powered by a **pure-Rust core**
6
+ ([`disarm`](https://crates.io/crates/disarm)) via [napi-rs](https://napi.rs); the
7
+ prebuilt native addons install with no Rust toolchain.
5
8
 
6
- This `0.0.0` release is an intentional placeholder and has no functional API.
9
+ ```sh
10
+ npm install disarm
11
+ ```
7
12
 
8
- Project: https://github.com/raeq/disarm
13
+ Ships TypeScript types (`.d.ts`) — no `@types/disarm` needed. Requires Node 14+.
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import {
19
+ normalizeConfusables,
20
+ transliterate,
21
+ slugify,
22
+ isSuspiciousHostname,
23
+ } from 'disarm'
24
+
25
+ // Visual (TR39) confusable folding — homoglyph defence
26
+ normalizeConfusables('раypal') // → 'paypal' (Cyrillic а/р folded to Latin)
27
+
28
+ // Phonetic romanization — readable ASCII, NOT a security control.
29
+ // A language profile sharpens the output: the uk profile gives Київ → Kyiv.
30
+ transliterate('Київ', { lang: 'uk' }) // → 'Kyiv'
31
+ slugify('Héllo Wörld') // → 'hello-world'
32
+
33
+ // Hostname / IDN spoof check (a false result is not a safety guarantee)
34
+ isSuspiciousHostname('pаypal.com') // → true (Cyrillic 'а')
35
+ ```
36
+
37
+ The two operations people most often confuse are *visual* confusable folding
38
+ (homoglyph defence) and *phonetic* transliteration (romanization) — see
39
+ [Which function do I want?](https://docs.disarm.dev/concepts/which-function/).
40
+
41
+ ## Idioms
42
+
43
+ - **Options objects with defaults** — `transliterate(text, { scheme, lang })`,
44
+ `slugify(text, { separator, maxLength, … })`, `normalize(text, { form })`.
45
+ - **String-union tokens** — `scheme: 'default' | 'strict_iso9' | 'gost7034'`,
46
+ `form: 'NFC' | 'NFD' | 'NFKC' | 'NFKD'`, `platform: 'universal' | 'windows' | 'posix'`,
47
+ fully typed in your editor.
48
+ - **A native error type** — bad input (an unknown scheme/target/form/platform)
49
+ throws `DisarmInvalidArgument`, a subclass of `DisarmError`:
50
+
51
+ ```ts
52
+ import { transliterate, DisarmError } from 'disarm'
53
+
54
+ try {
55
+ transliterate('x', { scheme: 'klingon' })
56
+ } catch (e) {
57
+ if (e instanceof DisarmError) console.warn(e.message)
58
+ }
59
+ ```
60
+
61
+ ## What's here
62
+
63
+ Transliteration (`transliterate`, `reverseTransliterate`, `findUntranslatable`),
64
+ confusables (`normalizeConfusables`, `isConfusable`), slugs (`slugify`),
65
+ normalization (`normalize`, `isNormalized`), text cleaning (`collapseWhitespace`,
66
+ `stripControlChars`, `stripZeroWidthChars`, `stripBidi`, `stripZalgo`, `isZalgo`),
67
+ deobfuscation/security presets (`stripObfuscation`, `canonicalize`,
68
+ `sanitizeFilename`), grapheme clusters (`graphemeLen`, `graphemeSplit`,
69
+ `graphemeTruncate`, `graphemeWidth`, `terminalWidth`), and script analysis
70
+ (`detectScripts`, `isMixedScript`, `isSuspiciousHostname`, `inspectAutoLang`).
71
+ Every export is fully typed.
72
+
73
+ ## Security posture
74
+
75
+ disarm normalizes **input**; it is a defense-in-depth layer, **not** an output
76
+ sanitizer. It performs no escaping and is not an XSS/SQL/HTML defense — encode at
77
+ the output sink. Read the
78
+ [Threat Model](https://github.com/raeq/disarm/blob/main/THREAT_MODEL.md) before
79
+ relying on it in a security context.
80
+
81
+ ## Links
82
+
83
+ - **Docs:** <https://docs.disarm.dev>
84
+ - **Core (Rust):** <https://crates.io/crates/disarm>
85
+ - **Source / issues:** <https://github.com/raeq/disarm>
86
+
87
+ MIT licensed.
package/binding.d.ts ADDED
@@ -0,0 +1,283 @@
1
+ /* auto-generated by NAPI-RS */
2
+ /* eslint-disable */
3
+ /**
4
+ * A reusable, opaque lexicon handle (HAI-SDLC 6.1).
5
+ *
6
+ * `hasAnomalies` / `inspectAnomalies` rebuild a `HashSet<String>` from the
7
+ * caller's word array on every call. A caller hitting these in a loop with a
8
+ * large lexicon pays that rebuild each time. `Lexicon` builds the internal set
9
+ * once in its constructor and is then reused across calls.
10
+ */
11
+ export declare class Lexicon {
12
+ /**
13
+ * Build a reusable lexicon from a word list, folding it into the internal
14
+ * set once.
15
+ */
16
+ constructor(words: Array<string>)
17
+ }
18
+
19
+ /**
20
+ * A reusable, opaque named-policy-profile pipeline handle (#404).
21
+ *
22
+ * `getPipeline` validates and compiles a profile's steps once; the resulting
23
+ * handle is then applied to any number of inputs via `process`. Like the
24
+ * `Lexicon` handle, the build cost is paid a single time and reused across
25
+ * calls, rather than re-resolved per call.
26
+ */
27
+ export declare class Pipeline {
28
+ /** Run the named pipeline over `text`, returning the cleaned string. */
29
+ process(text: string): string
30
+ }
31
+
32
+ /** Structured anomaly report. */
33
+ export interface AnomalyReport {
34
+ /** Whether any token tripped (the same value `hasAnomalies` returns). */
35
+ anomalous: boolean
36
+ /** The anomaly kinds that fired, in first-appearance order. */
37
+ kinds: Array<string>
38
+ /** Every finding, with span and detail. */
39
+ findings: Array<Finding>
40
+ /** The first finding's reason, if any. */
41
+ reason?: string
42
+ }
43
+
44
+ /** How `lang: "auto"` detection resolves `text`. */
45
+ export interface AutoLangInspection {
46
+ /** The primary non-Latin script detected, if any (e.g. `"Cyrillic"`). */
47
+ script?: string
48
+ /** The language auto-detection chose, if any (e.g. `"ru"`). */
49
+ chosenLang?: string
50
+ /** Why that choice was made. */
51
+ reason: string
52
+ /** The discriminator characters that drove the choice, if any. */
53
+ discriminatorsHit: Array<string>
54
+ }
55
+
56
+ export declare function canonicalize(text: string): string
57
+
58
+ /**
59
+ * Library catalog deduplication key (like `searchKey` plus confusable folding).
60
+ * `lang` selects the transliteration table; `strict_iso9` picks the ISO 9:1995
61
+ * Cyrillic scheme.
62
+ */
63
+ export declare function catalogKey(text: string, lang: string | undefined | null, strictIso9: boolean): string
64
+
65
+ export declare function collapseWhitespace(text: string): string
66
+
67
+ /** Replace emoji with their plain names; `strip_modifiers` drops skin-tone marks. */
68
+ export declare function demojize(text: string, stripModifiers: boolean): string
69
+
70
+ /**
71
+ * The Unicode scripts present, in first-appearance order (Common/Inherited
72
+ * excluded), as stable UCD identifiers.
73
+ */
74
+ export declare function detectScripts(text: string): Array<string>
75
+
76
+ /** One reason a token is anomalous (a single finding). */
77
+ export interface Finding {
78
+ /** Which branch fired: `"invisible"` | `"bidi"` | `"zalgo"` | `"mixed_script"` | `"bidi_mixed"` | `"leet"` | `"segmentation"`. */
79
+ kind: string
80
+ /** The offending whitespace token, as it appeared. */
81
+ token: string
82
+ /** Byte offset of the token start in the input. */
83
+ start: number
84
+ /** Byte offset of the token end in the input. */
85
+ end: number
86
+ /** Evidence: the codepoint, the scripts, or the decoded word. */
87
+ detail: string
88
+ /** A plain-language sentence describing the finding. */
89
+ reason: string
90
+ }
91
+
92
+ /** Characters with no romanization, as `{ char, offset }` (byte offset), in order. */
93
+ export declare function findUntranslatable(text: string, scheme: string, lang?: string | undefined | null): Array<Untranslatable>
94
+
95
+ export declare function foldCase(text: string): string
96
+
97
+ /**
98
+ * `getPipeline(profile)` — build a reusable `Pipeline` handle for a named
99
+ * policy profile. An unknown profile raises a `DisarmInvalidArgument`-tagged
100
+ * error naming the available profiles.
101
+ */
102
+ export declare function getPipeline(profile: string): Pipeline
103
+
104
+ export declare function graphemeLen(text: string): number
105
+
106
+ export declare function graphemeSplit(text: string): Array<string>
107
+
108
+ export declare function graphemeTruncate(text: string, maxGraphemes: number): string
109
+
110
+ export declare function graphemeWidth(cluster: string, ambiguousWide: boolean): number
111
+
112
+ /**
113
+ * `hasAnomalies(text, lexicon)` — `lexicon` is either an array of common words
114
+ * or a prebuilt `Lexicon` handle (no per-call rebuild).
115
+ */
116
+ export declare function hasAnomalies(text: string, lexicon: Array<string> | Lexicon): boolean
117
+
118
+ /**
119
+ * Whether `text` mixes strong left-to-right and strong right-to-left characters
120
+ * — the precondition for Bidi display-reordering ("BiDi Swap"). Fires on the
121
+ * real letters (no `U+202x` override); a `false` result is not a safety
122
+ * guarantee.
123
+ */
124
+ export declare function hasBidiConflict(text: string): boolean
125
+
126
+ /**
127
+ * `inspectAnomalies(text, lexicon)` — full analysis with per-token findings.
128
+ * `lexicon` is either an array of common words or a prebuilt `Lexicon` handle.
129
+ */
130
+ export declare function inspectAnomalies(text: string, lexicon: Array<string> | Lexicon): AnomalyReport
131
+
132
+ /** Explain how auto-language detection resolves `text`. */
133
+ export declare function inspectAutoLang(text: string): AutoLangInspection
134
+
135
+ /** Whether `text` contains a character confusable with `target`. */
136
+ export declare function isConfusable(text: string, target: string): boolean
137
+
138
+ /** Whether `text` mixes characters from more than one script. */
139
+ export declare function isMixedScript(text: string): boolean
140
+
141
+ /** Whether `text` is already in normalization `form`. */
142
+ export declare function isNormalized(text: string, form: string): boolean
143
+
144
+ /**
145
+ * Whether the hostname looks like a mixed-script / confusable / bidi-reorder
146
+ * IDN spoof. Flags a single mixed-script label, a Latin confusable, or a
147
+ * bidi-direction conflict (`hasBidiConflict`, the "BiDi Swap" precondition,
148
+ * #412). A `false` asserts nothing was *found*, not that the host is safe.
149
+ */
150
+ export declare function isSuspiciousHostname(host: string): boolean
151
+
152
+ export declare function isZalgo(text: string, threshold: number): boolean
153
+
154
+ /**
155
+ * Look up static facts about a language `code`. An unknown code raises a
156
+ * `DisarmInvalidArgument`-tagged error.
157
+ */
158
+ export declare function langInfo(code: string): LangMeta
159
+
160
+ /** Static facts about a language profile (the `lang` codes accepted across the API). */
161
+ export interface LangMeta {
162
+ /** The language's English name (e.g. `"German"`). */
163
+ name: string
164
+ /** The primary script it is written in (e.g. `"Latin"`). */
165
+ script: string
166
+ /** The region/locale it is associated with. */
167
+ region: string
168
+ /** Context-aware transliteration support: `"none"`, `"partial"`, or `"full"`. */
169
+ context: string
170
+ }
171
+
172
+ /** Every language code that has a context-aware transliteration profile. */
173
+ export declare function listContextLangs(): Array<string>
174
+
175
+ /** Every Unicode script name known to the transliteration tables. */
176
+ export declare function listScripts(): Array<string>
177
+
178
+ /** Apply a normalization form: `"NFC"` | `"NFD"` | `"NFKC"` | `"NFKD"`. */
179
+ export declare function normalize(text: string, form: string): string
180
+
181
+ /** Fold cross-script confusables toward `target` (`"latin"` | `"cyrillic"`). */
182
+ export declare function normalizeConfusables(text: string, target: string): string
183
+
184
+ /** Reverse-transliterate Latin → native script. `lang` is `"el"` | `"ru"` | `"uk"`. */
185
+ export declare function reverseTransliterate(text: string, lang: string): string
186
+
187
+ /**
188
+ * Turn arbitrary text into a safe filename. `platform` is `"universal"` |
189
+ * `"windows"` | `"posix"`.
190
+ */
191
+ export declare function sanitizeFilename(text: string, separator: string, maxLength: number, platform: string, lang: string | undefined | null, preserveExtension: boolean): string
192
+
193
+ /**
194
+ * Look up static facts about a script by `name`. An unknown name raises a
195
+ * `DisarmInvalidArgument`-tagged error.
196
+ */
197
+ export declare function scriptInfo(name: string): ScriptMeta
198
+
199
+ /** Static facts about a Unicode script known to the transliteration tables. */
200
+ export interface ScriptMeta {
201
+ /** The script's name (e.g. `"Coptic"`). */
202
+ name: string
203
+ /** The default language code for the script, if any (e.g. `"cop"`). */
204
+ defaultLang?: string
205
+ /** A short example string in the script. */
206
+ example: string
207
+ /** Whether transliteration of this script is context-aware. */
208
+ contextAware: boolean
209
+ }
210
+
211
+ /**
212
+ * Case/accent/script-insensitive search lookup key. `lang` selects the
213
+ * transliteration table (omit for none).
214
+ */
215
+ export declare function searchKey(text: string, lang?: string | undefined | null): string
216
+
217
+ /** Generate a URL-safe slug. */
218
+ export declare function slugify(text: string, opts: SlugOptions): string
219
+
220
+ /** The full slug option surface (the TS layer fills defaults before calling). */
221
+ export interface SlugOptions {
222
+ separator: string
223
+ lowercase: boolean
224
+ maxLength: number
225
+ wordBoundary: boolean
226
+ saveOrder: boolean
227
+ stopwords: Array<string>
228
+ allowUnicode: boolean
229
+ lang?: string
230
+ entities: boolean
231
+ decimal: boolean
232
+ hexadecimal: boolean
233
+ safeChars: string
234
+ }
235
+
236
+ /**
237
+ * Collation sort key (like `searchKey` but preserves base accented characters
238
+ * for correct ordering). `lang` selects the transliteration table.
239
+ */
240
+ export declare function sortKey(text: string, lang?: string | undefined | null): string
241
+
242
+ export declare function stripAccents(text: string): string
243
+
244
+ export declare function stripBidi(text: string): string
245
+
246
+ export declare function stripControlChars(text: string): string
247
+
248
+ /** Strip every Unicode noncharacter (#413). */
249
+ export declare function stripNoncharacters(text: string): string
250
+
251
+ export declare function stripObfuscation(text: string): string
252
+
253
+ /** Strip every Private Use Area code point (#413). */
254
+ export declare function stripPua(text: string): string
255
+
256
+ /** Strip the Unicode Tags block (U+E0000–U+E007F), preserving valid emoji flags (#413). */
257
+ export declare function stripTags(text: string): string
258
+
259
+ /** Strip every variation selector (VS1–VS256) (#413). */
260
+ export declare function stripVariationSelectors(text: string): string
261
+
262
+ export declare function stripZalgo(text: string, maxMarks: number): string
263
+
264
+ export declare function stripZeroWidthChars(text: string): string
265
+
266
+ export declare function terminalWidth(text: string, ambiguousWide: boolean): number
267
+
268
+ /** Unicode → ASCII with the default scheme (the borrow-on-no-op fast path). */
269
+ export declare function transliterate(text: string): string
270
+
271
+ /**
272
+ * Transliterate with a scheme (`"default"` | `"strict_iso9"` | `"gost7034"`)
273
+ * and/or a language profile (`lang`), via the core's builder.
274
+ */
275
+ export declare function transliterateOpts(text: string, scheme: string, lang?: string | undefined | null): string
276
+
277
+ /** A character with no transliteration, located in the input. */
278
+ export interface Untranslatable {
279
+ /** The untranslatable character. */
280
+ char: string
281
+ /** Its byte offset in the input string. */
282
+ offset: number
283
+ }