@tnid/filter 0.1.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/LICENSE +21 -0
- package/README.md +124 -0
- package/esm/blocklist.d.ts +41 -0
- package/esm/blocklist.d.ts.map +1 -0
- package/esm/blocklist.js +68 -0
- package/esm/filter.d.ts +51 -0
- package/esm/filter.d.ts.map +1 -0
- package/esm/filter.js +84 -0
- package/esm/filter_encryption.d.ts +52 -0
- package/esm/filter_encryption.d.ts.map +1 -0
- package/esm/filter_encryption.js +74 -0
- package/esm/index.d.ts +25 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +24 -0
- package/esm/internals.d.ts +35 -0
- package/esm/internals.d.ts.map +1 -0
- package/esm/internals.js +61 -0
- package/esm/package.json +3 -0
- package/package.json +46 -0
- package/script/blocklist.d.ts +41 -0
- package/script/blocklist.d.ts.map +1 -0
- package/script/blocklist.js +72 -0
- package/script/filter.d.ts +51 -0
- package/script/filter.d.ts.map +1 -0
- package/script/filter.js +90 -0
- package/script/filter_encryption.d.ts +52 -0
- package/script/filter_encryption.d.ts.map +1 -0
- package/script/filter_encryption.js +79 -0
- package/script/index.d.ts +25 -0
- package/script/index.d.ts.map +1 -0
- package/script/index.js +31 -0
- package/script/internals.d.ts +35 -0
- package/script/internals.d.ts.map +1 -0
- package/script/internals.js +71 -0
- package/script/package.json +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Edlinger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# @tnid/filter
|
|
2
|
+
|
|
3
|
+
Generate TNIDs that don't contain blocklisted substrings (e.g., profanity).
|
|
4
|
+
|
|
5
|
+
## Why Filter TNIDs?
|
|
6
|
+
|
|
7
|
+
The 17-character data portion of a TNID string uses an alphabet that includes letters capable of forming recognizable words. For some applications, it may be undesirable for IDs to accidentally contain offensive terms.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# npm
|
|
13
|
+
npm install @tnid/filter @tnid/core
|
|
14
|
+
|
|
15
|
+
# pnpm
|
|
16
|
+
pnpm add @tnid/filter @tnid/core
|
|
17
|
+
|
|
18
|
+
# bun
|
|
19
|
+
bun add @tnid/filter @tnid/core
|
|
20
|
+
|
|
21
|
+
# deno
|
|
22
|
+
deno add npm:@tnid/filter npm:@tnid/core
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For encryption-aware filtering, also install `@tnid/encryption`.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Tnid, TnidType } from "@tnid/core";
|
|
31
|
+
import { Blocklist, newV0Filtered, newV1Filtered } from "@tnid/filter";
|
|
32
|
+
|
|
33
|
+
const UserId = Tnid("user");
|
|
34
|
+
type UserId = TnidType<typeof UserId>;
|
|
35
|
+
|
|
36
|
+
// Create a blocklist
|
|
37
|
+
const blocklist = new Blocklist(["TACO", "FOO", "BAZZ"]);
|
|
38
|
+
|
|
39
|
+
// Generate filtered IDs
|
|
40
|
+
const v0: UserId = newV0Filtered(UserId, blocklist);
|
|
41
|
+
const v1: UserId = newV1Filtered(UserId, blocklist);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### With Encryption
|
|
45
|
+
|
|
46
|
+
If you use the encryption extension to convert V0 to V1, you probably want both forms to be clean:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { Tnid } from "@tnid/core";
|
|
50
|
+
import { EncryptionKey } from "@tnid/encryption";
|
|
51
|
+
import { Blocklist } from "@tnid/filter";
|
|
52
|
+
import { newV0FilteredForEncryption } from "@tnid/filter/encryption";
|
|
53
|
+
|
|
54
|
+
const UserId = Tnid("user");
|
|
55
|
+
const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
56
|
+
const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
57
|
+
|
|
58
|
+
// Both the V0 and its encrypted V1 will be clean
|
|
59
|
+
const v0 = await newV0FilteredForEncryption(UserId, blocklist, key);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### `Blocklist`
|
|
65
|
+
|
|
66
|
+
A compiled blocklist for case-insensitive substring matching. Patterns must only contain characters from the TNID data alphabet (`-0-9A-Z_a-z`).
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const blocklist = new Blocklist(["TACO", "FOO", "BAZZ"]);
|
|
70
|
+
|
|
71
|
+
blocklist.containsMatch("xyzTACOxyz"); // true
|
|
72
|
+
blocklist.containsMatch("xyztacoxyz"); // true (case-insensitive)
|
|
73
|
+
blocklist.containsMatch("xyzHELLOxyz"); // false
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `newV0Filtered(factory, blocklist)`
|
|
77
|
+
|
|
78
|
+
Generate a time-ordered V0 TNID whose data string contains no blocklisted words.
|
|
79
|
+
|
|
80
|
+
- **Throws**: `FilterError` if the iteration limit is exceeded
|
|
81
|
+
|
|
82
|
+
### `newV1Filtered(factory, blocklist)`
|
|
83
|
+
|
|
84
|
+
Generate a random V1 TNID whose data string contains no blocklisted words.
|
|
85
|
+
|
|
86
|
+
- **Throws**: `FilterError` if the iteration limit is exceeded
|
|
87
|
+
|
|
88
|
+
### `newV0FilteredForEncryption(factory, blocklist, key)` (from `@tnid/filter/encryption`)
|
|
89
|
+
|
|
90
|
+
Generate a V0 TNID where both the V0 and its encrypted V1 form contain no blocklisted words.
|
|
91
|
+
|
|
92
|
+
- **Returns**: `Promise<TnidValue<Name>>`
|
|
93
|
+
- **Throws**: `FilterError` if the iteration limit is exceeded
|
|
94
|
+
|
|
95
|
+
### `FilterError`
|
|
96
|
+
|
|
97
|
+
Thrown when filtered generation exceeds the iteration limit, which typically means the blocklist is too restrictive.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { FilterError } from "@tnid/filter";
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const id = newV0Filtered(UserId, blocklist);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
if (e instanceof FilterError) {
|
|
106
|
+
console.log(`Failed after ${e.iterations} iterations`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## How It Works
|
|
112
|
+
|
|
113
|
+
For V1, all bits are random, so the function simply regenerates until clean.
|
|
114
|
+
|
|
115
|
+
For V0, the strategy depends on where the match appears in the data string:
|
|
116
|
+
|
|
117
|
+
- **Random portion** (characters 7-16): Regenerate the random bits
|
|
118
|
+
- **Timestamp portion** (characters 0-6): Advance the timestamp enough to change the matched characters, avoiding a potentially large "bad window"
|
|
119
|
+
|
|
120
|
+
A global last-known-safe timestamp avoids re-discovering the same bad windows across calls.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocklist for matching substrings in TNID data strings.
|
|
3
|
+
*
|
|
4
|
+
* Uses a single compiled RegExp with alternation for efficient matching.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A compiled blocklist for efficient case-insensitive substring matching.
|
|
8
|
+
*
|
|
9
|
+
* Patterns may only contain characters from the TNID data string alphabet
|
|
10
|
+
* (`-0-9A-Z_a-z`). Patterns with other characters can never match and
|
|
11
|
+
* will be rejected.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const blocklist = new Blocklist(["TACO", "FOO", "BAZZ"]);
|
|
16
|
+
*
|
|
17
|
+
* blocklist.containsMatch("xyzTACOxyz"); // true
|
|
18
|
+
* blocklist.containsMatch("xyztacoxyz"); // true (case-insensitive)
|
|
19
|
+
* blocklist.containsMatch("xyzHELLOxyz"); // false
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class Blocklist {
|
|
23
|
+
private pattern;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new blocklist from the given patterns.
|
|
26
|
+
*
|
|
27
|
+
* @throws {Error} If any pattern contains characters outside the TNID data alphabet.
|
|
28
|
+
*/
|
|
29
|
+
constructor(patterns: string[]);
|
|
30
|
+
/** Returns `true` if the text contains any blocklisted word. */
|
|
31
|
+
containsMatch(text: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Finds the first blocklist match in the text.
|
|
34
|
+
* Returns the start index and length, or `null` if no match.
|
|
35
|
+
*/
|
|
36
|
+
findFirstMatch(text: string): {
|
|
37
|
+
start: number;
|
|
38
|
+
length: number;
|
|
39
|
+
} | null;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=blocklist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocklist.d.ts","sourceRoot":"","sources":["../src/blocklist.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAAgB;IAE/B;;;;OAIG;gBACS,QAAQ,EAAE,MAAM,EAAE;IAgB9B,gEAAgE;IAChE,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAKpC;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAMvE"}
|
package/esm/blocklist.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocklist for matching substrings in TNID data strings.
|
|
3
|
+
*
|
|
4
|
+
* Uses a single compiled RegExp with alternation for efficient matching.
|
|
5
|
+
*/
|
|
6
|
+
/** TNID data string alphabet: `-0-9A-Z_a-z` */
|
|
7
|
+
const VALID_PATTERN = /^[-0-9A-Za-z_]+$/;
|
|
8
|
+
/**
|
|
9
|
+
* A compiled blocklist for efficient case-insensitive substring matching.
|
|
10
|
+
*
|
|
11
|
+
* Patterns may only contain characters from the TNID data string alphabet
|
|
12
|
+
* (`-0-9A-Z_a-z`). Patterns with other characters can never match and
|
|
13
|
+
* will be rejected.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const blocklist = new Blocklist(["TACO", "FOO", "BAZZ"]);
|
|
18
|
+
*
|
|
19
|
+
* blocklist.containsMatch("xyzTACOxyz"); // true
|
|
20
|
+
* blocklist.containsMatch("xyztacoxyz"); // true (case-insensitive)
|
|
21
|
+
* blocklist.containsMatch("xyzHELLOxyz"); // false
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class Blocklist {
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new blocklist from the given patterns.
|
|
27
|
+
*
|
|
28
|
+
* @throws {Error} If any pattern contains characters outside the TNID data alphabet.
|
|
29
|
+
*/
|
|
30
|
+
constructor(patterns) {
|
|
31
|
+
Object.defineProperty(this, "pattern", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: void 0
|
|
36
|
+
});
|
|
37
|
+
const nonEmpty = patterns.filter((p) => p.length > 0);
|
|
38
|
+
for (const p of nonEmpty) {
|
|
39
|
+
if (!VALID_PATTERN.test(p)) {
|
|
40
|
+
throw new Error(`invalid blocklist pattern "${p}": only TNID data characters are allowed (-0-9A-Za-z_)`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (nonEmpty.length === 0) {
|
|
44
|
+
this.pattern = null;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.pattern = new RegExp(nonEmpty.join("|"), "i");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Returns `true` if the text contains any blocklisted word. */
|
|
51
|
+
containsMatch(text) {
|
|
52
|
+
if (this.pattern === null)
|
|
53
|
+
return false;
|
|
54
|
+
return this.pattern.test(text);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Finds the first blocklist match in the text.
|
|
58
|
+
* Returns the start index and length, or `null` if no match.
|
|
59
|
+
*/
|
|
60
|
+
findFirstMatch(text) {
|
|
61
|
+
if (this.pattern === null)
|
|
62
|
+
return null;
|
|
63
|
+
const m = this.pattern.exec(text);
|
|
64
|
+
if (m === null)
|
|
65
|
+
return null;
|
|
66
|
+
return { start: m.index, length: m[0].length };
|
|
67
|
+
}
|
|
68
|
+
}
|
package/esm/filter.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtered TNID generation functions.
|
|
3
|
+
*
|
|
4
|
+
* Generate TNIDs guaranteed not to contain blocklisted substrings
|
|
5
|
+
* in their data string representation.
|
|
6
|
+
*/
|
|
7
|
+
import type { NamedTnid, TnidValue } from "@tnid/core";
|
|
8
|
+
import type { Blocklist } from "./blocklist.js";
|
|
9
|
+
/** Error thrown when filtered generation exceeds the iteration limit. */
|
|
10
|
+
export declare class FilterError extends Error {
|
|
11
|
+
readonly iterations: number;
|
|
12
|
+
constructor(iterations: number);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Generate a V0 TNID whose data string contains no blocklisted words.
|
|
16
|
+
*
|
|
17
|
+
* Uses smart timestamp bumping when a match is in the timestamp portion,
|
|
18
|
+
* and random regeneration when a match touches the random portion.
|
|
19
|
+
*
|
|
20
|
+
* @throws {FilterError} If maximum iterations exceeded.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { Tnid } from "@tnid/core";
|
|
25
|
+
* import { Blocklist, newV0Filtered } from "@tnid/filter";
|
|
26
|
+
*
|
|
27
|
+
* const UserId = Tnid("user");
|
|
28
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
29
|
+
* const id = newV0Filtered(UserId, blocklist);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function newV0Filtered<Name extends string>(factory: NamedTnid<Name>, blocklist: Blocklist): TnidValue<Name>;
|
|
33
|
+
/**
|
|
34
|
+
* Generate a V1 TNID whose data string contains no blocklisted words.
|
|
35
|
+
*
|
|
36
|
+
* Since all V1 bits are random, simply regenerates until clean.
|
|
37
|
+
*
|
|
38
|
+
* @throws {FilterError} If maximum iterations exceeded.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { Tnid } from "@tnid/core";
|
|
43
|
+
* import { Blocklist, newV1Filtered } from "@tnid/filter";
|
|
44
|
+
*
|
|
45
|
+
* const UserId = Tnid("user");
|
|
46
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
47
|
+
* const id = newV1Filtered(UserId, blocklist);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function newV1Filtered<Name extends string>(factory: NamedTnid<Name>, blocklist: Blocklist): TnidValue<Name>;
|
|
51
|
+
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAchD,yEAAyE;AACzE,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,UAAU,EAAE,MAAM;CAO/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,IAAI,SAAS,MAAM,EAC/C,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,EACxB,SAAS,EAAE,SAAS,GACnB,SAAS,CAAC,IAAI,CAAC,CAkBjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,IAAI,SAAS,MAAM,EAC/C,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,EACxB,SAAS,EAAE,SAAS,GACnB,SAAS,CAAC,IAAI,CAAC,CAYjB"}
|
package/esm/filter.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtered TNID generation functions.
|
|
3
|
+
*
|
|
4
|
+
* Generate TNIDs guaranteed not to contain blocklisted substrings
|
|
5
|
+
* in their data string representation.
|
|
6
|
+
*/
|
|
7
|
+
import { dataString, getStartingTimestamp, handleV0Match, randomBigInt, recordSafeTimestamp, V0_RANDOM_BITS, V1_RANDOM_BITS, } from "./internals.js";
|
|
8
|
+
const MAX_V0_ITERATIONS = 1_000;
|
|
9
|
+
const MAX_V1_ITERATIONS = 100;
|
|
10
|
+
/** Error thrown when filtered generation exceeds the iteration limit. */
|
|
11
|
+
export class FilterError extends Error {
|
|
12
|
+
constructor(iterations) {
|
|
13
|
+
super(`failed to generate clean ID after ${iterations} iterations; blocklist may be too restrictive`);
|
|
14
|
+
Object.defineProperty(this, "iterations", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
20
|
+
this.name = "FilterError";
|
|
21
|
+
this.iterations = iterations;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate a V0 TNID whose data string contains no blocklisted words.
|
|
26
|
+
*
|
|
27
|
+
* Uses smart timestamp bumping when a match is in the timestamp portion,
|
|
28
|
+
* and random regeneration when a match touches the random portion.
|
|
29
|
+
*
|
|
30
|
+
* @throws {FilterError} If maximum iterations exceeded.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { Tnid } from "@tnid/core";
|
|
35
|
+
* import { Blocklist, newV0Filtered } from "@tnid/filter";
|
|
36
|
+
*
|
|
37
|
+
* const UserId = Tnid("user");
|
|
38
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
39
|
+
* const id = newV0Filtered(UserId, blocklist);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function newV0Filtered(factory, blocklist) {
|
|
43
|
+
let timestamp = getStartingTimestamp();
|
|
44
|
+
for (let i = 0; i < MAX_V0_ITERATIONS; i++) {
|
|
45
|
+
const random = randomBigInt(V0_RANDOM_BITS);
|
|
46
|
+
const id = factory.v0_from_parts(timestamp, random);
|
|
47
|
+
const data = dataString(id);
|
|
48
|
+
const match = blocklist.findFirstMatch(data);
|
|
49
|
+
if (match === null) {
|
|
50
|
+
recordSafeTimestamp(timestamp);
|
|
51
|
+
return id;
|
|
52
|
+
}
|
|
53
|
+
timestamp = handleV0Match(match, timestamp);
|
|
54
|
+
}
|
|
55
|
+
throw new FilterError(MAX_V0_ITERATIONS);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate a V1 TNID whose data string contains no blocklisted words.
|
|
59
|
+
*
|
|
60
|
+
* Since all V1 bits are random, simply regenerates until clean.
|
|
61
|
+
*
|
|
62
|
+
* @throws {FilterError} If maximum iterations exceeded.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import { Tnid } from "@tnid/core";
|
|
67
|
+
* import { Blocklist, newV1Filtered } from "@tnid/filter";
|
|
68
|
+
*
|
|
69
|
+
* const UserId = Tnid("user");
|
|
70
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
71
|
+
* const id = newV1Filtered(UserId, blocklist);
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function newV1Filtered(factory, blocklist) {
|
|
75
|
+
for (let i = 0; i < MAX_V1_ITERATIONS; i++) {
|
|
76
|
+
const random = randomBigInt(V1_RANDOM_BITS);
|
|
77
|
+
const id = factory.v1_from_parts(random);
|
|
78
|
+
const data = dataString(id);
|
|
79
|
+
if (!blocklist.containsMatch(data)) {
|
|
80
|
+
return id;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw new FilterError(MAX_V1_ITERATIONS);
|
|
84
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tnid/filter/encryption - Encryption-aware blocklist filtering
|
|
3
|
+
*
|
|
4
|
+
* Generate V0 TNIDs where both the V0 and its encrypted V1 form
|
|
5
|
+
* contain no blocklisted words.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Tnid } from "@tnid/core";
|
|
10
|
+
* import { EncryptionKey } from "@tnid/encryption";
|
|
11
|
+
* import { Blocklist } from "@tnid/filter";
|
|
12
|
+
* import { newV0FilteredForEncryption } from "@tnid/filter/encryption";
|
|
13
|
+
*
|
|
14
|
+
* const UserId = Tnid("user");
|
|
15
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
16
|
+
* const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
17
|
+
*
|
|
18
|
+
* const id = await newV0FilteredForEncryption(UserId, blocklist, key);
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import type { NamedTnid, TnidValue } from "@tnid/core";
|
|
24
|
+
import { type EncryptionKey } from "@tnid/encryption";
|
|
25
|
+
import type { Blocklist } from "./blocklist.js";
|
|
26
|
+
import { FilterError } from "./filter.js";
|
|
27
|
+
export { Blocklist } from "./blocklist.js";
|
|
28
|
+
export { FilterError };
|
|
29
|
+
/**
|
|
30
|
+
* Generate a V0 TNID where both the V0 and its encrypted V1 form
|
|
31
|
+
* contain no blocklisted words.
|
|
32
|
+
*
|
|
33
|
+
* Requires the V0/V1 Encryption extension.
|
|
34
|
+
*
|
|
35
|
+
* @throws {FilterError} If maximum iterations exceeded.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { Tnid } from "@tnid/core";
|
|
40
|
+
* import { EncryptionKey } from "@tnid/encryption";
|
|
41
|
+
* import { Blocklist } from "@tnid/filter";
|
|
42
|
+
* import { newV0FilteredForEncryption } from "@tnid/filter/encryption";
|
|
43
|
+
*
|
|
44
|
+
* const UserId = Tnid("user");
|
|
45
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
46
|
+
* const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
47
|
+
*
|
|
48
|
+
* const id = await newV0FilteredForEncryption(UserId, blocklist, key);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function newV0FilteredForEncryption<Name extends string>(factory: NamedTnid<Name>, blocklist: Blocklist, key: EncryptionKey): Promise<TnidValue<Name>>;
|
|
52
|
+
//# sourceMappingURL=filter_encryption.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter_encryption.d.ts","sourceRoot":"","sources":["../src/filter_encryption.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAW1C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,CAAC;AAIvB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,SAAS,MAAM,EAClE,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,EACxB,SAAS,EAAE,SAAS,EACpB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CA2B1B"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tnid/filter/encryption - Encryption-aware blocklist filtering
|
|
3
|
+
*
|
|
4
|
+
* Generate V0 TNIDs where both the V0 and its encrypted V1 form
|
|
5
|
+
* contain no blocklisted words.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Tnid } from "@tnid/core";
|
|
10
|
+
* import { EncryptionKey } from "@tnid/encryption";
|
|
11
|
+
* import { Blocklist } from "@tnid/filter";
|
|
12
|
+
* import { newV0FilteredForEncryption } from "@tnid/filter/encryption";
|
|
13
|
+
*
|
|
14
|
+
* const UserId = Tnid("user");
|
|
15
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
16
|
+
* const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
17
|
+
*
|
|
18
|
+
* const id = await newV0FilteredForEncryption(UserId, blocklist, key);
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import { encryptV0ToV1 } from "@tnid/encryption";
|
|
24
|
+
import { FilterError } from "./filter.js";
|
|
25
|
+
import { dataString, getStartingTimestamp, handleV0Match, randomBigInt, recordSafeTimestamp, V0_RANDOM_BITS, } from "./internals.js";
|
|
26
|
+
// Re-export Blocklist and FilterError for convenience
|
|
27
|
+
export { Blocklist } from "./blocklist.js";
|
|
28
|
+
export { FilterError };
|
|
29
|
+
const MAX_ENCRYPTION_ITERATIONS = 1_000;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a V0 TNID where both the V0 and its encrypted V1 form
|
|
32
|
+
* contain no blocklisted words.
|
|
33
|
+
*
|
|
34
|
+
* Requires the V0/V1 Encryption extension.
|
|
35
|
+
*
|
|
36
|
+
* @throws {FilterError} If maximum iterations exceeded.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { Tnid } from "@tnid/core";
|
|
41
|
+
* import { EncryptionKey } from "@tnid/encryption";
|
|
42
|
+
* import { Blocklist } from "@tnid/filter";
|
|
43
|
+
* import { newV0FilteredForEncryption } from "@tnid/filter/encryption";
|
|
44
|
+
*
|
|
45
|
+
* const UserId = Tnid("user");
|
|
46
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
47
|
+
* const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
|
|
48
|
+
*
|
|
49
|
+
* const id = await newV0FilteredForEncryption(UserId, blocklist, key);
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export async function newV0FilteredForEncryption(factory, blocklist, key) {
|
|
53
|
+
let timestamp = getStartingTimestamp();
|
|
54
|
+
for (let i = 0; i < MAX_ENCRYPTION_ITERATIONS; i++) {
|
|
55
|
+
const random = randomBigInt(V0_RANDOM_BITS);
|
|
56
|
+
const v0 = factory.v0_from_parts(timestamp, random);
|
|
57
|
+
const v0Data = dataString(v0);
|
|
58
|
+
// Check V0 first
|
|
59
|
+
const v0Match = blocklist.findFirstMatch(v0Data);
|
|
60
|
+
if (v0Match !== null) {
|
|
61
|
+
timestamp = handleV0Match(v0Match, timestamp);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// V0 is clean, now check encrypted V1
|
|
65
|
+
const v1 = await encryptV0ToV1(v0, key);
|
|
66
|
+
const v1Data = dataString(v1);
|
|
67
|
+
if (!blocklist.containsMatch(v1Data)) {
|
|
68
|
+
recordSafeTimestamp(timestamp);
|
|
69
|
+
return v0;
|
|
70
|
+
}
|
|
71
|
+
// V1 had a match - regenerate (loop continues with new random)
|
|
72
|
+
}
|
|
73
|
+
throw new FilterError(MAX_ENCRYPTION_ITERATIONS);
|
|
74
|
+
}
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tnid/filter - Blocklist filtering for TNID generation
|
|
3
|
+
*
|
|
4
|
+
* Generate TNIDs guaranteed not to contain specified substrings
|
|
5
|
+
* (e.g., profanity) in their string representation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Tnid } from "@tnid/core";
|
|
10
|
+
* import { Blocklist, newV0Filtered, newV1Filtered } from "@tnid/filter";
|
|
11
|
+
*
|
|
12
|
+
* const UserId = Tnid("user");
|
|
13
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
14
|
+
*
|
|
15
|
+
* const v0 = newV0Filtered(UserId, blocklist);
|
|
16
|
+
* const v1 = newV1Filtered(UserId, blocklist);
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* For encryption-aware filtering, use the `@tnid/filter/encryption` entry point.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
export { Blocklist } from "./blocklist.js";
|
|
24
|
+
export { FilterError, newV0Filtered, newV1Filtered } from "./filter.js";
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tnid/filter - Blocklist filtering for TNID generation
|
|
3
|
+
*
|
|
4
|
+
* Generate TNIDs guaranteed not to contain specified substrings
|
|
5
|
+
* (e.g., profanity) in their string representation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Tnid } from "@tnid/core";
|
|
10
|
+
* import { Blocklist, newV0Filtered, newV1Filtered } from "@tnid/filter";
|
|
11
|
+
*
|
|
12
|
+
* const UserId = Tnid("user");
|
|
13
|
+
* const blocklist = new Blocklist(["TACO", "FOO"]);
|
|
14
|
+
*
|
|
15
|
+
* const v0 = newV0Filtered(UserId, blocklist);
|
|
16
|
+
* const v1 = newV1Filtered(UserId, blocklist);
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* For encryption-aware filtering, use the `@tnid/filter/encryption` entry point.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
export { Blocklist } from "./blocklist.js";
|
|
24
|
+
export { FilterError, newV0Filtered, newV1Filtered } from "./filter.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internals for filter functions.
|
|
3
|
+
* @internal Not part of the public API.
|
|
4
|
+
*/
|
|
5
|
+
export declare const FIRST_CHAR_WITH_RANDOM = 7;
|
|
6
|
+
export declare const V0_RANDOM_BITS = 57;
|
|
7
|
+
export declare const V1_RANDOM_BITS = 100;
|
|
8
|
+
/** Global last-known-safe timestamp to avoid re-discovering bad windows. */
|
|
9
|
+
export declare let lastSafeTimestamp: bigint;
|
|
10
|
+
export declare function recordSafeTimestamp(ts: bigint): void;
|
|
11
|
+
export declare function getStartingTimestamp(): bigint;
|
|
12
|
+
/** Generate a random bigint with the specified number of bits. */
|
|
13
|
+
export declare function randomBigInt(bits: number): bigint;
|
|
14
|
+
/** Extract the 17-char data string from a TNID string. */
|
|
15
|
+
export declare function dataString(tnid: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Check if a match touches the random portion of V0 data string.
|
|
18
|
+
* If true, regenerating random bits may resolve the match.
|
|
19
|
+
* If false, the match is entirely in the timestamp portion and requires bumping.
|
|
20
|
+
*/
|
|
21
|
+
export declare function matchTouchesRandomPortion(start: number, length: number): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Minimum timestamp bump (in ms) needed to change the character at position `pos`.
|
|
24
|
+
* Each character encodes 6 bits. Lower positions = more significant = larger bump.
|
|
25
|
+
*/
|
|
26
|
+
export declare function timestampBumpForChar(pos: number): bigint;
|
|
27
|
+
/**
|
|
28
|
+
* Handle a V0 blocklist match: bump timestamp if match is in timestamp portion.
|
|
29
|
+
* Returns the (possibly bumped) timestamp.
|
|
30
|
+
*/
|
|
31
|
+
export declare function handleV0Match(match: {
|
|
32
|
+
start: number;
|
|
33
|
+
length: number;
|
|
34
|
+
}, timestamp: bigint): bigint;
|
|
35
|
+
//# sourceMappingURL=internals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internals.d.ts","sourceRoot":"","sources":["../src/internals.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,4EAA4E;AAC5E,eAAO,IAAI,iBAAiB,QAAK,CAAC;AAElC,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAG7C;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,0DAA0D;AAC1D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEhF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACxC,SAAS,EAAE,MAAM,GAChB,MAAM,CAMR"}
|