@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.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared internals for filter functions.
3
+ * @internal Not part of the public API.
4
+ */
5
+ // V0 data string character layout:
6
+ // Chars 0-6: pure timestamp bits
7
+ // Char 7: 1 timestamp bit + 2 variant bits + 3 random bits
8
+ // Chars 8-16: pure random bits
9
+ export const FIRST_CHAR_WITH_RANDOM = 7;
10
+ export const V0_RANDOM_BITS = 57;
11
+ export const V1_RANDOM_BITS = 100;
12
+ /** Global last-known-safe timestamp to avoid re-discovering bad windows. */
13
+ export let lastSafeTimestamp = 0n;
14
+ export function recordSafeTimestamp(ts) {
15
+ if (ts > lastSafeTimestamp) {
16
+ lastSafeTimestamp = ts;
17
+ }
18
+ }
19
+ export function getStartingTimestamp() {
20
+ const current = BigInt(Date.now());
21
+ return current > lastSafeTimestamp ? current : lastSafeTimestamp;
22
+ }
23
+ /** Generate a random bigint with the specified number of bits. */
24
+ export function randomBigInt(bits) {
25
+ const bytes = new Uint8Array(Math.ceil(bits / 8));
26
+ crypto.getRandomValues(bytes);
27
+ let result = 0n;
28
+ for (const b of bytes)
29
+ result = (result << 8n) | BigInt(b);
30
+ return result & ((1n << BigInt(bits)) - 1n);
31
+ }
32
+ /** Extract the 17-char data string from a TNID string. */
33
+ export function dataString(tnid) {
34
+ return tnid.substring(tnid.indexOf(".") + 1);
35
+ }
36
+ /**
37
+ * Check if a match touches the random portion of V0 data string.
38
+ * If true, regenerating random bits may resolve the match.
39
+ * If false, the match is entirely in the timestamp portion and requires bumping.
40
+ */
41
+ export function matchTouchesRandomPortion(start, length) {
42
+ return start + length > FIRST_CHAR_WITH_RANDOM;
43
+ }
44
+ /**
45
+ * Minimum timestamp bump (in ms) needed to change the character at position `pos`.
46
+ * Each character encodes 6 bits. Lower positions = more significant = larger bump.
47
+ */
48
+ export function timestampBumpForChar(pos) {
49
+ return 1n << BigInt(42 - 6 * pos);
50
+ }
51
+ /**
52
+ * Handle a V0 blocklist match: bump timestamp if match is in timestamp portion.
53
+ * Returns the (possibly bumped) timestamp.
54
+ */
55
+ export function handleV0Match(match, timestamp) {
56
+ if (!matchTouchesRandomPortion(match.start, match.length)) {
57
+ const rightmostChar = match.start + match.length - 1;
58
+ return timestamp + timestampBumpForChar(rightmostChar);
59
+ }
60
+ return timestamp;
61
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@tnid/filter",
3
+ "version": "0.1.0",
4
+ "description": "Blocklist filtering for TNIDs - generate IDs that avoid specified substrings",
5
+ "keywords": [
6
+ "uuid",
7
+ "id",
8
+ "identifier",
9
+ "tnid",
10
+ "typed",
11
+ "type-safe"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/mkeedlinger/tnid-typescript.git"
16
+ },
17
+ "license": "MIT",
18
+ "bugs": {
19
+ "url": "https://github.com/mkeedlinger/tnid-typescript/issues"
20
+ },
21
+ "main": "./script/index.js",
22
+ "module": "./esm/index.js",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./esm/index.js",
26
+ "require": "./script/index.js"
27
+ },
28
+ "./encryption": {
29
+ "import": "./esm/filter_encryption.js",
30
+ "require": "./script/filter_encryption.js"
31
+ }
32
+ },
33
+ "scripts": {},
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "dependencies": {},
41
+ "peerDependencies": {
42
+ "@tnid/core": "^0.1.0",
43
+ "@tnid/encryption": "^0.1.0"
44
+ },
45
+ "_generatedBy": "dnt@dev"
46
+ }
@@ -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,72 @@
1
+ "use strict";
2
+ /**
3
+ * Blocklist for matching substrings in TNID data strings.
4
+ *
5
+ * Uses a single compiled RegExp with alternation for efficient matching.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.Blocklist = void 0;
9
+ /** TNID data string alphabet: `-0-9A-Z_a-z` */
10
+ const VALID_PATTERN = /^[-0-9A-Za-z_]+$/;
11
+ /**
12
+ * A compiled blocklist for efficient case-insensitive substring matching.
13
+ *
14
+ * Patterns may only contain characters from the TNID data string alphabet
15
+ * (`-0-9A-Z_a-z`). Patterns with other characters can never match and
16
+ * will be rejected.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const blocklist = new Blocklist(["TACO", "FOO", "BAZZ"]);
21
+ *
22
+ * blocklist.containsMatch("xyzTACOxyz"); // true
23
+ * blocklist.containsMatch("xyztacoxyz"); // true (case-insensitive)
24
+ * blocklist.containsMatch("xyzHELLOxyz"); // false
25
+ * ```
26
+ */
27
+ class Blocklist {
28
+ /**
29
+ * Creates a new blocklist from the given patterns.
30
+ *
31
+ * @throws {Error} If any pattern contains characters outside the TNID data alphabet.
32
+ */
33
+ constructor(patterns) {
34
+ Object.defineProperty(this, "pattern", {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: void 0
39
+ });
40
+ const nonEmpty = patterns.filter((p) => p.length > 0);
41
+ for (const p of nonEmpty) {
42
+ if (!VALID_PATTERN.test(p)) {
43
+ throw new Error(`invalid blocklist pattern "${p}": only TNID data characters are allowed (-0-9A-Za-z_)`);
44
+ }
45
+ }
46
+ if (nonEmpty.length === 0) {
47
+ this.pattern = null;
48
+ }
49
+ else {
50
+ this.pattern = new RegExp(nonEmpty.join("|"), "i");
51
+ }
52
+ }
53
+ /** Returns `true` if the text contains any blocklisted word. */
54
+ containsMatch(text) {
55
+ if (this.pattern === null)
56
+ return false;
57
+ return this.pattern.test(text);
58
+ }
59
+ /**
60
+ * Finds the first blocklist match in the text.
61
+ * Returns the start index and length, or `null` if no match.
62
+ */
63
+ findFirstMatch(text) {
64
+ if (this.pattern === null)
65
+ return null;
66
+ const m = this.pattern.exec(text);
67
+ if (m === null)
68
+ return null;
69
+ return { start: m.index, length: m[0].length };
70
+ }
71
+ }
72
+ exports.Blocklist = Blocklist;
@@ -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"}
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /**
3
+ * Filtered TNID generation functions.
4
+ *
5
+ * Generate TNIDs guaranteed not to contain blocklisted substrings
6
+ * in their data string representation.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.FilterError = void 0;
10
+ exports.newV0Filtered = newV0Filtered;
11
+ exports.newV1Filtered = newV1Filtered;
12
+ const internals_js_1 = require("./internals.js");
13
+ const MAX_V0_ITERATIONS = 1_000;
14
+ const MAX_V1_ITERATIONS = 100;
15
+ /** Error thrown when filtered generation exceeds the iteration limit. */
16
+ class FilterError extends Error {
17
+ constructor(iterations) {
18
+ super(`failed to generate clean ID after ${iterations} iterations; blocklist may be too restrictive`);
19
+ Object.defineProperty(this, "iterations", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: void 0
24
+ });
25
+ this.name = "FilterError";
26
+ this.iterations = iterations;
27
+ }
28
+ }
29
+ exports.FilterError = FilterError;
30
+ /**
31
+ * Generate a V0 TNID whose data string contains no blocklisted words.
32
+ *
33
+ * Uses smart timestamp bumping when a match is in the timestamp portion,
34
+ * and random regeneration when a match touches the random portion.
35
+ *
36
+ * @throws {FilterError} If maximum iterations exceeded.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * import { Tnid } from "@tnid/core";
41
+ * import { Blocklist, newV0Filtered } from "@tnid/filter";
42
+ *
43
+ * const UserId = Tnid("user");
44
+ * const blocklist = new Blocklist(["TACO", "FOO"]);
45
+ * const id = newV0Filtered(UserId, blocklist);
46
+ * ```
47
+ */
48
+ function newV0Filtered(factory, blocklist) {
49
+ let timestamp = (0, internals_js_1.getStartingTimestamp)();
50
+ for (let i = 0; i < MAX_V0_ITERATIONS; i++) {
51
+ const random = (0, internals_js_1.randomBigInt)(internals_js_1.V0_RANDOM_BITS);
52
+ const id = factory.v0_from_parts(timestamp, random);
53
+ const data = (0, internals_js_1.dataString)(id);
54
+ const match = blocklist.findFirstMatch(data);
55
+ if (match === null) {
56
+ (0, internals_js_1.recordSafeTimestamp)(timestamp);
57
+ return id;
58
+ }
59
+ timestamp = (0, internals_js_1.handleV0Match)(match, timestamp);
60
+ }
61
+ throw new FilterError(MAX_V0_ITERATIONS);
62
+ }
63
+ /**
64
+ * Generate a V1 TNID whose data string contains no blocklisted words.
65
+ *
66
+ * Since all V1 bits are random, simply regenerates until clean.
67
+ *
68
+ * @throws {FilterError} If maximum iterations exceeded.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * import { Tnid } from "@tnid/core";
73
+ * import { Blocklist, newV1Filtered } from "@tnid/filter";
74
+ *
75
+ * const UserId = Tnid("user");
76
+ * const blocklist = new Blocklist(["TACO", "FOO"]);
77
+ * const id = newV1Filtered(UserId, blocklist);
78
+ * ```
79
+ */
80
+ function newV1Filtered(factory, blocklist) {
81
+ for (let i = 0; i < MAX_V1_ITERATIONS; i++) {
82
+ const random = (0, internals_js_1.randomBigInt)(internals_js_1.V1_RANDOM_BITS);
83
+ const id = factory.v1_from_parts(random);
84
+ const data = (0, internals_js_1.dataString)(id);
85
+ if (!blocklist.containsMatch(data)) {
86
+ return id;
87
+ }
88
+ }
89
+ throw new FilterError(MAX_V1_ITERATIONS);
90
+ }
@@ -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,79 @@
1
+ "use strict";
2
+ /**
3
+ * @tnid/filter/encryption - Encryption-aware blocklist filtering
4
+ *
5
+ * Generate V0 TNIDs where both the V0 and its encrypted V1 form
6
+ * contain no blocklisted words.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { Tnid } from "@tnid/core";
11
+ * import { EncryptionKey } from "@tnid/encryption";
12
+ * import { Blocklist } from "@tnid/filter";
13
+ * import { newV0FilteredForEncryption } from "@tnid/filter/encryption";
14
+ *
15
+ * const UserId = Tnid("user");
16
+ * const blocklist = new Blocklist(["TACO", "FOO"]);
17
+ * const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
18
+ *
19
+ * const id = await newV0FilteredForEncryption(UserId, blocklist, key);
20
+ * ```
21
+ *
22
+ * @module
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.FilterError = exports.Blocklist = void 0;
26
+ exports.newV0FilteredForEncryption = newV0FilteredForEncryption;
27
+ const encryption_1 = require("@tnid/encryption");
28
+ const filter_js_1 = require("./filter.js");
29
+ Object.defineProperty(exports, "FilterError", { enumerable: true, get: function () { return filter_js_1.FilterError; } });
30
+ const internals_js_1 = require("./internals.js");
31
+ // Re-export Blocklist and FilterError for convenience
32
+ var blocklist_js_1 = require("./blocklist.js");
33
+ Object.defineProperty(exports, "Blocklist", { enumerable: true, get: function () { return blocklist_js_1.Blocklist; } });
34
+ const MAX_ENCRYPTION_ITERATIONS = 1_000;
35
+ /**
36
+ * Generate a V0 TNID where both the V0 and its encrypted V1 form
37
+ * contain no blocklisted words.
38
+ *
39
+ * Requires the V0/V1 Encryption extension.
40
+ *
41
+ * @throws {FilterError} If maximum iterations exceeded.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { Tnid } from "@tnid/core";
46
+ * import { EncryptionKey } from "@tnid/encryption";
47
+ * import { Blocklist } from "@tnid/filter";
48
+ * import { newV0FilteredForEncryption } from "@tnid/filter/encryption";
49
+ *
50
+ * const UserId = Tnid("user");
51
+ * const blocklist = new Blocklist(["TACO", "FOO"]);
52
+ * const key = EncryptionKey.fromHex("0102030405060708090a0b0c0d0e0f10");
53
+ *
54
+ * const id = await newV0FilteredForEncryption(UserId, blocklist, key);
55
+ * ```
56
+ */
57
+ async function newV0FilteredForEncryption(factory, blocklist, key) {
58
+ let timestamp = (0, internals_js_1.getStartingTimestamp)();
59
+ for (let i = 0; i < MAX_ENCRYPTION_ITERATIONS; i++) {
60
+ const random = (0, internals_js_1.randomBigInt)(internals_js_1.V0_RANDOM_BITS);
61
+ const v0 = factory.v0_from_parts(timestamp, random);
62
+ const v0Data = (0, internals_js_1.dataString)(v0);
63
+ // Check V0 first
64
+ const v0Match = blocklist.findFirstMatch(v0Data);
65
+ if (v0Match !== null) {
66
+ timestamp = (0, internals_js_1.handleV0Match)(v0Match, timestamp);
67
+ continue;
68
+ }
69
+ // V0 is clean, now check encrypted V1
70
+ const v1 = await (0, encryption_1.encryptV0ToV1)(v0, key);
71
+ const v1Data = (0, internals_js_1.dataString)(v1);
72
+ if (!blocklist.containsMatch(v1Data)) {
73
+ (0, internals_js_1.recordSafeTimestamp)(timestamp);
74
+ return v0;
75
+ }
76
+ // V1 had a match - regenerate (loop continues with new random)
77
+ }
78
+ throw new filter_js_1.FilterError(MAX_ENCRYPTION_ITERATIONS);
79
+ }
@@ -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"}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * @tnid/filter - Blocklist filtering for TNID generation
4
+ *
5
+ * Generate TNIDs guaranteed not to contain specified substrings
6
+ * (e.g., profanity) in their string representation.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { Tnid } from "@tnid/core";
11
+ * import { Blocklist, newV0Filtered, newV1Filtered } from "@tnid/filter";
12
+ *
13
+ * const UserId = Tnid("user");
14
+ * const blocklist = new Blocklist(["TACO", "FOO"]);
15
+ *
16
+ * const v0 = newV0Filtered(UserId, blocklist);
17
+ * const v1 = newV1Filtered(UserId, blocklist);
18
+ * ```
19
+ *
20
+ * For encryption-aware filtering, use the `@tnid/filter/encryption` entry point.
21
+ *
22
+ * @module
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.newV1Filtered = exports.newV0Filtered = exports.FilterError = exports.Blocklist = void 0;
26
+ var blocklist_js_1 = require("./blocklist.js");
27
+ Object.defineProperty(exports, "Blocklist", { enumerable: true, get: function () { return blocklist_js_1.Blocklist; } });
28
+ var filter_js_1 = require("./filter.js");
29
+ Object.defineProperty(exports, "FilterError", { enumerable: true, get: function () { return filter_js_1.FilterError; } });
30
+ Object.defineProperty(exports, "newV0Filtered", { enumerable: true, get: function () { return filter_js_1.newV0Filtered; } });
31
+ Object.defineProperty(exports, "newV1Filtered", { enumerable: true, get: function () { return filter_js_1.newV1Filtered; } });
@@ -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"}