@resq-systems/security 1.0.5 → 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.
- package/README.md +236 -33
- package/lib/controls/address.d.mts +142 -0
- package/lib/controls/address.d.mts.map +1 -0
- package/lib/controls/address.mjs +533 -0
- package/lib/controls/address.mjs.map +1 -0
- package/lib/controls/csrf.d.mts +91 -0
- package/lib/controls/csrf.d.mts.map +1 -0
- package/lib/controls/csrf.mjs +200 -0
- package/lib/controls/csrf.mjs.map +1 -0
- package/lib/controls/index.d.mts +8 -0
- package/lib/controls/index.mjs +8 -0
- package/lib/controls/origin.d.mts +95 -0
- package/lib/controls/origin.d.mts.map +1 -0
- package/lib/controls/origin.mjs +156 -0
- package/lib/controls/origin.mjs.map +1 -0
- package/lib/controls/payload.d.mts +84 -0
- package/lib/controls/payload.d.mts.map +1 -0
- package/lib/controls/payload.mjs +147 -0
- package/lib/controls/payload.mjs.map +1 -0
- package/lib/controls/query.d.mts +157 -0
- package/lib/controls/query.d.mts.map +1 -0
- package/lib/controls/query.mjs +368 -0
- package/lib/controls/query.mjs.map +1 -0
- package/lib/controls/redirect.d.mts +92 -0
- package/lib/controls/redirect.d.mts.map +1 -0
- package/lib/controls/redirect.mjs +110 -0
- package/lib/controls/redirect.mjs.map +1 -0
- package/lib/controls/upload.d.mts +108 -0
- package/lib/controls/upload.d.mts.map +1 -0
- package/lib/controls/upload.mjs +374 -0
- package/lib/controls/upload.mjs.map +1 -0
- package/lib/crypto.d.mts +18 -5
- package/lib/crypto.d.mts.map +1 -1
- package/lib/crypto.mjs +35 -24
- package/lib/crypto.mjs.map +1 -1
- package/lib/hash.d.mts +51 -6
- package/lib/hash.d.mts.map +1 -1
- package/lib/hash.mjs +51 -6
- package/lib/hash.mjs.map +1 -1
- package/lib/index.d.mts +17 -2
- package/lib/index.mjs +19 -2
- package/lib/paths.d.mts +92 -0
- package/lib/paths.d.mts.map +1 -0
- package/lib/paths.mjs +140 -0
- package/lib/paths.mjs.map +1 -0
- package/lib/sanitize.d.mts +137 -35
- package/lib/sanitize.d.mts.map +1 -1
- package/lib/sanitize.mjs +170 -46
- package/lib/sanitize.mjs.map +1 -1
- package/lib/threats/capec.generated.d.mts +59 -0
- package/lib/threats/capec.generated.d.mts.map +1 -0
- package/lib/threats/capec.generated.mjs +644 -0
- package/lib/threats/capec.generated.mjs.map +1 -0
- package/lib/threats/engine.d.mts +94 -0
- package/lib/threats/engine.d.mts.map +1 -0
- package/lib/threats/engine.mjs +167 -0
- package/lib/threats/engine.mjs.map +1 -0
- package/lib/threats/index.d.mts +11 -0
- package/lib/threats/index.mjs +11 -0
- package/lib/threats/rules/datastore.d.mts +13 -0
- package/lib/threats/rules/datastore.d.mts.map +1 -0
- package/lib/threats/rules/datastore.mjs +366 -0
- package/lib/threats/rules/datastore.mjs.map +1 -0
- package/lib/threats/rules/index.d.mts +54 -0
- package/lib/threats/rules/index.d.mts.map +1 -0
- package/lib/threats/rules/index.mjs +121 -0
- package/lib/threats/rules/index.mjs.map +1 -0
- package/lib/threats/rules/markup.d.mts +28 -0
- package/lib/threats/rules/markup.d.mts.map +1 -0
- package/lib/threats/rules/markup.mjs +373 -0
- package/lib/threats/rules/markup.mjs.map +1 -0
- package/lib/threats/rules/protocol.d.mts +49 -0
- package/lib/threats/rules/protocol.d.mts.map +1 -0
- package/lib/threats/rules/protocol.mjs +175 -0
- package/lib/threats/rules/protocol.mjs.map +1 -0
- package/lib/threats/rules/system.d.mts +19 -0
- package/lib/threats/rules/system.d.mts.map +1 -0
- package/lib/threats/rules/system.mjs +455 -0
- package/lib/threats/rules/system.mjs.map +1 -0
- package/lib/threats/rules/web.d.mts +26 -0
- package/lib/threats/rules/web.d.mts.map +1 -0
- package/lib/threats/rules/web.mjs +412 -0
- package/lib/threats/rules/web.mjs.map +1 -0
- package/lib/threats/scoring.d.mts +59 -0
- package/lib/threats/scoring.d.mts.map +1 -0
- package/lib/threats/scoring.mjs +111 -0
- package/lib/threats/scoring.mjs.map +1 -0
- package/lib/threats/types.d.mts +245 -0
- package/lib/threats/types.d.mts.map +1 -0
- package/lib/threats/types.mjs +52 -0
- package/lib/threats/types.mjs.map +1 -0
- package/lib/threats/variants.d.mts +57 -0
- package/lib/threats/variants.d.mts.map +1 -0
- package/lib/threats/variants.mjs +144 -0
- package/lib/threats/variants.mjs.map +1 -0
- package/lib/unicode/confusables.d.mts +82 -0
- package/lib/unicode/confusables.d.mts.map +1 -0
- package/lib/unicode/confusables.mjs +954 -0
- package/lib/unicode/confusables.mjs.map +1 -0
- package/lib/unicode/index.d.mts +126 -0
- package/lib/unicode/index.d.mts.map +1 -0
- package/lib/unicode/index.mjs +288 -0
- package/lib/unicode/index.mjs.map +1 -0
- package/lib/validators.d.mts +341 -164
- package/lib/validators.d.mts.map +1 -1
- package/lib/validators.mjs +519 -338
- package/lib/validators.mjs.map +1 -1
- 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
|