@resq-systems/security 1.0.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +236 -33
  2. package/lib/controls/address.d.mts +142 -0
  3. package/lib/controls/address.d.mts.map +1 -0
  4. package/lib/controls/address.mjs +533 -0
  5. package/lib/controls/address.mjs.map +1 -0
  6. package/lib/controls/csrf.d.mts +91 -0
  7. package/lib/controls/csrf.d.mts.map +1 -0
  8. package/lib/controls/csrf.mjs +200 -0
  9. package/lib/controls/csrf.mjs.map +1 -0
  10. package/lib/controls/index.d.mts +8 -0
  11. package/lib/controls/index.mjs +8 -0
  12. package/lib/controls/origin.d.mts +95 -0
  13. package/lib/controls/origin.d.mts.map +1 -0
  14. package/lib/controls/origin.mjs +156 -0
  15. package/lib/controls/origin.mjs.map +1 -0
  16. package/lib/controls/payload.d.mts +84 -0
  17. package/lib/controls/payload.d.mts.map +1 -0
  18. package/lib/controls/payload.mjs +147 -0
  19. package/lib/controls/payload.mjs.map +1 -0
  20. package/lib/controls/query.d.mts +157 -0
  21. package/lib/controls/query.d.mts.map +1 -0
  22. package/lib/controls/query.mjs +368 -0
  23. package/lib/controls/query.mjs.map +1 -0
  24. package/lib/controls/redirect.d.mts +92 -0
  25. package/lib/controls/redirect.d.mts.map +1 -0
  26. package/lib/controls/redirect.mjs +110 -0
  27. package/lib/controls/redirect.mjs.map +1 -0
  28. package/lib/controls/upload.d.mts +108 -0
  29. package/lib/controls/upload.d.mts.map +1 -0
  30. package/lib/controls/upload.mjs +374 -0
  31. package/lib/controls/upload.mjs.map +1 -0
  32. package/lib/crypto.d.mts +18 -5
  33. package/lib/crypto.d.mts.map +1 -1
  34. package/lib/crypto.mjs +35 -24
  35. package/lib/crypto.mjs.map +1 -1
  36. package/lib/hash.d.mts +83 -0
  37. package/lib/hash.d.mts.map +1 -0
  38. package/lib/hash.mjs +111 -0
  39. package/lib/hash.mjs.map +1 -0
  40. package/lib/index.d.mts +18 -2
  41. package/lib/index.mjs +20 -2
  42. package/lib/paths.d.mts +92 -0
  43. package/lib/paths.d.mts.map +1 -0
  44. package/lib/paths.mjs +140 -0
  45. package/lib/paths.mjs.map +1 -0
  46. package/lib/sanitize.d.mts +137 -35
  47. package/lib/sanitize.d.mts.map +1 -1
  48. package/lib/sanitize.mjs +170 -46
  49. package/lib/sanitize.mjs.map +1 -1
  50. package/lib/threats/capec.generated.d.mts +59 -0
  51. package/lib/threats/capec.generated.d.mts.map +1 -0
  52. package/lib/threats/capec.generated.mjs +644 -0
  53. package/lib/threats/capec.generated.mjs.map +1 -0
  54. package/lib/threats/engine.d.mts +94 -0
  55. package/lib/threats/engine.d.mts.map +1 -0
  56. package/lib/threats/engine.mjs +167 -0
  57. package/lib/threats/engine.mjs.map +1 -0
  58. package/lib/threats/index.d.mts +11 -0
  59. package/lib/threats/index.mjs +11 -0
  60. package/lib/threats/rules/datastore.d.mts +13 -0
  61. package/lib/threats/rules/datastore.d.mts.map +1 -0
  62. package/lib/threats/rules/datastore.mjs +366 -0
  63. package/lib/threats/rules/datastore.mjs.map +1 -0
  64. package/lib/threats/rules/index.d.mts +54 -0
  65. package/lib/threats/rules/index.d.mts.map +1 -0
  66. package/lib/threats/rules/index.mjs +121 -0
  67. package/lib/threats/rules/index.mjs.map +1 -0
  68. package/lib/threats/rules/markup.d.mts +28 -0
  69. package/lib/threats/rules/markup.d.mts.map +1 -0
  70. package/lib/threats/rules/markup.mjs +373 -0
  71. package/lib/threats/rules/markup.mjs.map +1 -0
  72. package/lib/threats/rules/protocol.d.mts +49 -0
  73. package/lib/threats/rules/protocol.d.mts.map +1 -0
  74. package/lib/threats/rules/protocol.mjs +175 -0
  75. package/lib/threats/rules/protocol.mjs.map +1 -0
  76. package/lib/threats/rules/system.d.mts +19 -0
  77. package/lib/threats/rules/system.d.mts.map +1 -0
  78. package/lib/threats/rules/system.mjs +455 -0
  79. package/lib/threats/rules/system.mjs.map +1 -0
  80. package/lib/threats/rules/web.d.mts +26 -0
  81. package/lib/threats/rules/web.d.mts.map +1 -0
  82. package/lib/threats/rules/web.mjs +412 -0
  83. package/lib/threats/rules/web.mjs.map +1 -0
  84. package/lib/threats/scoring.d.mts +59 -0
  85. package/lib/threats/scoring.d.mts.map +1 -0
  86. package/lib/threats/scoring.mjs +111 -0
  87. package/lib/threats/scoring.mjs.map +1 -0
  88. package/lib/threats/types.d.mts +245 -0
  89. package/lib/threats/types.d.mts.map +1 -0
  90. package/lib/threats/types.mjs +52 -0
  91. package/lib/threats/types.mjs.map +1 -0
  92. package/lib/threats/variants.d.mts +57 -0
  93. package/lib/threats/variants.d.mts.map +1 -0
  94. package/lib/threats/variants.mjs +144 -0
  95. package/lib/threats/variants.mjs.map +1 -0
  96. package/lib/unicode/confusables.d.mts +82 -0
  97. package/lib/unicode/confusables.d.mts.map +1 -0
  98. package/lib/unicode/confusables.mjs +954 -0
  99. package/lib/unicode/confusables.mjs.map +1 -0
  100. package/lib/unicode/index.d.mts +126 -0
  101. package/lib/unicode/index.d.mts.map +1 -0
  102. package/lib/unicode/index.mjs +288 -0
  103. package/lib/unicode/index.mjs.map +1 -0
  104. package/lib/validators.d.mts +341 -164
  105. package/lib/validators.d.mts.map +1 -1
  106. package/lib/validators.mjs +519 -338
  107. package/lib/validators.mjs.map +1 -1
  108. package/package.json +35 -8
