disarm 0.0.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -5
- package/binding.d.ts +283 -0
- package/binding.js +636 -0
- package/disarm.darwin-arm64.node +0 -0
- package/disarm.darwin-x64.node +0 -0
- package/disarm.linux-arm64-gnu.node +0 -0
- package/disarm.linux-x64-gnu.node +0 -0
- package/disarm.win32-x64-msvc.node +0 -0
- package/index.d.ts +261 -0
- package/index.js +413 -0
- package/package.json +49 -7
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 (
|
|
1
|
+
# disarm (Node.js)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
9
|
+
```sh
|
|
10
|
+
npm install disarm
|
|
11
|
+
```
|
|
7
12
|
|
|
8
|
-
|
|
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
|
+
}
|