namespace-guard 0.22.0 → 0.23.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 (42) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +50 -40
  3. package/THIRD-PARTY-NOTICES.md +120 -0
  4. package/dist/adapters/drizzle.js +6 -5
  5. package/dist/adapters/drizzle.mjs +6 -5
  6. package/dist/adapters/knex.js +1 -1
  7. package/dist/adapters/knex.mjs +1 -1
  8. package/dist/adapters/kysely.js +5 -2
  9. package/dist/adapters/kysely.mjs +5 -2
  10. package/dist/adapters/mikro-orm.js +5 -2
  11. package/dist/adapters/mikro-orm.mjs +5 -2
  12. package/dist/adapters/mongoose.js +4 -6
  13. package/dist/adapters/mongoose.mjs +4 -6
  14. package/dist/adapters/prisma.js +2 -4
  15. package/dist/adapters/prisma.mjs +2 -4
  16. package/dist/adapters/raw.js +1 -2
  17. package/dist/adapters/raw.mjs +1 -2
  18. package/dist/adapters/sequelize.js +1 -1
  19. package/dist/adapters/sequelize.mjs +1 -1
  20. package/dist/adapters/typeorm.js +5 -5
  21. package/dist/adapters/typeorm.mjs +5 -5
  22. package/dist/cli.js +8721 -5620
  23. package/dist/cli.mjs +8721 -5620
  24. package/dist/composability-vectors.js +7660 -5106
  25. package/dist/composability-vectors.mjs +7660 -5106
  26. package/dist/confusable-weights.d.mts +7 -1
  27. package/dist/confusable-weights.d.ts +7 -1
  28. package/dist/confusable-weights.js +4563 -378
  29. package/dist/confusable-weights.mjs +4563 -378
  30. package/dist/font-specific-weights.d.mts +8 -2
  31. package/dist/font-specific-weights.d.ts +8 -2
  32. package/dist/font-specific-weights.js +34890 -2492
  33. package/dist/font-specific-weights.mjs +34890 -2492
  34. package/dist/index.d.mts +162 -81
  35. package/dist/index.d.ts +162 -81
  36. package/dist/index.js +10927 -5776
  37. package/dist/index.mjs +10924 -5776
  38. package/dist/profanity-en.d.mts +5 -3
  39. package/dist/profanity-en.d.ts +5 -3
  40. package/dist/profanity-en.js +10306 -5304
  41. package/dist/profanity-en.mjs +10305 -5304
  42. package/package.json +10 -3
package/dist/index.d.ts CHANGED
@@ -1,3 +1,47 @@
1
+ /*! namespace-guard, MIT License, copyright (c) 2026 Paul Wood FRSA (@paultendo).
2
+ * Includes data from Unicode confusables.txt (2026-08-06, 01:05:35 GMT), copyright 1991-Present Unicode, Inc., Unicode License v3
3
+ * (https://www.unicode.org/terms_of_use.html), and from confusable-vision measurements by Paul Wood FRSA (@paultendo),
4
+ * CC BY 4.0 (https://github.com/paultendo/confusable-vision). */
5
+ /** Unicode confusables.txt version these maps were built from. */
6
+ declare const CONFUSABLES_DATE = "2026-08-06, 01:05:35 GMT";
7
+ /**
8
+ * Non-ASCII characters that look like Latin letters or digits, to their lowercase prototype, for pipelines that run
9
+ * NFKC first and for rejecting names that contain one: Unicode's mappings, Latin small capitals, and confusable-vision's
10
+ * strict-tier lookalikes Unicode does not map. 1018 entries.
11
+ */
12
+ declare const CONFUSABLE_MAP: Record<string, string>;
13
+ /**
14
+ * Every character that looks like Latin letters or digits, to its lowercase prototype: all of Unicode's mappings to
15
+ * Latin (including multi-character targets such as m -> rn and the ASCII entries 1 -> l, 0 -> o, | -> l), Latin small
16
+ * capitals, and confusable-vision's measured lookalikes. Case matters: look a character up as typed. 2216 entries.
17
+ */
18
+ declare const CONFUSABLE_MAP_FULL: Record<string, string>;
19
+ /** ASCII capitals Unicode maps to another letter, used where case is preserved (capital I looks like l). */
20
+ declare const CONFUSABLE_MAP_CASED: Record<string, string>;
21
+ /** One measured lookalike: the letter or digit it passes for, its prototype, and where it holds. */
22
+ type MeasuredConfusable = {
23
+ /** The ASCII letter(s) or digit(s) it was measured against. */
24
+ letter: string;
25
+ /** namespace-guard's lowercase prototype of that letter. */
26
+ target: string;
27
+ /** strict: as alike as the median accepted confusable in that context; broad: as alike as the least alike one;
28
+ * design: alike to the letter as another common font draws it. */
29
+ tier: "strict" | "broad" | "design";
30
+ /** Where it holds: font and size (px), set between neighbours with the platform's fallback. */
31
+ contexts: string[];
32
+ };
33
+ /**
34
+ * confusable-vision's in-place measurements: each character found alike to an ASCII letter or digit, with the best
35
+ * letter it passes for and where. 974 characters.
36
+ */
37
+ declare const MEASURED_CONFUSABLES: Record<string, MeasuredConfusable>;
38
+
39
+ /*! Data licensing:
40
+ * - TR39-derived mappings: Unicode confusables.txt, copyright 1991-Present Unicode, Inc.,
41
+ * Unicode License v3 (https://www.unicode.org/license.txt)
42
+ * - confusable-vision-derived weights: CC-BY-4.0 (https://creativecommons.org/licenses/by/4.0/).
43
+ * Attribution: Paul Wood FRSA (@paultendo), confusable-vision
44
+ * (https://github.com/paultendo/confusable-vision). */
1
45
  type LlmConfusableSource = "tr39" | "novel";
