namespace-guard 0.7.0 → 0.8.2
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 +37 -4
- package/dist/adapters/raw.d.mts +0 -26
- package/dist/adapters/raw.d.ts +0 -26
- package/dist/adapters/raw.js +10 -0
- package/dist/adapters/raw.mjs +10 -0
- package/dist/cli.js +21 -9
- package/dist/cli.mjs +21 -9
- package/dist/index.d.mts +13 -6
- package/dist/index.d.ts +13 -6
- package/dist/index.js +677 -37
- package/dist/index.mjs +677 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -299,7 +299,7 @@ No words are bundled — use any word list you like (e.g., the `bad-words` npm p
|
|
|
299
299
|
|
|
300
300
|
### Built-in Homoglyph Validator
|
|
301
301
|
|
|
302
|
-
Prevent spoofing attacks where
|
|
302
|
+
Prevent spoofing attacks where visually similar characters from any Unicode script are substituted for Latin letters (e.g., Cyrillic "а" for Latin "a" in "admin"):
|
|
303
303
|
|
|
304
304
|
```typescript
|
|
305
305
|
import { createNamespaceGuard, createHomoglyphValidator } from "namespace-guard";
|
|
@@ -318,11 +318,43 @@ Options:
|
|
|
318
318
|
createHomoglyphValidator({
|
|
319
319
|
message: "Custom rejection message.", // optional
|
|
320
320
|
additionalMappings: { "\u0261": "g" }, // extend the built-in map
|
|
321
|
-
rejectMixedScript: true, // also reject Latin +
|
|
321
|
+
rejectMixedScript: true, // also reject Latin + non-Latin script mixing
|
|
322
322
|
})
|
|
323
323
|
```
|
|
324
324
|
|
|
325
|
-
The built-in `CONFUSABLE_MAP`
|
|
325
|
+
The built-in `CONFUSABLE_MAP` contains 613 character pairs generated from [Unicode TR39 confusables.txt](https://unicode.org/reports/tr39/) plus supplemental Latin small capitals. It covers Cyrillic, Greek, Armenian, Cherokee, IPA, Coptic, Lisu, Canadian Syllabics, Georgian, and 20+ other scripts. The map is exported for inspection or extension, and is regenerable for new Unicode versions with `npx tsx scripts/generate-confusables.ts`.
|
|
326
|
+
|
|
327
|
+
### How the anti-spoofing pipeline works
|
|
328
|
+
|
|
329
|
+
Most confusable-detection libraries apply a character map in isolation. namespace-guard uses a three-stage pipeline where each stage is aware of the others:
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
Input → NFKC normalize → Confusable map → Mixed-script reject
|
|
333
|
+
(stage 1) (stage 2) (stage 3)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Stage 1: NFKC normalization** collapses full-width characters (`I` → `I`), ligatures (`fi` → `fi`), superscripts, and other Unicode compatibility forms to their canonical equivalents. This runs first, before any confusable check.
|
|
337
|
+
|
|
338
|
+
**Stage 2: Confusable map** catches characters that survive NFKC but visually mimic Latin letters — Cyrillic `а` for `a`, Greek `ο` for `o`, Cherokee `Ꭺ` for `A`, and 600+ others from the Unicode Consortium's [confusables.txt](https://unicode.org/Public/security/latest/confusables.txt).
|
|
339
|
+
|
|
340
|
+
**Stage 3: Mixed-script rejection** (`rejectMixedScript: true`) blocks identifiers that mix Latin with non-Latin scripts (Hebrew, Arabic, Devanagari, Thai, Georgian, Ethiopic, etc.) even if the specific characters aren't in the confusable map. This catches novel homoglyphs that the map doesn't cover.
|
|
341
|
+
|
|
342
|
+
#### Why NFKC-aware filtering matters
|
|
343
|
+
|
|
344
|
+
The key insight: TR39's confusables.txt and NFKC normalization sometimes disagree. For example, Unicode says capital `I` (U+0049) is confusable with lowercase `l` — visually true in many fonts. But NFKC maps Mathematical Bold `𝐈` (U+1D408) to `I`, not `l`. If you naively ship the TR39 mapping (`𝐈` → `l`), the confusable check will never see that character — NFKC already converted it to `I` in stage 1.
|
|
345
|
+
|
|
346
|
+
We found 31 entries where this happens:
|
|
347
|
+
|
|
348
|
+
| Character | TR39 says | NFKC says | Winner |
|
|
349
|
+
|-----------|-----------|-----------|--------|
|
|
350
|
+
| `ſ` Long S (U+017F) | `f` | `s` | NFKC (`s` is correct) |
|
|
351
|
+
| `Ⅰ` Roman Numeral I (U+2160) | `l` | `i` | NFKC (`i` is correct) |
|
|
352
|
+
| `I` Fullwidth I (U+FF29) | `l` | `i` | NFKC (`i` is correct) |
|
|
353
|
+
| `𝟎` Math Bold 0 (U+1D7CE) | `o` | `0` | NFKC (`0` is correct) |
|
|
354
|
+
| 11 Mathematical I variants | `l` | `i` | NFKC |
|
|
355
|
+
| 12 Mathematical 0/1 variants | `o`/`l` | `0`/`1` | NFKC |
|
|
356
|
+
|
|
357
|
+
These entries are dead code in any pipeline that runs NFKC first — and worse, they encode the *wrong* mapping. The generate script (`scripts/generate-confusables.ts`) automatically detects and excludes them.
|
|
326
358
|
|
|
327
359
|
## Unicode Normalization
|
|
328
360
|
|
|
@@ -616,7 +648,8 @@ Enable in-memory caching to reduce database calls during rapid checks (e.g., liv
|
|
|
616
648
|
const guard = createNamespaceGuard({
|
|
617
649
|
sources: [/* ... */],
|
|
618
650
|
cache: {
|
|
619
|
-
ttl: 5000,
|
|
651
|
+
ttl: 5000, // milliseconds (default: 5000)
|
|
652
|
+
maxSize: 1000, // max cached entries before LRU eviction (default: 1000)
|
|
620
653
|
},
|
|
621
654
|
}, adapter);
|
|
622
655
|
|
package/dist/adapters/raw.d.mts
CHANGED
|
@@ -3,32 +3,6 @@ import { NamespaceAdapter } from '../index.mjs';
|
|
|
3
3
|
type QueryExecutor = (sql: string, params: unknown[]) => Promise<{
|
|
4
4
|
rows: Record<string, unknown>[];
|
|
5
5
|
}>;
|
|
6
|
-
/**
|
|
7
|
-
* Create a namespace adapter for raw SQL queries
|
|
8
|
-
*
|
|
9
|
-
* Works with PostgreSQL-compatible clients that use $1-style parameter placeholders (pg).
|
|
10
|
-
* For MySQL or SQLite, wrap the executor to translate parameter syntax.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* import { Pool } from "pg";
|
|
15
|
-
* import { createNamespaceGuard } from "namespace-guard";
|
|
16
|
-
* import { createRawAdapter } from "namespace-guard/adapters/raw";
|
|
17
|
-
*
|
|
18
|
-
* const pool = new Pool();
|
|
19
|
-
*
|
|
20
|
-
* const guard = createNamespaceGuard(
|
|
21
|
-
* {
|
|
22
|
-
* reserved: ["admin", "api", "settings"],
|
|
23
|
-
* sources: [
|
|
24
|
-
* { name: "users", column: "handle", scopeKey: "id" },
|
|
25
|
-
* { name: "organizations", column: "slug", scopeKey: "id" },
|
|
26
|
-
* ],
|
|
27
|
-
* },
|
|
28
|
-
* createRawAdapter((sql, params) => pool.query(sql, params))
|
|
29
|
-
* );
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
6
|
declare function createRawAdapter(execute: QueryExecutor): NamespaceAdapter;
|
|
33
7
|
|
|
34
8
|
export { createRawAdapter };
|
package/dist/adapters/raw.d.ts
CHANGED
|
@@ -3,32 +3,6 @@ import { NamespaceAdapter } from '../index.js';
|
|
|
3
3
|
type QueryExecutor = (sql: string, params: unknown[]) => Promise<{
|
|
4
4
|
rows: Record<string, unknown>[];
|
|
5
5
|
}>;
|
|
6
|
-
/**
|
|
7
|
-
* Create a namespace adapter for raw SQL queries
|
|
8
|
-
*
|
|
9
|
-
* Works with PostgreSQL-compatible clients that use $1-style parameter placeholders (pg).
|
|
10
|
-
* For MySQL or SQLite, wrap the executor to translate parameter syntax.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* import { Pool } from "pg";
|
|
15
|
-
* import { createNamespaceGuard } from "namespace-guard";
|
|
16
|
-
* import { createRawAdapter } from "namespace-guard/adapters/raw";
|
|
17
|
-
*
|
|
18
|
-
* const pool = new Pool();
|
|
19
|
-
*
|
|
20
|
-
* const guard = createNamespaceGuard(
|
|
21
|
-
* {
|
|
22
|
-
* reserved: ["admin", "api", "settings"],
|
|
23
|
-
* sources: [
|
|
24
|
-
* { name: "users", column: "handle", scopeKey: "id" },
|
|
25
|
-
* { name: "organizations", column: "slug", scopeKey: "id" },
|
|
26
|
-
* ],
|
|
27
|
-
* },
|
|
28
|
-
* createRawAdapter((sql, params) => pool.query(sql, params))
|
|
29
|
-
* );
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
6
|
declare function createRawAdapter(execute: QueryExecutor): NamespaceAdapter;
|
|
33
7
|
|
|
34
8
|
export { createRawAdapter };
|
package/dist/adapters/raw.js
CHANGED
|
@@ -23,10 +23,20 @@ __export(raw_exports, {
|
|
|
23
23
|
createRawAdapter: () => createRawAdapter
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(raw_exports);
|
|
26
|
+
var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
27
|
+
function assertSafeIdentifier(name, label) {
|
|
28
|
+
if (!SAFE_IDENTIFIER.test(name)) {
|
|
29
|
+
throw new Error(`Unsafe ${label}: ${JSON.stringify(name)}. Use only letters, digits, and underscores.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
26
32
|
function createRawAdapter(execute) {
|
|
27
33
|
return {
|
|
28
34
|
async findOne(source, value, options) {
|
|
29
35
|
const idColumn = source.idColumn ?? "id";
|
|
36
|
+
assertSafeIdentifier(source.name, "table name");
|
|
37
|
+
assertSafeIdentifier(source.column, "column name");
|
|
38
|
+
assertSafeIdentifier(idColumn, "id column name");
|
|
39
|
+
if (source.scopeKey) assertSafeIdentifier(source.scopeKey, "scope key");
|
|
30
40
|
const columns = source.scopeKey && source.scopeKey !== idColumn ? `"${idColumn}", "${source.scopeKey}"` : `"${idColumn}"`;
|
|
31
41
|
const whereClause = options?.caseInsensitive ? `LOWER("${source.column}") = LOWER($1)` : `"${source.column}" = $1`;
|
|
32
42
|
const sql = `SELECT ${columns} FROM "${source.name}" WHERE ${whereClause} LIMIT 1`;
|
package/dist/adapters/raw.mjs
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
// src/adapters/raw.ts
|
|
2
|
+
var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3
|
+
function assertSafeIdentifier(name, label) {
|
|
4
|
+
if (!SAFE_IDENTIFIER.test(name)) {
|
|
5
|
+
throw new Error(`Unsafe ${label}: ${JSON.stringify(name)}. Use only letters, digits, and underscores.`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
2
8
|
function createRawAdapter(execute) {
|
|
3
9
|
return {
|
|
4
10
|
async findOne(source, value, options) {
|
|
5
11
|
const idColumn = source.idColumn ?? "id";
|
|
12
|
+
assertSafeIdentifier(source.name, "table name");
|
|
13
|
+
assertSafeIdentifier(source.column, "column name");
|
|
14
|
+
assertSafeIdentifier(idColumn, "id column name");
|
|
15
|
+
if (source.scopeKey) assertSafeIdentifier(source.scopeKey, "scope key");
|
|
6
16
|
const columns = source.scopeKey && source.scopeKey !== idColumn ? `"${idColumn}", "${source.scopeKey}"` : `"${idColumn}"`;
|
|
7
17
|
const whereClause = options?.caseInsensitive ? `LOWER("${source.column}") = LOWER($1)` : `"${source.column}" = $1`;
|
|
8
18
|
const sql = `SELECT ${columns} FROM "${source.name}" WHERE ${whereClause} LIMIT 1`;
|
package/dist/cli.js
CHANGED
|
@@ -144,14 +144,12 @@ function createScrambleStrategy(_pattern) {
|
|
|
144
144
|
const candidates = [];
|
|
145
145
|
const chars = identifier.split("");
|
|
146
146
|
for (let i = 0; i < chars.length - 1; i++) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
candidates.push(candidate);
|
|
154
|
-
}
|
|
147
|
+
const swapped = [...chars];
|
|
148
|
+
[swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
|
|
149
|
+
const candidate = swapped.join("");
|
|
150
|
+
if (candidate !== identifier && !seen.has(candidate)) {
|
|
151
|
+
seen.add(candidate);
|
|
152
|
+
candidates.push(candidate);
|
|
155
153
|
}
|
|
156
154
|
}
|
|
157
155
|
return candidates;
|
|
@@ -298,7 +296,7 @@ function createNamespaceGuard(config, adapter) {
|
|
|
298
296
|
const purelyNumericMsg = configMessages.purelyNumeric ?? "Identifiers cannot be purely numeric.";
|
|
299
297
|
const cacheEnabled = !!config.cache;
|
|
300
298
|
const cacheTtl = config.cache?.ttl ?? 5e3;
|
|
301
|
-
const cacheMaxSize = 1e3;
|
|
299
|
+
const cacheMaxSize = config.cache?.maxSize ?? 1e3;
|
|
302
300
|
const cacheMap = /* @__PURE__ */ new Map();
|
|
303
301
|
let cacheHits = 0;
|
|
304
302
|
let cacheMisses = 0;
|
|
@@ -488,10 +486,20 @@ function createNamespaceGuard(config, adapter) {
|
|
|
488
486
|
}
|
|
489
487
|
|
|
490
488
|
// src/adapters/raw.ts
|
|
489
|
+
var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
490
|
+
function assertSafeIdentifier(name, label) {
|
|
491
|
+
if (!SAFE_IDENTIFIER.test(name)) {
|
|
492
|
+
throw new Error(`Unsafe ${label}: ${JSON.stringify(name)}. Use only letters, digits, and underscores.`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
491
495
|
function createRawAdapter(execute) {
|
|
492
496
|
return {
|
|
493
497
|
async findOne(source, value, options) {
|
|
494
498
|
const idColumn = source.idColumn ?? "id";
|
|
499
|
+
assertSafeIdentifier(source.name, "table name");
|
|
500
|
+
assertSafeIdentifier(source.column, "column name");
|
|
501
|
+
assertSafeIdentifier(idColumn, "id column name");
|
|
502
|
+
if (source.scopeKey) assertSafeIdentifier(source.scopeKey, "scope key");
|
|
495
503
|
const columns = source.scopeKey && source.scopeKey !== idColumn ? `"${idColumn}", "${source.scopeKey}"` : `"${idColumn}"`;
|
|
496
504
|
const whereClause = options?.caseInsensitive ? `LOWER("${source.column}") = LOWER($1)` : `"${source.column}" = $1`;
|
|
497
505
|
const sql = `SELECT ${columns} FROM "${source.name}" WHERE ${whereClause} LIMIT 1`;
|
|
@@ -574,6 +582,10 @@ async function createDatabaseAdapter(url) {
|
|
|
574
582
|
process.exit(1);
|
|
575
583
|
}
|
|
576
584
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
585
|
+
if (!Pool) {
|
|
586
|
+
console.error("Could not find Pool export from pg package. Check your pg version.");
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
577
589
|
const pool = new Pool({ connectionString: url });
|
|
578
590
|
return {
|
|
579
591
|
adapter: createRawAdapter((sql, params) => pool.query(sql, params)),
|
package/dist/cli.mjs
CHANGED
|
@@ -121,14 +121,12 @@ function createScrambleStrategy(_pattern) {
|
|
|
121
121
|
const candidates = [];
|
|
122
122
|
const chars = identifier.split("");
|
|
123
123
|
for (let i = 0; i < chars.length - 1; i++) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
candidates.push(candidate);
|
|
131
|
-
}
|
|
124
|
+
const swapped = [...chars];
|
|
125
|
+
[swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
|
|
126
|
+
const candidate = swapped.join("");
|
|
127
|
+
if (candidate !== identifier && !seen.has(candidate)) {
|
|
128
|
+
seen.add(candidate);
|
|
129
|
+
candidates.push(candidate);
|
|
132
130
|
}
|
|
133
131
|
}
|
|
134
132
|
return candidates;
|
|
@@ -275,7 +273,7 @@ function createNamespaceGuard(config, adapter) {
|
|
|
275
273
|
const purelyNumericMsg = configMessages.purelyNumeric ?? "Identifiers cannot be purely numeric.";
|
|
276
274
|
const cacheEnabled = !!config.cache;
|
|
277
275
|
const cacheTtl = config.cache?.ttl ?? 5e3;
|
|
278
|
-
const cacheMaxSize = 1e3;
|
|
276
|
+
const cacheMaxSize = config.cache?.maxSize ?? 1e3;
|
|
279
277
|
const cacheMap = /* @__PURE__ */ new Map();
|
|
280
278
|
let cacheHits = 0;
|
|
281
279
|
let cacheMisses = 0;
|
|
@@ -465,10 +463,20 @@ function createNamespaceGuard(config, adapter) {
|
|
|
465
463
|
}
|
|
466
464
|
|
|
467
465
|
// src/adapters/raw.ts
|
|
466
|
+
var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
467
|
+
function assertSafeIdentifier(name, label) {
|
|
468
|
+
if (!SAFE_IDENTIFIER.test(name)) {
|
|
469
|
+
throw new Error(`Unsafe ${label}: ${JSON.stringify(name)}. Use only letters, digits, and underscores.`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
468
472
|
function createRawAdapter(execute) {
|
|
469
473
|
return {
|
|
470
474
|
async findOne(source, value, options) {
|
|
471
475
|
const idColumn = source.idColumn ?? "id";
|
|
476
|
+
assertSafeIdentifier(source.name, "table name");
|
|
477
|
+
assertSafeIdentifier(source.column, "column name");
|
|
478
|
+
assertSafeIdentifier(idColumn, "id column name");
|
|
479
|
+
if (source.scopeKey) assertSafeIdentifier(source.scopeKey, "scope key");
|
|
472
480
|
const columns = source.scopeKey && source.scopeKey !== idColumn ? `"${idColumn}", "${source.scopeKey}"` : `"${idColumn}"`;
|
|
473
481
|
const whereClause = options?.caseInsensitive ? `LOWER("${source.column}") = LOWER($1)` : `"${source.column}" = $1`;
|
|
474
482
|
const sql = `SELECT ${columns} FROM "${source.name}" WHERE ${whereClause} LIMIT 1`;
|
|
@@ -551,6 +559,10 @@ async function createDatabaseAdapter(url) {
|
|
|
551
559
|
process.exit(1);
|
|
552
560
|
}
|
|
553
561
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
562
|
+
if (!Pool) {
|
|
563
|
+
console.error("Could not find Pool export from pg package. Check your pg version.");
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
554
566
|
const pool = new Pool({ connectionString: url });
|
|
555
567
|
return {
|
|
556
568
|
adapter: createRawAdapter((sql, params) => pool.query(sql, params)),
|
package/dist/index.d.mts
CHANGED
|
@@ -53,6 +53,8 @@ type NamespaceConfig = {
|
|
|
53
53
|
cache?: {
|
|
54
54
|
/** Time-to-live in milliseconds (default: 5000) */
|
|
55
55
|
ttl?: number;
|
|
56
|
+
/** Maximum number of cached entries before LRU eviction (default: 1000) */
|
|
57
|
+
maxSize?: number;
|
|
56
58
|
};
|
|
57
59
|
};
|
|
58
60
|
/** Options passed to adapter `findOne` calls. */
|
|
@@ -136,22 +138,27 @@ declare function createProfanityValidator(words: string[], options?: {
|
|
|
136
138
|
message: string;
|
|
137
139
|
} | null>;
|
|
138
140
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
141
|
+
* Mapping of visually confusable Unicode characters to their Latin/digit equivalents.
|
|
142
|
+
* Generated from Unicode TR39 confusables.txt + supplemental Latin small capitals.
|
|
143
|
+
* Covers every single-character mapping to a lowercase Latin letter or digit,
|
|
144
|
+
* excluding characters already handled by NFKC normalization (either collapsed
|
|
145
|
+
* to the same target, or mapped to a different valid Latin char/digit).
|
|
146
|
+
* Regenerate: `npx tsx scripts/generate-confusables.ts`
|
|
142
147
|
*/
|
|
143
148
|
declare const CONFUSABLE_MAP: Record<string, string>;
|
|
144
149
|
/**
|
|
145
150
|
* Create a validator that rejects identifiers containing homoglyph/confusable characters.
|
|
146
151
|
*
|
|
147
|
-
* Catches spoofing attacks where
|
|
152
|
+
* Catches spoofing attacks where characters from other scripts are substituted for
|
|
148
153
|
* visually identical Latin characters (e.g., Cyrillic "а" for Latin "a" in "admin").
|
|
149
|
-
* Uses a
|
|
154
|
+
* Uses a comprehensive mapping of 613 character pairs generated from Unicode TR39
|
|
155
|
+
* confusables.txt, covering Cyrillic, Greek, Armenian, Cherokee, IPA, Latin small
|
|
156
|
+
* capitals, Canadian Syllabics, Georgian, Lisu, Coptic, and many other scripts.
|
|
150
157
|
*
|
|
151
158
|
* @param options - Optional settings
|
|
152
159
|
* @param options.message - Custom rejection message (default: "That name contains characters that could be confused with other letters.")
|
|
153
160
|
* @param options.additionalMappings - Extra confusable pairs to merge with the built-in map
|
|
154
|
-
* @param options.rejectMixedScript - Also reject identifiers that mix Latin with Cyrillic
|
|
161
|
+
* @param options.rejectMixedScript - Also reject identifiers that mix Latin with non-Latin characters from any covered script (Cyrillic, Greek, Armenian, Hebrew, Arabic, Georgian, Cherokee, Canadian Syllabics, Ethiopic, Coptic, Lisu, and more) (default: false)
|
|
155
162
|
* @returns An async validator function for use in `config.validators`
|
|
156
163
|
*
|
|
157
164
|
* @example
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ type NamespaceConfig = {
|
|
|
53
53
|
cache?: {
|
|
54
54
|
/** Time-to-live in milliseconds (default: 5000) */
|
|
55
55
|
ttl?: number;
|
|
56
|
+
/** Maximum number of cached entries before LRU eviction (default: 1000) */
|
|
57
|
+
maxSize?: number;
|
|
56
58
|
};
|
|
57
59
|
};
|
|
58
60
|
/** Options passed to adapter `findOne` calls. */
|
|
@@ -136,22 +138,27 @@ declare function createProfanityValidator(words: string[], options?: {
|
|
|
136
138
|
message: string;
|
|
137
139
|
} | null>;
|
|
138
140
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
141
|
+
* Mapping of visually confusable Unicode characters to their Latin/digit equivalents.
|
|
142
|
+
* Generated from Unicode TR39 confusables.txt + supplemental Latin small capitals.
|
|
143
|
+
* Covers every single-character mapping to a lowercase Latin letter or digit,
|
|
144
|
+
* excluding characters already handled by NFKC normalization (either collapsed
|
|
145
|
+
* to the same target, or mapped to a different valid Latin char/digit).
|
|
146
|
+
* Regenerate: `npx tsx scripts/generate-confusables.ts`
|
|
142
147
|
*/
|
|
143
148
|
declare const CONFUSABLE_MAP: Record<string, string>;
|
|
144
149
|
/**
|
|
145
150
|
* Create a validator that rejects identifiers containing homoglyph/confusable characters.
|
|
146
151
|
*
|
|
147
|
-
* Catches spoofing attacks where
|
|
152
|
+
* Catches spoofing attacks where characters from other scripts are substituted for
|
|
148
153
|
* visually identical Latin characters (e.g., Cyrillic "а" for Latin "a" in "admin").
|
|
149
|
-
* Uses a
|
|
154
|
+
* Uses a comprehensive mapping of 613 character pairs generated from Unicode TR39
|
|
155
|
+
* confusables.txt, covering Cyrillic, Greek, Armenian, Cherokee, IPA, Latin small
|
|
156
|
+
* capitals, Canadian Syllabics, Georgian, Lisu, Coptic, and many other scripts.
|
|
150
157
|
*
|
|
151
158
|
* @param options - Optional settings
|
|
152
159
|
* @param options.message - Custom rejection message (default: "That name contains characters that could be confused with other letters.")
|
|
153
160
|
* @param options.additionalMappings - Extra confusable pairs to merge with the built-in map
|
|
154
|
-
* @param options.rejectMixedScript - Also reject identifiers that mix Latin with Cyrillic
|
|
161
|
+
* @param options.rejectMixedScript - Also reject identifiers that mix Latin with non-Latin characters from any covered script (Cyrillic, Greek, Armenian, Hebrew, Arabic, Georgian, Cherokee, Canadian Syllabics, Ethiopic, Coptic, Lisu, and more) (default: false)
|
|
155
162
|
* @returns An async validator function for use in `config.validators`
|
|
156
163
|
*
|
|
157
164
|
* @example
|