@@ -0,0 +1,954 @@
1
+ //#region src/unicode/confusables.ts
2
+ /**
3
+ * Copyright 2026 ResQ Systems, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * @fileoverview Confusable-character data and UTS #39 skeleton generation.
19
+ *
20
+ * A *skeleton* is an opaque comparison key. Two strings whose skeletons are equal are
21
+ * visually confusable; that is the only thing a skeleton means. It is not a normalized
22
+ * identifier, it is not safe to display, and it must never be stored in place of what
23
+ * the user actually typed — the point is to keep the original for display and compare
24
+ * skeletons for collisions.
25
+ *
26
+ * Coverage is split in two:
27
+ *
28
+ * - **Algorithmic ranges** — fullwidth forms, mathematical alphanumerics, and
29
+ * enclosed/parenthesized letters are contiguous ranges at a fixed offset from
30
+ * ASCII, so they fold by computation rather than by table. That covers several
31
+ * thousand code points in a few lines.
32
+ * - **A curated table** — the Cyrillic, Greek, Armenian, Cherokee, Coptic, and
33
+ * Canadian-Aboriginal glyphs that render like Latin letters, written as numeric
34
+ * code points so the source stays ASCII and every entry is reviewable in a diff.
35
+ *
36
+ * This is not the complete `confusables.txt` dataset (~6 000 mappings). It covers the
37
+ * Latin-target subset, which is what matters for the identifiers this package guards:
38
+ * usernames, domains, org names, package names. Callers needing full UTS #39 coverage
39
+ * should layer a dedicated dataset on top and track Unicode's release cadence.
40
+ *
41
+ * @module @resq-systems/security/unicode/confusables
42
+ */
43
+ /**
44
+ * Confusable code points grouped by the ASCII prototype they fold to.
45
+ *
46
+ * Written as numeric code points on purpose. A table of raw lookalike glyphs is by
47
+ * construction unreadable in review — the entries are supposed to be
48
+ * indistinguishable from their targets — and one stray copy-paste produces exactly
49
+ * the silent duplicate the previous `HOMOGLYPH_MAP` carried on its `a` row.
50
+ *
51
+ * The `l` and `o` rows fold across case (`I`, `l`, `1` all become `l`; `O`, `o`, `0`
52
+ * all become `o`) because those glyph families are genuinely indistinguishable in
53
+ * most typefaces. Every other row preserves case.
54
+ *
55
+ * Rows are keyed by the letter a code point **renders as**, not by its Unicode general
56
+ * category, and the two disagree often enough to matter. A Greek, Cyrillic, Coptic or
57
+ * Roman-numeral capital whose glyph is the Latin capital belongs in the uppercase row:
58
+ * filing `Ρ` (U+03A1) under `p` folds it to `p` while `P` folds to `P`, so the spoof
59
+ * pair an identifier check exists to catch compares as *not* confusable. The reverse
60
+ * also happens — `Ƅ` (U+0184), `Ь` (U+042C), `Ꮟ` (U+13CF), `Ꮒ` (U+13C2), `Ꮷ` (U+13E7)
61
+ * and `Ꭹ` (U+13A9) carry general category `Lu` but draw as lowercase shapes, so they
62
+ * stay in the lowercase rows. An audit keyed on general category flags those six; they
63
+ * are correct as written.
64
+ */
65
+ const CONFUSABLES_BY_PROTOTYPE = {
66
+ a: [
67
+ 1072,
68
+ 945,
69
+ 593,
70
+ 594,
71
+ 7424,
72
+ 9082,
73
+ 43898
74
+ ],
75
+ b: [
76
+ 388,
77
+ 1068,
78
+ 1100,
79
+ 5071,
80
+ 5551,
81
+ 1074,
82
+ 7427,
83
+ 384
84
+ ],
85
+ c: [
86
+ 1089,
87
+ 1010,
88
+ 11429,
89
+ 7428,
90
+ 8573,
91
+ 392
92
+ ],
93
+ d: [
94
+ 1281,
95
+ 5095,
96
+ 5231,
97
+ 8518,
98
+ 8574,
99
+ 599,
100
+ 7429
101
+ ],
102
+ e: [
103
+ 1077,
104
+ 1213,
105
+ 8494,
106
+ 7431,
107
+ 43826,
108
+ 1108,
109
+ 8455
110
+ ],
111
+ f: [
112
+ 383,
113
+ 402,
114
+ 989,
115
+ 7837,
116
+ 42905
117
+ ],
118
+ g: [
119
+ 609,
120
+ 485,
121
+ 1409,
122
+ 5115,
123
+ 7555,
124
+ 8458
125
+ ],
126
+ h: [
127
+ 1211,
128
+ 1392,
129
+ 5058,
130
+ 8462,
131
+ 1163
132
+ ],
133
+ i: [
134
+ 1110,
135
+ 953,
136
+ 617,
137
+ 8560,
138
+ 8505,
139
+ 305,
140
+ 8126,
141
+ 9075
142
+ ],
143
+ j: [
144
+ 1112,
145
+ 1011,
146
+ 1397,
147
+ 8521
148
+ ],
149
+ k: [
150
+ 1082,
151
+ 954,
152
+ 7435,
153
+ 11413,
154
+ 312
155
+ ],
156
+ l: [
157
+ 73,
158
+ 49,
159
+ 124,
160
+ 1231,
161
+ 8467,
162
+ 8572,
163
+ 406,
164
+ 448,
165
+ 1472,
166
+ 8544,
167
+ 8556,
168
+ 8739,
169
+ 921,
170
+ 1030,
171
+ 8464,
172
+ 322
173
+ ],
174
+ m: [
175
+ 1084,
176
+ 8575,
177
+ 7437,
178
+ 653
179
+ ],
180
+ n: [
181
+ 1400,
182
+ 7438,
183
+ 627,
184
+ 1404
185
+ ],
186
+ o: [
187
+ 79,
188
+ 48,
189
+ 1086,
190
+ 1054,
191
+ 959,
192
+ 927,
193
+ 1413,
194
+ 1365,
195
+ 1637,
196
+ 1781,
197
+ 2406,
198
+ 2790,
199
+ 3046,
200
+ 3174,
201
+ 7439,
202
+ 11422,
203
+ 11423,
204
+ 2918,
205
+ 4125,
206
+ 7441
207
+ ],
208
+ p: [
209
+ 1088,
210
+ 961,
211
+ 9076,
212
+ 7448,
213
+ 11427
214
+ ],
215
+ q: [
216
+ 1307,
217
+ 1379,
218
+ 42839
219
+ ],
220
+ r: [
221
+ 1075,
222
+ 7462,
223
+ 11397,
224
+ 1107,
225
+ 638
226
+ ],
227
+ s: [
228
+ 1109,
229
+ 42801,
230
+ 445,
231
+ 642
232
+ ],
233
+ t: [
234
+ 1090,
235
+ 964,
236
+ 7451,
237
+ 355
238
+ ],
239
+ u: [
240
+ 965,
241
+ 1405,
242
+ 7452,
243
+ 1094,
244
+ 651
245
+ ],
246
+ v: [
247
+ 957,
248
+ 1141,
249
+ 8564,
250
+ 8744,
251
+ 7456
252
+ ],
253
+ w: [
254
+ 1309,
255
+ 1377,
256
+ 7457,
257
+ 1096,
258
+ 43907
259
+ ],
260
+ x: [
261
+ 1093,
262
+ 967,
263
+ 8569,
264
+ 5741,
265
+ 215
266
+ ],
267
+ y: [
268
+ 1091,
269
+ 947,
270
+ 655,
271
+ 1199,
272
+ 5033,
273
+ 7935,
274
+ 978
275
+ ],
276
+ z: [
277
+ 656,
278
+ 7458,
279
+ 42851,
280
+ 438
281
+ ],
282
+ A: [
283
+ 1040,
284
+ 913,
285
+ 5034,
286
+ 11373
287
+ ],
288
+ B: [
289
+ 1042,
290
+ 914,
291
+ 5108,
292
+ 5623,
293
+ 11394,
294
+ 8492
295
+ ],
296
+ C: [
297
+ 1057,
298
+ 1017,
299
+ 8557,
300
+ 5087,
301
+ 8450,
302
+ 8493,
303
+ 11428
304
+ ],
305
+ D: [
306
+ 5024,
307
+ 8558,
308
+ 5610,
309
+ 8517
310
+ ],
311
+ E: [
312
+ 1045,
313
+ 917,
314
+ 5036,
315
+ 11577,
316
+ 8496,
317
+ 1212
318
+ ],
319
+ F: [
320
+ 988,
321
+ 5556,
322
+ 42904,
323
+ 8497
324
+ ],
325
+ G: [
326
+ 1292,
327
+ 5056,
328
+ 42912,
329
+ 1293,
330
+ 11424
331
+ ],
332
+ H: [
333
+ 1053,
334
+ 919,
335
+ 5051,
336
+ 8459,
337
+ 11406,
338
+ 8460,
339
+ 8461
340
+ ],
341
+ J: [
342
+ 1032,
343
+ 5035,
344
+ 5261,
345
+ 7434
346
+ ],
347
+ K: [
348
+ 1050,
349
+ 922,
350
+ 8490,
351
+ 5094,
352
+ 11412
353
+ ],
354
+ L: [
355
+ 5086,
356
+ 7436,
357
+ 11472,
358
+ 8466,
359
+ 5290
360
+ ],
361
+ M: [
362
+ 1052,
363
+ 924,
364
+ 5047,
365
+ 11416,
366
+ 8499,
367
+ 8559
368
+ ],
369
+ N: [
370
+ 925,
371
+ 11418,
372
+ 8469,
373
+ 628
374
+ ],
375
+ P: [
376
+ 5090,
377
+ 8473,
378
+ 5229,
379
+ 1056,
380
+ 929,
381
+ 11426
382
+ ],
383
+ Q: [
384
+ 1306,
385
+ 11605,
386
+ 42838,
387
+ 8474,
388
+ 8506
389
+ ],
390
+ R: [
391
+ 5025,
392
+ 5074,
393
+ 7449,
394
+ 422,
395
+ 8475,
396
+ 8477
397
+ ],
398
+ S: [
399
+ 11425,
400
+ 536,
401
+ 1029,
402
+ 5082
403
+ ],
404
+ T: [
405
+ 1058,
406
+ 932,
407
+ 5026,
408
+ 8868,
409
+ 11430,
410
+ 1006
411
+ ],
412
+ U: [
413
+ 1357,
414
+ 8746,
415
+ 1359
416
+ ],
417
+ V: [
418
+ 8548,
419
+ 1140,
420
+ 5081
421
+ ],
422
+ W: [
423
+ 5043,
424
+ 1310,
425
+ 1308
426
+ ],
427
+ X: [
428
+ 9587,
429
+ 11437,
430
+ 1061,
431
+ 935,
432
+ 8553,
433
+ 11436
434
+ ],
435
+ Y: [
436
+ 1198,
437
+ 8511,
438
+ 1059,
439
+ 933,
440
+ 11432
441
+ ],
442
+ Z: [
443
+ 8484,
444
+ 5060,
445
+ 918,
446
+ 5059,
447
+ 11391
448
+ ],
449
+ "2": [
450
+ 423,
451
+ 1e3,
452
+ 42564,
453
+ 5311
454
+ ],
455
+ "3": [
456
+ 1047,
457
+ 1248,
458
+ 439,
459
+ 11468,
460
+ 1177
461
+ ],
462
+ "4": [5070, 5228],
463
+ "5": [444, 7549],
464
+ "6": [
465
+ 1073,
466
+ 5102,
467
+ 1041
468
+ ],
469
+ "7": [1216, 7548],
470
+ "8": [
471
+ 546,
472
+ 2819,
473
+ 2538
474
+ ],
475
+ "9": [
476
+ 1641,
477
+ 1785,
478
+ 5101,
479
+ 2407
480
+ ],
481
+ "-": [
482
+ 8208,
483
+ 8209,
484
+ 8210,
485
+ 8211,
486
+ 8212,
487
+ 8213,
488
+ 8722,
489
+ 8259,
490
+ 65112,
491
+ 1418,
492
+ 6150
493
+ ],
494
+ ".": [
495
+ 1632,
496
+ 1776,
497
+ 8228,
498
+ 42232,
499
+ 1793,
500
+ 1794
501
+ ],
502
+ "/": [
503
+ 8260,
504
+ 8725,
505
+ 9585,
506
+ 10744,
507
+ 5941,
508
+ 12339
509
+ ],
510
+ "\\": [
511
+ 8726,
512
+ 10741,
513
+ 9586,
514
+ 9290,
515
+ 65128
516
+ ],
517
+ "'": [
518
+ 8216,
519
+ 8217,
520
+ 697,
521
+ 700,
522
+ 712,
523
+ 1370,
524
+ 1523,
525
+ 8242
526
+ ],
527
+ "\"": [
528
+ 8220,
529
+ 8221,
530
+ 698,
531
+ 1524,
532
+ 8243
533
+ ],
534
+ ":": [
535
+ 1417,
536
+ 1475,
537
+ 8758,
538
+ 42889,
539
+ 65072,
540
+ 10626
541
+ ],
542
+ ";": [894, 65044],
543
+ "!": [
544
+ 451,
545
+ 11601,
546
+ 65045
547
+ ],
548
+ "?": [
549
+ 660,
550
+ 577,
551
+ 2429,
552
+ 5038
553
+ ],
554
+ "(": [10088, 9115],
555
+ ")": [10089, 9118],
556
+ "[": [10098, 12304],
557
+ "]": [10099, 12305],
558
+ "{": [10100],
559
+ "}": [10101],
560
+ "@": [3663],
561
+ "#": [11614],
562
+ $: [7839],
563
+ "%": [1642, 8274],
564
+ "&": [42872],
565
+ "*": [
566
+ 1645,
567
+ 8270,
568
+ 8727,
569
+ 66335
570
+ ],
571
+ "+": [
572
+ 5869,
573
+ 10133,
574
+ 10010
575
+ ],
576
+ ",": [
577
+ 1549,
578
+ 1643,
579
+ 8218,
580
+ 12289
581
+ ],
582
+ "<": [
583
+ 706,
584
+ 5176,
585
+ 8249,
586
+ 10094
587
+ ],
588
+ ">": [
589
+ 707,
590
+ 5171,
591
+ 8250,
592
+ 10095
593
+ ],
594
+ "=": [
595
+ 11840,
596
+ 12448,
597
+ 42239
598
+ ],
599
+ "~": [
600
+ 732,
601
+ 8128,
602
+ 8275,
603
+ 8764
604
+ ],
605
+ "^": [708, 710],
606
+ _: [717, 65343],
607
+ "`": [715],
608
+ "|": [
609
+ 10072,
610
+ 65512,
611
+ 2404
612
+ ]
613
+ };
614
+ /**
615
+ * Flattened lookup: confusable code point to its ASCII prototype.
616
+ *
617
+ * Later rows win on collision. That is intentional and harmless — a code point listed
618
+ * under two prototypes is confusable with both, so either answer yields the same
619
+ * collision behavior provided the choice is deterministic, which it is.
620
+ */
621
+ const CONFUSABLE_MAP = (() => {
622
+ const map = /* @__PURE__ */ new Map();
623
+ for (const [prototype, codePoints] of Object.entries(CONFUSABLES_BY_PROTOTYPE)) for (const codePoint of codePoints) map.set(codePoint, prototype);
624
+ return map;
625
+ })();
626
+ /** Fullwidth `U+FF01`–`U+FF5E` map to `!`–`~` by subtracting this offset. */
627
+ const FULLWIDTH_OFFSET = 65248;
628
+ /** Lowest fullwidth form that folds to ASCII. */
629
+ const FULLWIDTH_START = 65281;
630
+ /** Highest fullwidth form that folds to ASCII. */
631
+ const FULLWIDTH_END = 65374;
632
+ /**
633
+ * Contiguous ranges that fold to ASCII at a fixed offset from the range start.
634
+ *
635
+ * Each entry is `[rangeStart, rangeEnd, asciiStart]`. Handling these by computation
636
+ * rather than by table folds several thousand code points — the mathematical
637
+ * alphanumeric block alone is over 1 000 — without a single table row.
638
+ */
639
+ const ALGORITHMIC_RANGES = [
640
+ [
641
+ 119808,
642
+ 119833,
643
+ 65
644
+ ],
645
+ [
646
+ 119834,
647
+ 119859,
648
+ 97
649
+ ],
650
+ [
651
+ 119860,
652
+ 119885,
653
+ 65
654
+ ],
655
+ [
656
+ 119886,
657
+ 119911,
658
+ 97
659
+ ],
660
+ [
661
+ 119912,
662
+ 119937,
663
+ 65
664
+ ],
665
+ [
666
+ 119938,
667
+ 119963,
668
+ 97
669
+ ],
670
+ [
671
+ 119964,
672
+ 119989,
673
+ 65
674
+ ],
675
+ [
676
+ 119990,
677
+ 120015,
678
+ 97
679
+ ],
680
+ [
681
+ 120016,
682
+ 120041,
683
+ 65
684
+ ],
685
+ [
686
+ 120042,
687
+ 120067,
688
+ 97
689
+ ],
690
+ [
691
+ 120068,
692
+ 120093,
693
+ 65
694
+ ],
695
+ [
696
+ 120094,
697
+ 120119,
698
+ 97
699
+ ],
700
+ [
701
+ 120120,
702
+ 120145,
703
+ 65
704
+ ],
705
+ [
706
+ 120146,
707
+ 120171,
708
+ 97
709
+ ],
710
+ [
711
+ 120172,
712
+ 120197,
713
+ 65
714
+ ],
715
+ [
716
+ 120198,
717
+ 120223,
718
+ 97
719
+ ],
720
+ [
721
+ 120224,
722
+ 120249,
723
+ 65
724
+ ],
725
+ [
726
+ 120250,
727
+ 120275,
728
+ 97
729
+ ],
730
+ [
731
+ 120276,
732
+ 120301,
733
+ 65
734
+ ],
735
+ [
736
+ 120302,
737
+ 120327,
738
+ 97
739
+ ],
740
+ [
741
+ 120328,
742
+ 120353,
743
+ 65
744
+ ],
745
+ [
746
+ 120354,
747
+ 120379,
748
+ 97
749
+ ],
750
+ [
751
+ 120380,
752
+ 120405,
753
+ 65
754
+ ],
755
+ [
756
+ 120406,
757
+ 120431,
758
+ 97
759
+ ],
760
+ [
761
+ 120432,
762
+ 120457,
763
+ 65
764
+ ],
765
+ [
766
+ 120458,
767
+ 120483,
768
+ 97
769
+ ],
770
+ [
771
+ 120782,
772
+ 120791,
773
+ 48
774
+ ],
775
+ [
776
+ 120792,
777
+ 120801,
778
+ 48
779
+ ],
780
+ [
781
+ 120802,
782
+ 120811,
783
+ 48
784
+ ],
785
+ [
786
+ 120812,
787
+ 120821,
788
+ 48
789
+ ],
790
+ [
791
+ 120822,
792
+ 120831,
793
+ 48
794
+ ],
795
+ [
796
+ 9398,
797
+ 9423,
798
+ 65
799
+ ],
800
+ [
801
+ 9424,
802
+ 9449,
803
+ 97
804
+ ],
805
+ [
806
+ 9372,
807
+ 9397,
808
+ 97
809
+ ],
810
+ [
811
+ 127280,
812
+ 127305,
813
+ 65
814
+ ],
815
+ [
816
+ 127312,
817
+ 127337,
818
+ 65
819
+ ],
820
+ [
821
+ 127344,
822
+ 127369,
823
+ 65
824
+ ]
825
+ ];
826
+ /**
827
+ * {@link ALGORITHMIC_RANGES} as start-sorted parallel arrays, built once at module load.
828
+ *
829
+ * The literal above stays the source of truth because it is grouped for reading — maths
830
+ * alphanumerics, then digits, then enclosed forms — which leaves it unsorted, so it can
831
+ * only be searched linearly. Sorting a derived copy allows a binary search without
832
+ * reordering the table a reviewer reads.
833
+ */
834
+ const [RANGE_STARTS, RANGE_ENDS, RANGE_ASCII_STARTS] = (() => {
835
+ const sorted = [...ALGORITHMIC_RANGES].sort((a, b) => a[0] - b[0]);
836
+ return [
837
+ Int32Array.from(sorted, (range) => range[0]),
838
+ Int32Array.from(sorted, (range) => range[1]),
839
+ Int32Array.from(sorted, (range) => range[2])
840
+ ];
841
+ })();
842
+ /** Lowest code point covered by any algorithmic range. */
843
+ const MIN_RANGE_START = RANGE_STARTS[0];
844
+ /** Highest code point covered by any algorithmic range. */
845
+ const MAX_RANGE_END = RANGE_ENDS.reduce((max, end) => end > max ? end : max, 0);
846
+ /**
847
+ * Fold one code point to its ASCII prototype.
848
+ *
849
+ * @param codePoint - Code point to fold.
850
+ * @returns The ASCII prototype, or `null` when the code point is not confusable with
851
+ * anything in the ASCII range.
852
+ */
853
+ function foldCodePoint(codePoint) {
854
+ if (codePoint >= FULLWIDTH_START && codePoint <= FULLWIDTH_END) return String.fromCodePoint(codePoint - FULLWIDTH_OFFSET);
855
+ if (codePoint >= MIN_RANGE_START && codePoint <= MAX_RANGE_END) {
856
+ let low = 0;
857
+ let high = RANGE_STARTS.length - 1;
858
+ while (low <= high) {
859
+ const mid = low + high >> 1;
860
+ if (codePoint < RANGE_STARTS[mid]) high = mid - 1;
861
+ else if (codePoint > RANGE_ENDS[mid]) low = mid + 1;
862
+ else {
863
+ const offset = codePoint - RANGE_STARTS[mid];
864
+ return String.fromCodePoint(RANGE_ASCII_STARTS[mid] + offset);
865
+ }
866
+ }
867
+ }
868
+ return CONFUSABLE_MAP.get(codePoint) ?? null;
869
+ }
870
+ /** Combining marks, dropped during skeleton generation. */
871
+ const COMBINING_MARKS = /\p{M}/gu;
872
+ /** Default-ignorable and zero-width code points, which contribute nothing visually. */
873
+ const INVISIBLE_CODE_POINTS = /[\u00ad\u200b-\u200f\u2060-\u2064\ufeff\u202a-\u202e\u2066-\u2069]/g;
874
+ /**
875
+ * Generate a UTS #39-style confusable skeleton.
876
+ *
877
+ * Pipeline: decompose to NFD, drop invisible and combining code points, fold each
878
+ * remaining code point through the confusable tables, recompose to NFC.
879
+ *
880
+ * Two strings with equal skeletons are visually confusable. That is *all* an equal
881
+ * skeleton means — in particular it does not mean the strings are equivalent, and the
882
+ * skeleton is neither a display nor a storage form. Accents are dropped and the
883
+ * `I/l/1` and `O/o/0` families collapse across case, so `José`, `Jose`, and `J0sé`
884
+ * share a skeleton by design.
885
+ *
886
+ * @param input - Raw string. Non-string or empty input yields `""`.
887
+ * @returns The comparison key.
888
+ *
889
+ * @example
890
+ * ```ts
891
+ * // Cyrillic а in an otherwise Latin string.
892
+ * getSkeleton("pаypal") === getSkeleton("paypal"); // true
893
+ * getSkeleton("paypaI") === getSkeleton("paypal"); // true — I folds to l
894
+ * ```
895
+ */
896
+ function getSkeleton(input) {
897
+ if (!input || typeof input !== "string") return "";
898
+ const decomposed = input.normalize("NFD").replace(INVISIBLE_CODE_POINTS, "").replace(COMBINING_MARKS, "");
899
+ let skeleton = "";
900
+ for (const character of decomposed) {
901
+ const codePoint = character.codePointAt(0);
902
+ if (codePoint === void 0) continue;
903
+ skeleton += foldCodePoint(codePoint) ?? character;
904
+ }
905
+ return skeleton.normalize("NFC");
906
+ }
907
+ /**
908
+ * Map non-ASCII lookalike characters onto their ASCII prototypes, preserving
909
+ * everything else.
910
+ *
911
+ * Unlike {@link getSkeleton} this is a *conservative* transform: accents and other
912
+ * combining marks survive, ASCII characters are never rewritten, and the result stays
913
+ * readable. `Ολγα` keeps its Greek letters only insofar as they are not Latin
914
+ * lookalikes; `café` stays `café`.
915
+ *
916
+ * Even so, prefer {@link getSkeleton} for collision checks and keep the original for
917
+ * display. Rewriting a user's identifier into a different string is a lossy operation
918
+ * that this function can only make *look* safe.
919
+ *
920
+ * @param input - Raw string. Non-string or empty input yields `""`.
921
+ * @returns NFC-composed string with non-ASCII confusables folded to ASCII.
922
+ *
923
+ * @example
924
+ * ```ts
925
+ * foldConfusables("pаypal"); // "paypal" — Cyrillic а folded
926
+ * foldConfusables("café"); // "café" — accent preserved
927
+ * foldConfusables("HELLO"); // "HELLO" — ASCII untouched
928
+ * ```
929
+ */
930
+ function foldConfusables(input) {
931
+ if (!input || typeof input !== "string") return "";
932
+ let folded = "";
933
+ for (const character of input.normalize("NFC")) {
934
+ const codePoint = character.codePointAt(0);
935
+ if (codePoint === void 0) continue;
936
+ folded += codePoint <= 127 ? character : foldCodePoint(codePoint) ?? character;
937
+ }
938
+ return folded.normalize("NFC");
939
+ }
940
+ /**
941
+ * Test whether two distinct strings are visually confusable.
942
+ *
943
+ * @param left - First string.
944
+ * @param right - Second string.
945
+ * @returns `true` when the strings differ but their skeletons match.
946
+ */
947
+ function areConfusable(left, right) {
948
+ if (left === right) return false;
949
+ return getSkeleton(left) === getSkeleton(right);
950
+ }
951
+ //#endregion
952
+ export { CONFUSABLE_MAP, areConfusable, foldConfusables, getSkeleton };
953
+
954
+ //# sourceMappingURL=confusables.mjs.map