2
46
  type LlmConfusableMapEntry = {
3
47
  latin: string;
@@ -12,13 +56,18 @@ type LlmConfusableMapEntry = {
12
56
  };
13
57
  type LlmConfusableMap = Readonly<Record<string, readonly LlmConfusableMapEntry[]>>;
14
58
  declare const LLM_CONFUSABLE_MAP: LlmConfusableMap;
15
- declare const LLM_CONFUSABLE_MAP_PAIR_COUNT = 1591;
16
- declare const LLM_CONFUSABLE_MAP_CHAR_COUNT = 1556;
59
+ declare const LLM_CONFUSABLE_MAP_PAIR_COUNT = 2326;
60
+ declare const LLM_CONFUSABLE_MAP_CHAR_COUNT = 2219;
17
61
  declare const LLM_CONFUSABLE_MAP_SOURCE_COUNTS: Readonly<{
18
- tr39: 1425;
19
- novel: 166;
62
+ tr39: 2016;
63
+ novel: 310;
20
64
  }>;
21
65
 
66
+ /*! namespace-guard, MIT License, copyright (c) 2026 Paul Wood FRSA (@paultendo).
67
+ * Includes data from Unicode confusables.txt, copyright 1991-Present Unicode, Inc.,
68
+ * under the Unicode License v3 (https://www.unicode.org/license.txt), and from confusable-vision measurements by
69
+ * Paul Wood FRSA (@paultendo), CC BY 4.0 (https://github.com/paultendo/confusable-vision). */
70
+
22
71
  /** A database table or model to check for slug/handle collisions. */
23
72
  type NamespaceSource = {
24
73
  /** Table/model name (must match the adapter's lookup key) */
@@ -37,8 +86,11 @@ type NamespaceValidatorResult = {
37
86
  available: false;
38
87
  message: string;
39
88
  } | null;
40
- /** Async validator hook type used by `NamespaceConfig.validators`. */
41
- type NamespaceValidator = (value: string) => Promise<NamespaceValidatorResult>;
89
+ /** Async validator hook type used by `NamespaceConfig.validators`. Receives the normalized value, and the identifier
90
+ * as typed (before lowercasing), since whether a character looks like a letter can depend on its case. */
91
+ type NamespaceValidator = (value: string, context?: {
92
+ identifier: string;
93
+ }) => Promise<NamespaceValidatorResult>;
42
94
  /** Configuration for a namespace guard instance. */
43
95
  type NamespaceConfig = {
44
96
  /** Reserved names - flat list, Set, or categorized record */
@@ -83,10 +135,13 @@ type NamespaceConfig = {
83
135
  };
84
136
  /** Default risk policy for `checkRisk()` / `enforceRisk()` */
85
137
  risk?: {
86
- /** Include reserved names as protected targets by default (default: true) */
138
+ /** Include reserved names as protected targets by default (default: true). A reserved name that is not in
139
+ * `protect` is matched by lookalikes only. */
87
140
  includeReserved?: boolean;
88
141
  /** Default protected targets when none are passed to `checkRisk`/`enforceRisk` (default: none). */
89
142
  protect?: string[];
143
+ /** Count leetspeak swaps (4 for a, 1 for i, 5 for s) as lookalikes (default: false) */
144
+ leetspeak?: boolean;
90
145
  /** Number of top matches returned by default (default: 3) */
91
146
  maxMatches?: number;
92
147
  /** Default warn threshold for score/action mapping (default: 45) */
@@ -133,18 +188,27 @@ type ProfanityVariantProfile = "balanced" | "aggressive";
133
188
  type ProfanityValidatorOptions = {
134
189
  /** Custom rejection message (default: "That name is not allowed."). */
135
190
  message?: string;
136
- /** Check if identifier contains a blocked word as a substring (default: `true`). */
191
+ /** Also refuse names that contain a listed word, not only names that are one (default: `true`). Short entries,
192
+ * entries of several words and entries common inside English words are matched there only as whole words. */
137
193
  checkSubstrings?: boolean;
138
194
  /** `basic`: lowercase matching only. `evasion`: Unicode+substitute folding (default: `evasion`). */
139
195
  mode?: ProfanityValidationMode;
140
196
  /** Variant strictness for evasion mode (default: `balanced`). */
141
197
  variantProfile?: ProfanityVariantProfile;
142
- /** Minimum blocked-word length for substring checks (default: `4`). Exact matches are always checked. */
198
+ /** Fewest letters (digits count, spaces and symbols don't) an entry needs to be looked for inside longer words
199
+ * (default: `4`). Shorter entries are matched as whole words: the whole name, or a part between separators or digits. */
143
200
  minSubstringLength?: number;
144
201
  /** Confusable map used for evasion folding (default: `CONFUSABLE_MAP_FULL`). */
145
202
  map?: Record<string, string>;
146
203
  /** Max folded candidates generated per value in evasion mode (default: `64`). */
147
204
  maxFoldVariants?: number;
205
+ /** Words that contain a listed word but are fine, such as names and places: `["scunthorpe", "dickson"]`. A listed
206
+ * word found only inside one of these doesn't count, wherever the allowed word appears in the name
207
+ * ("scunthorpe-fc"), while a listed word outside it still does. Case and lookalike letters are folded as for the
208
+ * list, but a digit in the name is not read as an allowed word's letter ("scunth0rpe" is refused). An allowed word
209
+ * that is itself a listed word lets that word through. An entry written with separators ("chorlton-cum-hardy")
210
+ * is exact: it lets through only a name that is exactly it, separators included. (default: none) */
211
+ allowlist?: readonly string[];
148
212
  };
149
213
  /** Options for `createInvisibleCharacterValidator()`. */
150
214
  type InvisibleCharacterValidatorOptions = {
@@ -157,9 +221,20 @@ type InvisibleCharacterValidatorOptions = {
157
221
  /** Reject combining marks (Unicode category `M*`) often used for visual obfuscation (default: `false`). */
158
222
  rejectCombiningMarks?: boolean;
159
223
  };
160
- /** Options for `canonicalise()` LLM preprocessing. */
224
+ /**
225
+ * Options for `canonicalise()` LLM preprocessing.
226
+ *
227
+ * A word is rewritten when it shows a sign of tampering: it mixes Latin with another script, it has a letter not in
228
+ * modern use that is built on a Latin letter (ɦ, ꞡ, ɏ; a fullwidth or mathematical letter), or it has Latin letters
229
+ * and a listed lookalike scoring at least `threshold`. In such a word every lookalike is replaced: listed lookalikes
230
+ * whatever their score, and any letter built on a Latin letter (ŧ, İ) by its base letter, in the case of the word
231
+ * around it. A word with no such sign is left as it is, so Turkish İstanbul or Russian Москва pass through.
232
+ */
161
233
  type CanonicaliseOptions = {
162
- /** Minimum visual score required for replacement (default: `0.7`). */
234
+ /**
235
+ * The visual score at which a listed lookalike on its own marks a word as tampered (default: `0.7`). When you set it,
236
+ * it is also a floor: lookalikes scoring below it are never replaced.
237
+ */
163
238
  threshold?: number;
164
239
  /** Include confusable-vision novel discoveries in addition to TR39 mappings (default: `true`). */
165
240
  includeNovel?: boolean;
@@ -168,14 +243,15 @@ type CanonicaliseOptions = {
168
243
  /**
169
244
  * Canonicalisation strategy (default: `"mixed"`).
170
245
  *
171
- * - `"mixed"` -- only replace confusable characters inside tokens that already
172
- * contain Latin letters. Standalone non-Latin words (e.g. "Москва") are
173
- * preserved. Safe for multilingual text.
246
+ * - `"mixed"` -- only rewrite words that show a sign of tampering (see above).
247
+ * Standalone non-Latin words (e.g. "Москва") and ordinary accented words
248
+ * (e.g. "İstanbul") are preserved. Safe for multilingual text.
174
249
  *
175
250
  * - `"all"` -- replace every confusable character regardless of surrounding
176
- * context. Use this when the document is known to be Latin-script (e.g.
177
- * an English contract) and you want to catch attackers who substitute
178
- * every character in a word.
251
+ * context, and fold every letter built on a Latin letter (ŧ, İ, ö) to it.
252
+ * Use this when the document is known to be Latin-script (e.g. an English
253
+ * contract) and you want to catch attackers who substitute every character
254
+ * in a word.
179
255
  */
180
256
  strategy?: "mixed" | "all";
181
257
  /**
@@ -206,10 +282,13 @@ type ScanFinding = {
206
282
  script: string;
207
283
  /** Canonical Latin equivalent selected by the lookup table. */
208
284
  latinEquivalent: string;
209
- /** Visual similarity score for this mapping (0–1, from RaySpace measurement). */
285
+ /** Visual similarity score for this mapping (0–1, from RaySpace measurement; 0 for a `fold`). */
210
286
  visualScore: number;
211
- /** Mapping source (`tr39` baseline or `novel` discovery). */
212
- source: "tr39" | "novel";
287
+ /**
288
+ * Mapping source: `tr39` baseline, `novel` discovery, or `fold`, a letter built on a Latin letter (ŧ, ɦ, İ) folded
289
+ * to it in a word that shows tampering.
290
+ */
291
+ source: "tr39" | "novel" | "fold";
213
292
  /** UTF-16 code-unit offset in the input string. */
214
293
  index: number;
215
294
  /** Token/word containing this character. */
@@ -244,6 +323,10 @@ type SkeletonOptions = {
244
323
  * Default: `CONFUSABLE_MAP_FULL` (complete TR39 map, no NFKC filtering).
245
324
  * Pass `CONFUSABLE_MAP` if your pipeline runs NFKC before calling skeleton(). */
246
325
  map?: Record<string, string>;
326
+ /** Treat the input as shown with its case, so ASCII capitals Unicode maps to another letter count as that letter
327
+ * (capital I as l: "paypaI" has the skeleton of "paypal"). For names displayed as typed. Default `false`: names
328
+ * compare without case ("ADMIN" is "admin"). `areConfusable()` checks both. */
329
+ preserveCase?: boolean;
247
330
  /** Also remove diacritics (the combining marks of the Combining Diacritical Marks blocks, such as a dot below,
248
331
  * an accent or a hook), so `ạ` matches `a` and `ẹ` matches `e`. TR39 keeps them, but a small mark is easy to
249
332
  * miss, and Chromium removes Latin diacritics when it checks a domain against top domains. Marks that scripts
@@ -253,11 +336,11 @@ type SkeletonOptions = {
253
336
  /** Options for `areConfusable()` with optional weight-based matching. */
254
337
  type AreConfusableOptions = SkeletonOptions & {
255
338
  /** Optional measured visual weights for weight-based confusable matching.
256
- * When provided, character pairs in the weight table are also considered confusable. */
339
+ * When provided, a measured pair at the same position also counts as alike. */
257
340
  weights?: ConfusableWeights;
258
341
  /** Filter weights by deployment context.
259
- * - `'identifier'`: only apply weights for XID_Continue sources
260
- * - `'domain'`: only apply weights for IDNA PVALID sources
342
+ * - `'identifier'`: only apply pairs whose two characters are both XID_Continue
343
+ * - `'domain'`: only apply pairs whose two characters are both IDNA PVALID
261
344
  * - `'all'` (default): apply all weights regardless of properties */
262
345
  context?: "identifier" | "domain" | "all";
263
346
  };
@@ -290,8 +373,8 @@ type ConfusableDistanceOptions = {
290
373
  * and novel pairs (not in TR39 map) use their visual-weight cost. */
291
374
  weights?: ConfusableWeights;
292
375
  /** Filter weights by deployment context.
293
- * - `'identifier'`: only apply weights for XID_Continue sources
294
- * - `'domain'`: only apply weights for IDNA PVALID sources
376
+ * - `'identifier'`: only apply pairs whose two characters are both XID_Continue
377
+ * - `'domain'`: only apply pairs whose two characters are both IDNA PVALID
295
378
  * - `'all'` (default): apply all weights regardless of properties */
296
379
  context?: "identifier" | "domain" | "all";
297
380
  };
@@ -366,6 +449,14 @@ type RiskReason = {
366
449
  type RiskLevel = "low" | "medium" | "high";
367
450
  /** Policy action derived from the configured thresholds. */
368
451
  type RiskAction = "allow" | "warn" | "block";
452
+ /**
453
+ * How a name matched a protected target:
454
+ * - `exact`: it is the target.
455
+ * - `lookalike`: it differs only by lookalikes and invisible characters, or its skeleton collides. Can block.
456
+ * - `lookalike-extension`: it spells the whole target with a lookalike and adds letters. Warns at most.
457
+ * - `typo`: one letter changed, added, dropped or swapped, not a lookalike, from a name in `protect`. Warns at most.
458
+ */
459
+ type RiskMatchEvidence = "exact" | "lookalike" | "lookalike-extension" | "typo";
369
460
  /** A nearest protected target returned by risk scoring. */
370
461
  type RiskMatch = {
371
462
  target: string;
@@ -373,14 +464,20 @@ type RiskMatch = {
373
464
  distance: number;
374
465
  chainDepth: number;
375
466
  skeletonEqual: boolean;
467
+ /** How the name matched the target. */
468
+ evidence: RiskMatchEvidence;
376
469
  reasons: string[];
377
470
  };
378
471
  /** Options for `guard.checkRisk()`. */
379
472
  type CheckRiskOptions = {
380
473
  /** Additional high-value identifiers to protect against confusable variants. */
381
474
  protect?: string[];
382
- /** Include configured reserved names in the protected target set (default: true). */
475
+ /** Include configured reserved names in the protected target set (default: true). A reserved name that is not in
476
+ * `protect` is matched by lookalikes only, not by a one-letter typo. */
383
477
  includeReserved?: boolean;
478
+ /** Count leetspeak swaps (4 for a, 1 for i, 5 for s, $ for s) as lookalikes, so p4ypal or adm1n can block
479
+ * (default: false). The table is the profanity matcher's aggressive one. */
480
+ leetspeak?: boolean;
384
481
  /** Confusable map used for skeletoning and distance scoring (default: `CONFUSABLE_MAP_FULL`). */
385
482
  map?: Record<string, string>;
386
483
  /** Number of highest-risk matches to return (default: 3). */
@@ -397,6 +494,9 @@ type RiskCheckResult = {
397
494
  score: number;
398
495
  level: RiskLevel;
399
496
  action: RiskAction;
497
+ /** False when the name shows nothing visual: no lookalike or exact match of a protected target, and no lookalike,
498
+ * invisible or mixed-script characters. Its score is then held below the block threshold, whatever it is. */
499
+ canBlock: boolean;
400
500
  reasons: RiskReason[];
401
501
  matches: RiskMatch[];
402
502
  };
@@ -452,6 +552,8 @@ type NamespaceProfileName = "consumer-handle" | "org-slug" | "developer-id";
452
552
  type NamespaceProfilePreset = {
453
553
  description: string;
454
554
  pattern: RegExp;
555
+ /** The message for a name that doesn't match `pattern`, used unless you set `messages.invalid` or your own pattern */
556
+ invalidMessage?: string;
455
557
  normalizeUnicode: boolean;
456
558
  allowPurelyNumeric: boolean;
457
559
  risk: Required<NonNullable<NamespaceConfig["risk"]>>;
@@ -504,16 +606,24 @@ declare function createPredicateValidator(predicate: (value: string) => boolean
504
606
  /**
505
607
  * Create a validator that rejects identifiers containing profanity or offensive words.
506
608
  *
507
- * Supply your own word list - no words are bundled with the library.
609
+ * Supply your own word list, or the English list from `namespace-guard/profanity-en`.
508
610
  * The returned function is compatible with `config.validators`.
509
611
  *
612
+ * An entry of one word with at least `minSubstringLength` letters is looked for anywhere in a name ("shitposter").
613
+ * Other entries are matched as whole words: the whole name, or a part of it between separators, digits or its ends,
614
+ * with or without separators between its letters. These are shorter entries ("ass" refuses "my-ass", not "class"),
615
+ * entries written as several words ("h e l l", "blow job"), and entries whose letters are common inside ordinary
616
+ * English words (122 sequences measured from the English list, such as anal, rape and cock). Words on `allowlist`
617
+ * shield the listed words inside them: with `allowlist: ["scunthorpe"]`, "scunthorpe-fc" and "scun-thorpe" pass, and
618
+ * "xcuntx" and "s-cunt-horpe" don't, since a part of a name that is a listed word on its own still counts.
619
+ *
510
620
  * @param words - Array of words to block
511
621
  * @param options - Optional settings
512
622
  * @param options.message - Custom rejection message (default: "That name is not allowed.")
513
- * @param options.checkSubstrings - Check if identifier contains a blocked word as a substring (default: true)
623
+ * @param options.checkSubstrings - Also refuse names that contain a listed word, not only names that are one (default: true)
514
624
  * @param options.mode - `basic` (lowercase only) or `evasion` (Unicode/substitute folding) (default: `evasion`)
515
625
  * @param options.variantProfile - `balanced` (precision-first) or `aggressive` (broader substitutes) (default: `balanced`)
516
- * @param options.minSubstringLength - Minimum blocked-word length used in substring checks (default: `4`)
626
+ * @param options.minSubstringLength - Fewest letters an entry needs to be looked for inside longer words (default: `4`)
517
627
  * @param options.map - Confusable map used by `mode: "evasion"` (default: `CONFUSABLE_MAP_FULL`)
518
628
  * @param options.maxFoldVariants - Max folded candidates considered in evasion mode (default: 64)
519
629
  * @returns An async validator function for use in `config.validators`
@@ -532,39 +642,6 @@ declare function createPredicateValidator(predicate: (value: string) => boolean
532
642
  * ```
533
643
  */
534
644
  declare function createProfanityValidator(words: readonly string[], options?: ProfanityValidatorOptions): NamespaceValidator;
535
- /**
536
- * Mapping of visually confusable Unicode characters to their Latin/digit equivalents.
537
- * Generated from Unicode TR39 confusables.txt + supplemental Latin small capitals.
538
- * Covers every single-character mapping to a lowercase Latin letter or digit,
539
- * excluding characters already handled by NFKC normalization (either collapsed
540
- * to the same target, or mapped to a different valid Latin char/digit).
541
- * Regenerate: `npx tsx scripts/generate-confusables.ts`
542
- *
543
- * Data source: Unicode confusables.txt (https://unicode.org/Public/security/latest/confusables.txt)
544
- * Copyright 1991-Present Unicode, Inc. Licensed under the Unicode License v3.
545
- * See https://www.unicode.org/terms_of_use.html
546
- */
547
- declare const CONFUSABLE_MAP: Record<string, string>;
548
- /**
549
- * Complete TR39 confusable mapping: every single-character mapping to a
550
- * lowercase Latin letter or digit from confusables.txt, with no NFKC filtering.
551
- *
552
- * Use this when your pipeline does NOT run NFKC normalization before confusable
553
- * detection (which is most real-world systems: TR39 skeleton uses NFD, Chromium
554
- * uses NFD, Rust uses NFC, django-registration uses no normalization at all).
555
- *
556
- * Includes ~1,400 entries vs CONFUSABLE_MAP's ~613 NFKC-deduped entries.
557
- * The additional entries cover characters that NFKC normalization would handle
558
- * (mathematical alphanumerics, fullwidth forms, etc.) plus the 31 entries where
559
- * TR39 and NFKC disagree on the target letter.
560
- *
561
- * Regenerate: `npx tsx scripts/generate-confusables.ts`
562
- *
563
- * Data source: Unicode confusables.txt (https://unicode.org/Public/security/latest/confusables.txt)
564
- * Copyright 1991-Present Unicode, Inc. Licensed under the Unicode License v3.
565
- * See https://www.unicode.org/terms_of_use.html
566
- */
567
- declare const CONFUSABLE_MAP_FULL: Record<string, string>;
568
645
  /**
569
646
  * Derive the set of characters where TR39 prototype mapping and NFKC lowercase
570
647
  * mapping disagree on single ASCII letter/digit outcomes.
@@ -576,7 +653,7 @@ declare function deriveNfkcTr39DivergenceVectors(map?: Record<string, string>):
576
653
  */
577
654
  declare const NFKC_TR39_DIVERGENCE_VECTORS: NfkcTr39DivergenceVector[];
578
655
  /** Named composability regression suite (TR39-full vs NFKC lowercase). */
579
- declare const COMPOSABILITY_VECTOR_SUITE = "nfkc-tr39-divergence-v1";
656
+ declare const COMPOSABILITY_VECTOR_SUITE = "nfkc-tr39-divergence-v2";
580
657
  /** Named alias for `NFKC_TR39_DIVERGENCE_VECTORS` for cross-library regression tests. */
581
658
  declare const COMPOSABILITY_VECTORS: readonly ComposabilityVector[];
582
659
  /** Number of vectors in the composability regression suite. */
@@ -586,9 +663,10 @@ declare const COMPOSABILITY_VECTORS_COUNT: number;
586
663
  *
587
664
  * Catches spoofing attacks where characters from other scripts are substituted for
588
665
  * visually identical Latin characters (e.g., Cyrillic "а" for Latin "a" in "admin").
589
- * Uses a comprehensive mapping of 613 character pairs generated from Unicode TR39
590
- * confusables.txt, covering Cyrillic, Greek, Armenian, Cherokee, IPA, Latin small
591
- * capitals, Canadian Syllabics, Georgian, Lisu, Coptic, and many other scripts.
666
+ * Uses `CONFUSABLE_MAP` (1,018 characters): Unicode's confusables.txt with NFKC's own
667
+ * mappings left out, plus the lookalikes confusable-vision found alike in place. It
668
+ * refuses any name containing one of these characters, including names written wholly
669
+ * in another script (москва) and Latin letters listed as lookalikes (í, ı).
592
670
  *
593
671
  * @param options - Optional settings
594
672
  * @param options.message - Custom rejection message (default: "That name contains characters that could be confused with other letters.")
@@ -639,14 +717,16 @@ declare function canonicalise(text: string, options?: CanonicaliseOptions): stri
639
717
  */
640
718
  declare function scan(text: string, options?: ScanOptions): ScanResult;
641
719
  /**
642
- * Fast gate for LLM pipelines.
720
+ * Fast gate for LLM pipelines: `true` exactly when `canonicalise()` with the
721
+ * same options would leave the text unchanged.
643
722
  *
644
- * With the default `strategy: "mixed"`, returns `false` as soon as a
645
- * mixed-script confusable substitution is found. Standalone non-Latin
723
+ * With the default `strategy: "mixed"`, returns `false` as soon as a word
724
+ * that shows a sign of tampering has a character to rewrite (see
725
+ * `CanonicaliseOptions`). Standalone non-Latin words and ordinary accented
646
726
  * words do not fail this gate.
647
727
  *
648
- * With `strategy: "all"`, returns `false` if any confusable character is
649
- * found, regardless of surrounding context.
728
+ * With `strategy: "all"`, returns `false` if any confusable character or
729
+ * letter built on a Latin letter is found, regardless of surrounding context.
650
730
  */
651
731
  declare function isClean(text: string, options?: ScanOptions): boolean;
652
732
  /**
@@ -689,8 +769,9 @@ declare function confusableDistance(a: string, b: string, options?: ConfusableDi
689
769
  * Check whether two strings are visually confusable.
690
770
  *
691
771
  * Without `weights`, compares TR39 skeletons only (backward compatible).
692
- * With `weights`, also checks whether any character pair in `a` x `b`
693
- * has a measured visual weight, enabling cross-script confusable detection.
772
+ * With `weights`, also lines the two strings up character by character: they are confusable when each position's
773
+ * characters share a skeleton or are a measured pair (in `context`), in order, with one character allowed to stand
774
+ * for several (m for rn). This finds pairs no standard lists, such as two non-Latin scripts.
694
775
  *
695
776
  * @param a - First string
696
777
  * @param b - Second string
@@ -701,8 +782,8 @@ declare function confusableDistance(a: string, b: string, options?: ConfusableDi
701
782
  * ```ts
702
783
  * areConfusable("paypal", "\u0440\u0430ypal") // true (skeleton match)
703
784
  * areConfusable("hello", "world") // false
704
- * areConfusable("\u1175", "\u4E28", { weights }) // true (cross-script weight)
705
- * areConfusable("\u1175", "\u4E28") // false (no weights = skeleton only)
785
+ * areConfusable("\u3163", "\u4E28", { weights }) // true: Hangul \u3163 and Han \u4E28, a measured pair
786
+ * areConfusable("\u3163", "\u4E28") // false (no weights = skeleton only)
706
787
  * ```
707
788
  */
708
789
  declare function areConfusable(a: string, b: string, options?: AreConfusableOptions): boolean;
@@ -739,7 +820,7 @@ type CrossScriptRiskResult = {
739
820
  * @example
740
821
  * ```ts
741
822
  * detectCrossScriptRisk("hello") // riskLevel: "none"
742
- * detectCrossScriptRisk("\u1175\u4E28", { weights }) // riskLevel: "high"
823
+ * detectCrossScriptRisk("\u3163\u4E28", { weights }) // riskLevel: "high", scripts: ["han", "hangul"]
743
824
  * ```
744
825
  */
745
826
  declare function detectCrossScriptRisk(identifier: string, options?: {
@@ -801,8 +882,8 @@ type DomainSpoofOptions = {
801
882
  * character in the target, verified against the confusable map and optional
802
883
  * measured weights.
803
884
  *
804
- * Mixed-script substitutions are deliberately excluded because registrars
805
- * reject them.
885
+ * Mixed-script substitutions are left out because most registries refuse them
886
+ * (some registered ones exist; d0ma1n finds those).
806
887
  *
807
888
  * @param label - The suspicious domain label to check (e.g. Cyrillic "раураl")
808
889
  * @param target - The legitimate domain label (e.g. "paypal")
@@ -817,9 +898,9 @@ type DomainSpoofOptions = {
817
898
  * // Full-Cyrillic lookalike — realistic, registrable spoof
818
899
  * isDomainSpoof("\u0440\u0430\u0443\u0440\u0430\u04cf", "paypal",
819
900
  * { weights: CONFUSABLE_WEIGHTS });
820
- * // { spoof: true, script: "cyrillic", danger: 0.91, substitutions: [...] }
901
+ * // { spoof: true, script: "cyrillic", danger: 0.781, substitutions: [...] }
821
902
  *
822
- * // Mixed-script — cannot be registered, not a spoof
903
+ * // Mixed-script — most registries refuse it, so it isn't checked
823
904
  * isDomainSpoof("\u0440aypal", "paypal",
824
905
  * { weights: CONFUSABLE_WEIGHTS });
825
906
  * // { spoof: false }
@@ -904,4 +985,4 @@ declare function createNamespaceGuard(config: NamespaceConfig, adapter: Namespac
904
985
  /** The guard instance returned by `createNamespaceGuard`. */
905
986
  type NamespaceGuard = ReturnType<typeof createNamespaceGuard>;
906
987
 
907
- export { type AreConfusableOptions, type AssertClaimableOptions, COMPOSABILITY_VECTORS, COMPOSABILITY_VECTORS_COUNT, COMPOSABILITY_VECTOR_SUITE, CONFUSABLE_MAP, CONFUSABLE_MAP_FULL, type CanonicaliseOptions, type CheckManyOptions, type CheckResult, type CheckRiskOptions, type ClaimOptions, type ClaimResult, type ComposabilityVector, type ConfusableDistanceOptions, type ConfusableDistanceResult, type ConfusableDistanceStep, type ConfusableWeight, type ConfusableWeights, type CrossScriptRiskResult, DEFAULT_PROTECTED_TOKENS, type DomainSpoofOptions, type DomainSpoofResult, type DomainSpoofSubstitution, type EnforceRiskOptions, type EnforceRiskResult, type FindOneOptions, type InvisibleCharacterValidatorOptions, LLM_CONFUSABLE_MAP, LLM_CONFUSABLE_MAP_CHAR_COUNT, LLM_CONFUSABLE_MAP_PAIR_COUNT, LLM_CONFUSABLE_MAP_SOURCE_COUNTS, NAMESPACE_PROFILES, NFKC_TR39_DIVERGENCE_VECTORS, type NamespaceAdapter, type NamespaceConfig, type NamespaceGuard, type NamespaceProfileName, type NamespaceProfilePreset, type NamespaceSource, type NamespaceValidator, type NamespaceValidatorResult, type NfkcTr39DivergenceVector, type OwnershipScope, type PredicateValidatorOptions, type ProfanityValidationMode, type ProfanityValidatorOptions, type ProfanityVariantProfile, type RiskAction, type RiskCheckResult, type RiskLevel, type RiskMatch, type RiskReason, type RiskReasonCode, type ScanFinding, type ScanOptions, type ScanResult, type SkeletonOptions, type SuggestStrategyName, type UniqueViolationDetector, areConfusable, canonicalise, confusableDistance, createHomoglyphValidator, createInvisibleCharacterValidator, createNamespaceGuard, createNamespaceGuardWithProfile, createPredicateValidator, createProfanityValidator, deriveNfkcTr39DivergenceVectors, detectCrossScriptRisk, isClean, isDomainSpoof, isLikelyUniqueViolationError, normalize, scan, skeleton };
988
+ export { type AreConfusableOptions, type AssertClaimableOptions, COMPOSABILITY_VECTORS, COMPOSABILITY_VECTORS_COUNT, COMPOSABILITY_VECTOR_SUITE, CONFUSABLES_DATE, CONFUSABLE_MAP, CONFUSABLE_MAP_CASED, CONFUSABLE_MAP_FULL, type CanonicaliseOptions, type CheckManyOptions, type CheckResult, type CheckRiskOptions, type ClaimOptions, type ClaimResult, type ComposabilityVector, type ConfusableDistanceOptions, type ConfusableDistanceResult, type ConfusableDistanceStep, type ConfusableWeight, type ConfusableWeights, type CrossScriptRiskResult, DEFAULT_PROTECTED_TOKENS, type DomainSpoofOptions, type DomainSpoofResult, type DomainSpoofSubstitution, type EnforceRiskOptions, type EnforceRiskResult, type FindOneOptions, type InvisibleCharacterValidatorOptions, LLM_CONFUSABLE_MAP, LLM_CONFUSABLE_MAP_CHAR_COUNT, LLM_CONFUSABLE_MAP_PAIR_COUNT, LLM_CONFUSABLE_MAP_SOURCE_COUNTS, type LlmConfusableMapEntry, MEASURED_CONFUSABLES, type MeasuredConfusable, NAMESPACE_PROFILES, NFKC_TR39_DIVERGENCE_VECTORS, type NamespaceAdapter, type NamespaceConfig, type NamespaceGuard, type NamespaceProfileName, type NamespaceProfilePreset, type NamespaceSource, type NamespaceValidator, type NamespaceValidatorResult, type NfkcTr39DivergenceVector, type OwnershipScope, type PredicateValidatorOptions, type ProfanityValidationMode, type ProfanityValidatorOptions, type ProfanityVariantProfile, type RiskAction, type RiskCheckResult, type RiskLevel, type RiskMatch, type RiskMatchEvidence, type RiskReason, type RiskReasonCode, type ScanFinding, type ScanOptions, type ScanResult, type SkeletonOptions, type SuggestStrategyName, type UniqueViolationDetector, areConfusable, canonicalise, confusableDistance, createHomoglyphValidator, createInvisibleCharacterValidator, createNamespaceGuard, createNamespaceGuardWithProfile, createPredicateValidator, createProfanityValidator, deriveNfkcTr39DivergenceVectors, detectCrossScriptRisk, isClean, isDomainSpoof, isLikelyUniqueViolationError, normalize, scan, skeleton };