disarm 0.13.0 → 0.14.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/binding.d.ts CHANGED
@@ -34,7 +34,7 @@ export declare class Pipeline {
34
34
  * [`HostnameAnalysis`] (verdict + granular signals). `isSuspiciousHostname` is
35
35
  * the boolean shorthand for `.suspicious`.
36
36
  */
37
- export declare function analyzeHostname(host: string): HostnameAnalysis
37
+ export declare function analyzeHostname(host: string, contractions: boolean): HostnameAnalysis
38
38
 
39
39
  /** Structured anomaly report. */
40
40
  export interface AnomalyReport {
@@ -71,6 +71,14 @@ export declare function catalogKey(text: string, lang: string | undefined | null
71
71
 
72
72
  export declare function collapseWhitespace(text: string): string
73
73
 
74
+ /**
75
+ * The Unicode `confusables.txt` release the bundled confusable tables were folded
76
+ * from (#560). Deliberately not called a "Unicode version": disarm's other tables
77
+ * track different releases (see docs/provenance.md), so this answers one question —
78
+ * how current is the confusables fold?
79
+ */
80
+ export declare function confusablesVersion(): string
81
+
74
82
  /** Replace emoji with their plain names; `strip_modifiers` drops skin-tone marks. */
75
83
  export declare function demojize(text: string, stripModifiers: boolean): string
76
84
 
@@ -96,6 +104,18 @@ export interface Finding {
96
104
  reason: string
97
105
  }
98
106
 
107
+ /**
108
+ * Which of `values` reduce to the same identity key under `key` (#620).
109
+ *
110
+ * `key` is one of `"fold_case"`, `"search_key"`, `"catalog_key"`,
111
+ * `"canonicalize"`, `"canonicalize_strict"`, `"normalize_confusables"`. There is
112
+ * no default: the choice is the policy.
113
+ */
114
+ export declare function findKeyCollisions(values: Array<string>, key: string, lang?: string | undefined | null): Array<KeyCollision>
115
+
116
+ /** Scan `text` for confusable sources the bundled `target` table does not fold (#563). */
117
+ export declare function findUnmappedConfusables(text: string, target: string): Array<UnmappedConfusable>
118
+
99
119
  /** Characters with no romanization, as `{ char, offset }` (byte offset), in order. */
100
120
  export declare function findUntranslatable(text: string, scheme: string, lang?: string | undefined | null): Array<Untranslatable>
101
121
 
@@ -151,6 +171,21 @@ export interface HostnameAnalysis {
151
171
  * "BiDi Swap" precondition). Folded into `suspicious`.
152
172
  */
153
173
  bidiConflict: boolean
174
+ /**
175
+ * Whether the decoded hostname contains a UAX #9 bidi control character —
176
+ * override, embedding, isolate or directional mark (#603). Disjoint from
177
+ * `bidiConflict`, which reads strong-direction letters only. Folded into
178
+ * `suspicious`; the characters are stripped from `canonical`.
179
+ */
180
+ bidiControl: boolean
181
+ /**
182
+ * Whether the decoded hostname contains a zero-width or invisible-format
183
+ * character — `U+200B`-`U+200D`, `U+2060`-`U+2064`, `U+FEFF`, `U+180E` (#605).
184
+ * Disjoint from `bidiControl`: these carry no direction at all. Folded into
185
+ * `suspicious`; the characters are stripped before any other field is computed,
186
+ * so they never reach `scripts`, `mixedScript` or `canonical`.
187
+ */
188
+ hasInvisible: boolean
154
189
  /**
155
190
  * Whether the labels span more than one script. Broader/noisier than
156
191
  * `bidiConflict`; NOT folded into `suspicious`.
@@ -179,6 +214,12 @@ export declare function inspectAnomalies(text: string, lexicon: Array<string> |
179
214
  /** Explain how auto-language detection resolves `text`. */
180
215
  export declare function inspectAutoLang(text: string): AutoLangInspection
181
216
 
217
+ /**
218
+ * Whether case folding and simple lowercasing agree, so the value is a stable
219
+ * identity key (#619).
220
+ */
221
+ export declare function isCaseFoldStable(text: string): boolean
222
+
182
223
  /** Whether `text` contains a character confusable with `target`. */
183
224
  export declare function isConfusable(text: string, target: string): boolean
184
225
 
@@ -198,6 +239,20 @@ export declare function isSuspiciousHostname(host: string): boolean
198
239
 
199
240
  export declare function isZalgo(text: string, threshold: number): boolean
200
241
 
242
+ /** One group of distinct inputs that reduce to the same key (#620). */
243
+ export interface KeyCollision {
244
+ /** The reduced form every member of the group shares. */
245
+ key: string
246
+ /** The distinct inputs that reduce to it, in order of first appearance. */
247
+ values: Array<string>
248
+ /**
249
+ * Every position in the input array that belongs to this group, ascending.
250
+ * Not parallel to `values`: a value repeated verbatim appears once there and
251
+ * once per occurrence here.
252
+ */
253
+ indices: Array<number>
254
+ }
255
+
201
256
  /**
202
257
  * Look up static facts about a language `code`. An unknown code raises a
203
258
  * `DisarmInvalidArgument`-tagged error.
@@ -222,11 +277,21 @@ export declare function listContextLangs(): Array<string>
222
277
  /** Every Unicode script name known to the transliteration tables. */
223
278
  export declare function listScripts(): Array<string>
224
279
 
280
+ /**
281
+ * ML/NLP normalization: NFKC → emoji→text → transliterate → strip accents →
282
+ * [case fold] → strip control → strip zero-width → collapse whitespace.
283
+ *
284
+ * `fold_case` defaults to `true`. Pass `false` in front of a CASED model: the fold is
285
+ * destructive, cannot be undone downstream, and an uncased evaluation harness cannot
286
+ * measure what it cost. It restores case, not diacritics — `strip_accents` still runs.
287
+ */
288
+ export declare function mlNormalize(text: string, lang: string | undefined | null, emojiStyle: string, foldCase: boolean): string
289
+
225
290
  /** Apply a normalization form: `"NFC"` | `"NFD"` | `"NFKC"` | `"NFKD"`. */
226
291
  export declare function normalize(text: string, form: string): string
227
292
 
228
293
  /** Fold cross-script confusables toward `target` (`"latin"` | `"cyrillic"`). */
229
- export declare function normalizeConfusables(text: string, target: string): string
294
+ export declare function normalizeConfusables(text: string, target: string, digitPolicy: string): string
230
295
 
231
296
  /** Reverse-transliterate Latin → native script. `lang` is `"el"` | `"ru"` | `"uk"`. */
232
297
  export declare function reverseTransliterate(text: string, lang: string): string
@@ -321,6 +386,20 @@ export declare function transliterate(text: string): string
321
386
  */
322
387
  export declare function transliterateOpts(text: string, scheme: string, lang?: string | undefined | null): string
323
388
 
389
+ /**
390
+ * An upstream confusable source the bundled table does not fold, located in the
391
+ * input (#563). Mirrors `Untranslatable`, its transliteration analogue.
392
+ */
393
+ export interface UnmappedConfusable {
394
+ /** The unmapped character. */
395
+ char: string
396
+ /** Its byte offset in the input string. */
397
+ offset: number
398
+ }
399
+
400
+ /** Every upstream confusable source the bundled `target` table does not fold (#563). */
401
+ export declare function unmappedConfusables(target: string): Array<string>
402
+
324
403
  /** A character with no transliteration, located in the input. */
325
404
  export interface Untranslatable {
326
405
  /** The untranslatable character. */
package/binding.js CHANGED
@@ -77,8 +77,8 @@ function requireNative() {
77
77
  try {
78
78
  const binding = require('disarm-android-arm64')
79
79
  const bindingPackageVersion = require('disarm-android-arm64/package.json').version
80
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
81
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
80
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
81
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
82
82
  }
83
83
  return binding
84
84
  } catch (e) {
@@ -93,8 +93,8 @@ function requireNative() {
93
93
  try {
94
94
  const binding = require('disarm-android-arm-eabi')
95
95
  const bindingPackageVersion = require('disarm-android-arm-eabi/package.json').version
96
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
97
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
96
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
97
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
98
98
  }
99
99
  return binding
100
100
  } catch (e) {
@@ -114,8 +114,8 @@ function requireNative() {
114
114
  try {
115
115
  const binding = require('disarm-win32-x64-gnu')
116
116
  const bindingPackageVersion = require('disarm-win32-x64-gnu/package.json').version
117
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
118
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
117
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
118
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
119
119
  }
120
120
  return binding
121
121
  } catch (e) {
@@ -130,8 +130,8 @@ function requireNative() {
130
130
  try {
131
131
  const binding = require('disarm-win32-x64-msvc')
132
132
  const bindingPackageVersion = require('disarm-win32-x64-msvc/package.json').version
133
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
134
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
133
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
134
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
135
135
  }
136
136
  return binding
137
137
  } catch (e) {
@@ -147,8 +147,8 @@ function requireNative() {
147
147
  try {
148
148
  const binding = require('disarm-win32-ia32-msvc')
149
149
  const bindingPackageVersion = require('disarm-win32-ia32-msvc/package.json').version
150
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
151
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
150
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
151
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
152
152
  }
153
153
  return binding
154
154
  } catch (e) {
@@ -163,8 +163,8 @@ function requireNative() {
163
163
  try {
164
164
  const binding = require('disarm-win32-arm64-msvc')
165
165
  const bindingPackageVersion = require('disarm-win32-arm64-msvc/package.json').version
166
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
167
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
166
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
167
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
168
168
  }
169
169
  return binding
170
170
  } catch (e) {
@@ -182,8 +182,8 @@ function requireNative() {
182
182
  try {
183
183
  const binding = require('disarm-darwin-universal')
184
184
  const bindingPackageVersion = require('disarm-darwin-universal/package.json').version
185
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
186
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
185
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
186
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
187
187
  }
188
188
  return binding
189
189
  } catch (e) {
@@ -198,8 +198,8 @@ function requireNative() {
198
198
  try {
199
199
  const binding = require('disarm-darwin-x64')
200
200
  const bindingPackageVersion = require('disarm-darwin-x64/package.json').version
201
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
202
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
201
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
202
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
203
203
  }
204
204
  return binding
205
205
  } catch (e) {
@@ -214,8 +214,8 @@ function requireNative() {
214
214
  try {
215
215
  const binding = require('disarm-darwin-arm64')
216
216
  const bindingPackageVersion = require('disarm-darwin-arm64/package.json').version
217
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
218
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
217
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
218
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
219
219
  }
220
220
  return binding
221
221
  } catch (e) {
@@ -234,8 +234,8 @@ function requireNative() {
234
234
  try {
235
235
  const binding = require('disarm-freebsd-x64')
236
236
  const bindingPackageVersion = require('disarm-freebsd-x64/package.json').version
237
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
238
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
237
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
238
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
239
239
  }
240
240
  return binding
241
241
  } catch (e) {
@@ -250,8 +250,8 @@ function requireNative() {
250
250
  try {
251
251
  const binding = require('disarm-freebsd-arm64')
252
252
  const bindingPackageVersion = require('disarm-freebsd-arm64/package.json').version
253
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
254
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
253
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
254
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
255
255
  }
256
256
  return binding
257
257
  } catch (e) {
@@ -271,8 +271,8 @@ function requireNative() {
271
271
  try {
272
272
  const binding = require('disarm-linux-x64-musl')
273
273
  const bindingPackageVersion = require('disarm-linux-x64-musl/package.json').version
274
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
275
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
274
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
275
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
276
276
  }
277
277
  return binding
278
278
  } catch (e) {
@@ -287,8 +287,8 @@ function requireNative() {
287
287
  try {
288
288
  const binding = require('disarm-linux-x64-gnu')
289
289
  const bindingPackageVersion = require('disarm-linux-x64-gnu/package.json').version
290
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
291
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
290
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
291
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
292
292
  }
293
293
  return binding
294
294
  } catch (e) {
@@ -305,8 +305,8 @@ function requireNative() {
305
305
  try {
306
306
  const binding = require('disarm-linux-arm64-musl')
307
307
  const bindingPackageVersion = require('disarm-linux-arm64-musl/package.json').version
308
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
309
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
308
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
309
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
310
310
  }
311
311
  return binding
312
312
  } catch (e) {
@@ -321,8 +321,8 @@ function requireNative() {
321
321
  try {
322
322
  const binding = require('disarm-linux-arm64-gnu')
323
323
  const bindingPackageVersion = require('disarm-linux-arm64-gnu/package.json').version
324
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
325
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
324
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
325
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
326
326
  }
327
327
  return binding
328
328
  } catch (e) {
@@ -339,8 +339,8 @@ function requireNative() {
339
339
  try {
340
340
  const binding = require('disarm-linux-arm-musleabihf')
341
341
  const bindingPackageVersion = require('disarm-linux-arm-musleabihf/package.json').version
342
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
343
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
342
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
343
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
344
344
  }
345
345
  return binding
346
346
  } catch (e) {
@@ -355,8 +355,8 @@ function requireNative() {
355
355
  try {
356
356
  const binding = require('disarm-linux-arm-gnueabihf')
357
357
  const bindingPackageVersion = require('disarm-linux-arm-gnueabihf/package.json').version
358
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
359
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
358
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
359
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
360
360
  }
361
361
  return binding
362
362
  } catch (e) {
@@ -373,8 +373,8 @@ function requireNative() {
373
373
  try {
374
374
  const binding = require('disarm-linux-loong64-musl')
375
375
  const bindingPackageVersion = require('disarm-linux-loong64-musl/package.json').version
376
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
377
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
376
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
377
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
378
378
  }
379
379
  return binding
380
380
  } catch (e) {
@@ -389,8 +389,8 @@ function requireNative() {
389
389
  try {
390
390
  const binding = require('disarm-linux-loong64-gnu')
391
391
  const bindingPackageVersion = require('disarm-linux-loong64-gnu/package.json').version
392
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
393
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
392
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
393
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
394
394
  }
395
395
  return binding
396
396
  } catch (e) {
@@ -407,8 +407,8 @@ function requireNative() {
407
407
  try {
408
408
  const binding = require('disarm-linux-riscv64-musl')
409
409
  const bindingPackageVersion = require('disarm-linux-riscv64-musl/package.json').version
410
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
411
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
410
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
411
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
412
412
  }
413
413
  return binding
414
414
  } catch (e) {
@@ -423,8 +423,8 @@ function requireNative() {
423
423
  try {
424
424
  const binding = require('disarm-linux-riscv64-gnu')
425
425
  const bindingPackageVersion = require('disarm-linux-riscv64-gnu/package.json').version
426
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
427
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
426
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
427
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
428
428
  }
429
429
  return binding
430
430
  } catch (e) {
@@ -440,8 +440,8 @@ function requireNative() {
440
440
  try {
441
441
  const binding = require('disarm-linux-ppc64-gnu')
442
442
  const bindingPackageVersion = require('disarm-linux-ppc64-gnu/package.json').version
443
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
444
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
443
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
444
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
445
445
  }
446
446
  return binding
447
447
  } catch (e) {
@@ -456,8 +456,8 @@ function requireNative() {
456
456
  try {
457
457
  const binding = require('disarm-linux-s390x-gnu')
458
458
  const bindingPackageVersion = require('disarm-linux-s390x-gnu/package.json').version
459
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
460
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
459
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
460
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
461
461
  }
462
462
  return binding
463
463
  } catch (e) {
@@ -476,8 +476,8 @@ function requireNative() {
476
476
  try {
477
477
  const binding = require('disarm-openharmony-arm64')
478
478
  const bindingPackageVersion = require('disarm-openharmony-arm64/package.json').version
479
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
480
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
479
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
480
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
481
481
  }
482
482
  return binding
483
483
  } catch (e) {
@@ -492,8 +492,8 @@ function requireNative() {
492
492
  try {
493
493
  const binding = require('disarm-openharmony-x64')
494
494
  const bindingPackageVersion = require('disarm-openharmony-x64/package.json').version
495
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
496
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
495
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
496
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
497
497
  }
498
498
  return binding
499
499
  } catch (e) {
@@ -508,8 +508,8 @@ function requireNative() {
508
508
  try {
509
509
  const binding = require('disarm-openharmony-arm')
510
510
  const bindingPackageVersion = require('disarm-openharmony-arm/package.json').version
511
- if (bindingPackageVersion !== '0.13.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
512
- throw new Error(`Native binding package version mismatch, expected 0.13.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
511
+ if (bindingPackageVersion !== '0.14.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
512
+ throw new Error(`Native binding package version mismatch, expected 0.14.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
513
513
  }
514
514
  return binding
515
515
  } catch (e) {
@@ -593,8 +593,11 @@ module.exports.analyzeHostname = nativeBinding.analyzeHostname
593
593
  module.exports.canonicalize = nativeBinding.canonicalize
594
594
  module.exports.catalogKey = nativeBinding.catalogKey
595
595
  module.exports.collapseWhitespace = nativeBinding.collapseWhitespace
596
+ module.exports.confusablesVersion = nativeBinding.confusablesVersion
596
597
  module.exports.demojize = nativeBinding.demojize
597
598
  module.exports.detectScripts = nativeBinding.detectScripts
599
+ module.exports.findKeyCollisions = nativeBinding.findKeyCollisions
600
+ module.exports.findUnmappedConfusables = nativeBinding.findUnmappedConfusables
598
601
  module.exports.findUntranslatable = nativeBinding.findUntranslatable
599
602
  module.exports.foldCase = nativeBinding.foldCase
600
603
  module.exports.getPipeline = nativeBinding.getPipeline
@@ -606,6 +609,7 @@ module.exports.hasAnomalies = nativeBinding.hasAnomalies
606
609
  module.exports.hasBidiConflict = nativeBinding.hasBidiConflict
607
610
  module.exports.inspectAnomalies = nativeBinding.inspectAnomalies
608
611
  module.exports.inspectAutoLang = nativeBinding.inspectAutoLang
612
+ module.exports.isCaseFoldStable = nativeBinding.isCaseFoldStable
609
613
  module.exports.isConfusable = nativeBinding.isConfusable
610
614
  module.exports.isMixedScript = nativeBinding.isMixedScript
611
615
  module.exports.isNormalized = nativeBinding.isNormalized
@@ -614,6 +618,7 @@ module.exports.isZalgo = nativeBinding.isZalgo
614
618
  module.exports.langInfo = nativeBinding.langInfo
615
619
  module.exports.listContextLangs = nativeBinding.listContextLangs
616
620
  module.exports.listScripts = nativeBinding.listScripts
621
+ module.exports.mlNormalize = nativeBinding.mlNormalize
617
622
  module.exports.normalize = nativeBinding.normalize
618
623
  module.exports.normalizeConfusables = nativeBinding.normalizeConfusables
619
624
  module.exports.reverseTransliterate = nativeBinding.reverseTransliterate
@@ -635,3 +640,4 @@ module.exports.stripZeroWidthChars = nativeBinding.stripZeroWidthChars
635
640
  module.exports.terminalWidth = nativeBinding.terminalWidth
636
641
  module.exports.transliterate = nativeBinding.transliterate
637
642
  module.exports.transliterateOpts = nativeBinding.transliterateOpts
643
+ module.exports.unmappedConfusables = nativeBinding.unmappedConfusables
Binary file
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Lexicon, Pipeline } from './binding';
2
- import type { Untranslatable, AutoLangInspection, LangMeta, ScriptMeta, Finding as NativeFinding, AnomalyReport as NativeAnomalyReport, HostnameAnalysis as NativeHostnameAnalysis } from './binding';
3
- export type { Untranslatable, AutoLangInspection, LangMeta, ScriptMeta };
2
+ import type { Untranslatable, UnmappedConfusable, AutoLangInspection, KeyCollision, LangMeta, ScriptMeta, Finding as NativeFinding, AnomalyReport as NativeAnomalyReport, HostnameAnalysis as NativeHostnameAnalysis } from './binding';
3
+ export type { Untranslatable, UnmappedConfusable, AutoLangInspection, KeyCollision, LangMeta, ScriptMeta, };
4
4
  /** Findings from {@link analyzeHostname}. `suspicious` is a maximally
5
5
  * conservative screen, not a precise verdict (#549). */
6
6
  export type HostnameAnalysis = NativeHostnameAnalysis;
@@ -25,7 +25,7 @@ export { Lexicon };
25
25
  */
26
26
  export { Pipeline };
27
27
  /** The anomaly branch that fired for a finding. */
28
- export type AnomalyKind = 'invisible' | 'bidi' | 'zalgo' | 'mixed_script' | 'leet' | 'segmentation';
28
+ export type AnomalyKind = 'invisible' | 'bidi' | 'bidi_mixed' | 'zalgo' | 'mixed_script' | 'leet' | 'segmentation' | 'control' | 'compat_fold';
29
29
  /**
30
30
  * One reason a token is anomalous. Re-typed over the generated {@link NativeFinding}
31
31
  * so `kind` is the {@link AnomalyKind} string-union rather than a bare `string`.
@@ -49,6 +49,21 @@ export declare class DisarmInvalidArgument extends DisarmError {
49
49
  export type Scheme = 'default' | 'strict_iso9' | 'gost7034';
50
50
  /** Confusable-folding target script. */
51
51
  export type TargetScript = 'latin' | 'cyrillic';
52
+ /**
53
+ * How the fold treats non-Latin digits.
54
+ *
55
+ * `'numeric'` (default) sends them to the ASCII digit — `०` becomes `0` — which is right
56
+ * for prose. `'tr39'` uses upstream's targets, which send most of them to a Latin letter
57
+ * (`०` → `o`), and that is what an identifier *skeleton* wants. The two differ on 45 rows.
58
+ *
59
+ * Three of those rows do not land on a letter: `٠` and `۰` fold to `.`, and `𑣣` folds to
60
+ * the two characters `rn`. A skeleton feeding a label- or path-shaped key has to allow
61
+ * for that extra `.`.
62
+ *
63
+ * Scoped to `target: 'latin'`: the override rows are generated from the Latin table and
64
+ * carry TR39's Latin-script targets, so with `target: 'cyrillic'` the option is a no-op.
65
+ */
66
+ export type DigitPolicy = 'numeric' | 'tr39';
52
67
  /** Unicode normalization form. */
53
68
  export type NormalizationForm = 'NFC' | 'NFD' | 'NFKC' | 'NFKD';
54
69
  /** Filename-safety platform ruleset. */
@@ -72,11 +87,31 @@ export declare function findUntranslatable(text: string, options?: Transliterate
72
87
  /** Fold cross-script confusables toward `target` (default `'latin'`). */
73
88
  export declare function normalizeConfusables(text: string, options?: {
74
89
  target?: TargetScript;
90
+ digitPolicy?: DigitPolicy;
75
91
  }): string;
76
92
  /** Whether `text` contains a character confusable with `target` (default `'latin'`). */
77
93
  export declare function isConfusable(text: string, options?: {
78
94
  target?: TargetScript;
79
95
  }): boolean;
96
+ /**
97
+ * Every upstream confusable source the bundled `target` table does not fold.
98
+ *
99
+ * Read as exposure, not as a score — this is where an adaptive attacker goes when the
100
+ * mapped sources stop working. Note it includes five ASCII characters (`%`, `0`, `1`,
101
+ * `I`, `m`): TR39 is a skeleton transform, and disarm deliberately does not apply those
102
+ * rows because folding a legitimate `m` to `rn` corrupts prose.
103
+ */
104
+ export declare function unmappedConfusables(options?: {
105
+ target?: TargetScript;
106
+ }): Set<string>;
107
+ /**
108
+ * Confusable sources in `text` the bundled `target` table does not fold, as
109
+ * `{ char, offset }` (byte offset), in order — the confusables analogue of
110
+ * {@link findUntranslatable}.
111
+ */
112
+ export declare function findUnmappedConfusables(text: string, options?: {
113
+ target?: TargetScript;
114
+ }): UnmappedConfusable[];
80
115
  export interface SlugifyOptions {
81
116
  separator?: string;
82
117
  lowercase?: boolean;
@@ -97,6 +132,39 @@ export declare function slugify(text: string, options?: SlugifyOptions): string;
97
132
  export declare function stripAccents(text: string): string;
98
133
  /** Full Unicode case fold — more aggressive than `String.toLowerCase()`. */
99
134
  export declare function foldCase(text: string): string;
135
+ /**
136
+ * Whether `text` is a stable identity key under case folding — that is, whether
137
+ * {@link foldCase} and `String.toLowerCase()` agree on it (#619).
138
+ *
139
+ * `false` means some *other* string folds to the same value, so a table keyed on
140
+ * this one can collide: `'groß.txt'` and `'gross.txt'` are the pair node-tar
141
+ * collided on (CVE-2026-23950). It is a fact about the string and not an
142
+ * accusation — `groß` is an ordinary German word — so it is deliberately not
143
+ * folded into {@link hasAnomalies}.
144
+ */
145
+ export declare function isCaseFoldStable(text: string): boolean;
146
+ /** Which reducer {@link findKeyCollisions} builds its keys with. No default: the
147
+ * choice is the policy. */
148
+ export type CollisionKey = 'fold_case' | 'search_key' | 'catalog_key' | 'canonicalize' | 'canonicalize_strict' | 'normalize_confusables';
149
+ /**
150
+ * Which of `values` are the same name under `key` (#620).
151
+ *
152
+ * Every other disarm detector is a single-string predicate, and a collision is not
153
+ * a property of a single string — `groß.txt` is an ordinary German filename, and
154
+ * `аdmin` is only a problem next to `admin`. This is the set-shaped question that
155
+ * node-tar's `PathReservations` guard failed to ask before extracting two paths in
156
+ * parallel (CVE-2026-23950).
157
+ *
158
+ * A stronger `key` finds more collisions, including ones nobody attacked:
159
+ * `search_key` collides `Muller` with `Müller`. That is the cost of the key you
160
+ * chose, not a false positive. A group is reported only when two or more *distinct*
161
+ * inputs share a key; the same name twice is the same name twice.
162
+ *
163
+ * `options.lang` reaches `search_key` and `catalog_key` and is ignored by the rest.
164
+ */
165
+ export declare function findKeyCollisions(values: string[], key: CollisionKey, options?: {
166
+ lang?: string;
167
+ }): KeyCollision[];
100
168
  /** Replace emoji with their plain names. `stripModifiers` drops skin-tone/variation marks. */
101
169
  export declare function demojize(text: string, options?: {
102
170
  stripModifiers?: boolean;
@@ -201,6 +269,29 @@ export declare function catalogKey(text: string, options?: {
201
269
  lang?: string;
202
270
  strictIso9?: boolean;
203
271
  }): string;
272
+ /** Options for {@link mlNormalize}. */
273
+ export interface MlNormalizeOptions {
274
+ /** Language code selecting the transliteration table; omit for none. */
275
+ lang?: string;
276
+ /** `'cldr'` (default) expands emoji to CLDR short names; `'none'` leaves them. */
277
+ emojiStyle?: 'cldr' | 'none';
278
+ /**
279
+ * Apply Unicode case folding (default `true`).
280
+ *
281
+ * Pass `false` in front of a **cased** model: folding is destructive, cannot be undone
282
+ * downstream, and an uncased evaluation harness cannot measure what it cost. It
283
+ * restores case, not diacritics — accents are still stripped.
284
+ */
285
+ foldCase?: boolean;
286
+ }
287
+ /**
288
+ * ML/NLP normalization: NFKC → emoji→text → transliterate → strip accents →
289
+ * [case fold] → strip control → strip zero-width → collapse whitespace.
290
+ *
291
+ * Note this folds no confusables — it is not a homoglyph defence at any setting. Put
292
+ * {@link normalizeConfusables} in front of it when a model needs both.
293
+ */
294
+ export declare function mlNormalize(text: string, options?: MlNormalizeOptions): string;
204
295
  /** Number of grapheme clusters (user-perceived characters). */
205
296
  export declare function graphemeLen(text: string): number;
206
297
  /** Split `text` into grapheme-cluster strings. */
@@ -222,7 +313,9 @@ export declare function isSuspiciousHostname(host: string): boolean;
222
313
  * {@link HostnameAnalysis} (verdict + granular signals). `isSuspiciousHostname`
223
314
  * is the boolean shorthand for `.suspicious`.
224
315
  */
225
- export declare function analyzeHostname(host: string): HostnameAnalysis;
316
+ export declare function analyzeHostname(host: string, options?: {
317
+ contractions?: boolean;
318
+ }): HostnameAnalysis;
226
319
  /** The Unicode scripts present, in first-appearance order (Common/Inherited excluded). */
227
320
  export declare function detectScripts(text: string): string[];
228
321
  /** Whether `text` mixes characters from more than one script. */
@@ -248,6 +341,15 @@ export declare function langInfo(code: string): LangMeta;
248
341
  * unknown name throws {@link DisarmInvalidArgument}.
249
342
  */
250
343
  export declare function scriptInfo(name: string): ScriptMeta;
344
+ /**
345
+ * The Unicode `confusables.txt` release the bundled confusable tables were folded
346
+ * from, e.g. `"17.0.0"`.
347
+ *
348
+ * Not a Unicode version for the library as a whole — disarm's case-folding and width
349
+ * tables track different releases (see docs/provenance.md). Use this to answer "is my
350
+ * confusables fold stale?" without inferring it from behaviour.
351
+ */
352
+ export declare function confusablesVersion(): string;
251
353
  /** Every Unicode script name known to the transliteration tables. */
252
354
  export declare function listScripts(): string[];
253
355
  /** Every language code that has a context-aware transliteration profile. */
package/index.js CHANGED
@@ -39,9 +39,13 @@ exports.reverseTransliterate = reverseTransliterate;
39
39
  exports.findUntranslatable = findUntranslatable;
40
40
  exports.normalizeConfusables = normalizeConfusables;
41
41
  exports.isConfusable = isConfusable;
42
+ exports.unmappedConfusables = unmappedConfusables;
43
+ exports.findUnmappedConfusables = findUnmappedConfusables;
42
44
  exports.slugify = slugify;
43
45
  exports.stripAccents = stripAccents;
44
46
  exports.foldCase = foldCase;
47
+ exports.isCaseFoldStable = isCaseFoldStable;
48
+ exports.findKeyCollisions = findKeyCollisions;
45
49
  exports.demojize = demojize;
46
50
  exports.normalize = normalize;
47
51
  exports.isNormalized = isNormalized;
@@ -63,6 +67,7 @@ exports.sanitizeFilename = sanitizeFilename;
63
67
  exports.searchKey = searchKey;
64
68
  exports.sortKey = sortKey;
65
69
  exports.catalogKey = catalogKey;
70
+ exports.mlNormalize = mlNormalize;
66
71
  exports.graphemeLen = graphemeLen;
67
72
  exports.graphemeSplit = graphemeSplit;
68
73
  exports.graphemeTruncate = graphemeTruncate;
@@ -76,6 +81,7 @@ exports.hasBidiConflict = hasBidiConflict;
76
81
  exports.inspectAutoLang = inspectAutoLang;
77
82
  exports.langInfo = langInfo;
78
83
  exports.scriptInfo = scriptInfo;
84
+ exports.confusablesVersion = confusablesVersion;
79
85
  exports.listScripts = listScripts;
80
86
  exports.listContextLangs = listContextLangs;
81
87
  exports.hasAnomalies = hasAnomalies;
@@ -154,12 +160,31 @@ function findUntranslatable(text, options = {}) {
154
160
  // ── Confusables (TR39) ──────────────────────────────────────────────────────
155
161
  /** Fold cross-script confusables toward `target` (default `'latin'`). */
156
162
  function normalizeConfusables(text, options = {}) {
157
- return call(() => native.normalizeConfusables(text, options.target ?? 'latin'));
163
+ return call(() => native.normalizeConfusables(text, options.target ?? 'latin', options.digitPolicy ?? 'numeric'));
158
164
  }
159
165
  /** Whether `text` contains a character confusable with `target` (default `'latin'`). */
160
166
  function isConfusable(text, options = {}) {
161
167
  return call(() => native.isConfusable(text, options.target ?? 'latin'));
162
168
  }
169
+ /**
170
+ * Every upstream confusable source the bundled `target` table does not fold.
171
+ *
172
+ * Read as exposure, not as a score — this is where an adaptive attacker goes when the
173
+ * mapped sources stop working. Note it includes five ASCII characters (`%`, `0`, `1`,
174
+ * `I`, `m`): TR39 is a skeleton transform, and disarm deliberately does not apply those
175
+ * rows because folding a legitimate `m` to `rn` corrupts prose.
176
+ */
177
+ function unmappedConfusables(options = {}) {
178
+ return new Set(call(() => native.unmappedConfusables(options.target ?? 'latin')));
179
+ }
180
+ /**
181
+ * Confusable sources in `text` the bundled `target` table does not fold, as
182
+ * `{ char, offset }` (byte offset), in order — the confusables analogue of
183
+ * {@link findUntranslatable}.
184
+ */
185
+ function findUnmappedConfusables(text, options = {}) {
186
+ return call(() => native.findUnmappedConfusables(text, options.target ?? 'latin'));
187
+ }
163
188
  /** Generate a URL-safe slug. Mirrors the core's `SlugConfig` defaults. */
164
189
  function slugify(text, options = {}) {
165
190
  return call(() => native.slugify(text, {
@@ -186,6 +211,38 @@ function stripAccents(text) {
186
211
  function foldCase(text) {
187
212
  return native.foldCase(text);
188
213
  }
214
+ /**
215
+ * Whether `text` is a stable identity key under case folding — that is, whether
216
+ * {@link foldCase} and `String.toLowerCase()` agree on it (#619).
217
+ *
218
+ * `false` means some *other* string folds to the same value, so a table keyed on
219
+ * this one can collide: `'groß.txt'` and `'gross.txt'` are the pair node-tar
220
+ * collided on (CVE-2026-23950). It is a fact about the string and not an
221
+ * accusation — `groß` is an ordinary German word — so it is deliberately not
222
+ * folded into {@link hasAnomalies}.
223
+ */
224
+ function isCaseFoldStable(text) {
225
+ return native.isCaseFoldStable(text);
226
+ }
227
+ /**
228
+ * Which of `values` are the same name under `key` (#620).
229
+ *
230
+ * Every other disarm detector is a single-string predicate, and a collision is not
231
+ * a property of a single string — `groß.txt` is an ordinary German filename, and
232
+ * `аdmin` is only a problem next to `admin`. This is the set-shaped question that
233
+ * node-tar's `PathReservations` guard failed to ask before extracting two paths in
234
+ * parallel (CVE-2026-23950).
235
+ *
236
+ * A stronger `key` finds more collisions, including ones nobody attacked:
237
+ * `search_key` collides `Muller` with `Müller`. That is the cost of the key you
238
+ * chose, not a false positive. A group is reported only when two or more *distinct*
239
+ * inputs share a key; the same name twice is the same name twice.
240
+ *
241
+ * `options.lang` reaches `search_key` and `catalog_key` and is ignored by the rest.
242
+ */
243
+ function findKeyCollisions(values, key, options = {}) {
244
+ return call(() => native.findKeyCollisions(values, key, options.lang));
245
+ }
189
246
  /** Replace emoji with their plain names. `stripModifiers` drops skin-tone/variation marks. */
190
247
  function demojize(text, options = {}) {
191
248
  return native.demojize(text, options.stripModifiers ?? false);
@@ -312,6 +369,16 @@ function sortKey(text, options = {}) {
312
369
  function catalogKey(text, options = {}) {
313
370
  return call(() => native.catalogKey(text, options.lang ?? undefined, options.strictIso9 ?? false));
314
371
  }
372
+ /**
373
+ * ML/NLP normalization: NFKC → emoji→text → transliterate → strip accents →
374
+ * [case fold] → strip control → strip zero-width → collapse whitespace.
375
+ *
376
+ * Note this folds no confusables — it is not a homoglyph defence at any setting. Put
377
+ * {@link normalizeConfusables} in front of it when a model needs both.
378
+ */
379
+ function mlNormalize(text, options = {}) {
380
+ return call(() => native.mlNormalize(text, options.lang ?? undefined, options.emojiStyle ?? 'cldr', options.foldCase ?? true));
381
+ }
315
382
  // ── Grapheme clusters ───────────────────────────────────────────────────────
316
383
  /** Number of grapheme clusters (user-perceived characters). */
317
384
  function graphemeLen(text) {
@@ -343,8 +410,8 @@ function isSuspiciousHostname(host) {
343
410
  * {@link HostnameAnalysis} (verdict + granular signals). `isSuspiciousHostname`
344
411
  * is the boolean shorthand for `.suspicious`.
345
412
  */
346
- function analyzeHostname(host) {
347
- return native.analyzeHostname(host);
413
+ function analyzeHostname(host, options = {}) {
414
+ return call(() => native.analyzeHostname(host, options.contractions ?? false));
348
415
  }
349
416
  /** The Unicode scripts present, in first-appearance order (Common/Inherited excluded). */
350
417
  function detectScripts(text) {
@@ -384,6 +451,17 @@ function langInfo(code) {
384
451
  function scriptInfo(name) {
385
452
  return call(() => native.scriptInfo(name));
386
453
  }
454
+ /**
455
+ * The Unicode `confusables.txt` release the bundled confusable tables were folded
456
+ * from, e.g. `"17.0.0"`.
457
+ *
458
+ * Not a Unicode version for the library as a whole — disarm's case-folding and width
459
+ * tables track different releases (see docs/provenance.md). Use this to answer "is my
460
+ * confusables fold stale?" without inferring it from behaviour.
461
+ */
462
+ function confusablesVersion() {
463
+ return native.confusablesVersion();
464
+ }
387
465
  /** Every Unicode script name known to the transliteration tables. */
388
466
  function listScripts() {
389
467
  return native.listScripts();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "disarm",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Unicode confusable/text-security building blocks, powered by Rust",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",