@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 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"}
@@ -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
+ }
@@ -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"}