@cloudyventures/baseh 1.1.0 → 2.0.1
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 +168 -0
- package/dist/basen.d.ts +2 -0
- package/dist/basen.js +7 -0
- package/dist/blocklist.js +4 -1
- package/dist/checksum.d.ts +4 -2
- package/dist/checksum.js +14 -8
- package/dist/cjs/basen.d.ts +7 -0
- package/dist/cjs/basen.js +47 -0
- package/dist/cjs/blocklist.d.ts +15 -0
- package/dist/cjs/blocklist.js +37 -0
- package/dist/cjs/checksum.d.ts +10 -0
- package/dist/cjs/checksum.js +40 -0
- package/dist/cjs/codec.d.ts +94 -0
- package/dist/cjs/codec.js +408 -0
- package/dist/cjs/errors.d.ts +8 -0
- package/dist/cjs/errors.js +15 -0
- package/dist/cjs/facade.d.ts +9 -0
- package/dist/cjs/facade.js +36 -0
- package/dist/cjs/feistel.d.ts +16 -0
- package/dist/cjs/feistel.js +119 -0
- package/dist/cjs/index.d.ts +14 -0
- package/dist/cjs/index.js +50 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/profile.d.ts +91 -0
- package/dist/cjs/profile.js +279 -0
- package/dist/cjs/profiles.d.ts +33 -0
- package/dist/cjs/profiles.js +192 -0
- package/dist/codec.d.ts +58 -2
- package/dist/codec.js +246 -34
- package/dist/facade.d.ts +9 -0
- package/dist/facade.js +30 -0
- package/dist/feistel.d.ts +6 -0
- package/dist/feistel.js +11 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/profile.d.ts +52 -1
- package/dist/profile.js +129 -21
- package/dist/profiles.d.ts +18 -7
- package/dist/profiles.js +91 -29
- package/package.json +33 -7
- package/dist/zero.d.ts +0 -4
- package/dist/zero.js +0 -39
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# @cloudyventures/baseh
|
|
2
|
+
|
|
3
|
+
TypeScript implementation of the baseH (Human Reference Code) codec. Encodes
|
|
4
|
+
integer IDs as checksummed, human-friendly reference codes — short codes that
|
|
5
|
+
grow automatically in expandable mode (recommended), or fixed-length codes on
|
|
6
|
+
the classic tiers — with a feistel-v1 permutation on every tier and profanity
|
|
7
|
+
safety. The normative spec is `spec/IMPLEMENTATION_CODEC.md` in the
|
|
8
|
+
[monorepo](https://github.com/cloudyventures/baseh).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @cloudyventures/baseh
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
One runtime dependency (`@noble/hashes`, auditable and dependency-free itself). Requires Node 18 or later (native `BigInt`).
|
|
17
|
+
|
|
18
|
+
## Expandable mode (recommended)
|
|
19
|
+
|
|
20
|
+
Every profile carries a `mode` field: `"expandable"` or `"fixed"`. Expandable
|
|
21
|
+
is the recommended default for new users: codes start short (minimum 4
|
|
22
|
+
characters, profile field `minLength`) and grow automatically as the ID
|
|
23
|
+
sequence climbs past each length's capacity — transparently, with no
|
|
24
|
+
migration and no re-issue. Shorter codes already issued keep decoding
|
|
25
|
+
forever; the code's length selects the generation on decode.
|
|
26
|
+
|
|
27
|
+
The recommended starting tier is `baseh-expandable-v1`:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Baseh, basehExpandableV1 } from "@cloudyventures/baseh";
|
|
31
|
+
|
|
32
|
+
const codec = new Baseh(basehExpandableV1());
|
|
33
|
+
|
|
34
|
+
const code = codec.encode(123456n); // short code; grows as ids climb
|
|
35
|
+
|
|
36
|
+
const result = codec.decode(code);
|
|
37
|
+
result.id; // 123456n
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Expandable mode differs from the fixed tiers as follows:
|
|
41
|
+
|
|
42
|
+
- The default body alphabet is the 27 symbols left after the medium visual
|
|
43
|
+
and spoken safety strips and the `0`/`O` zero ban, so an issued code
|
|
44
|
+
never emits a visual or spoken confusable. A custom alphabet that includes
|
|
45
|
+
`0`/`O` has those symbols silently removed during profile preparation.
|
|
46
|
+
- The checksum alphabet is the body alphabet plus `0` (28 symbols by
|
|
47
|
+
default). The `O -> 0` input alias remains, so a misread `O` in a checksum
|
|
48
|
+
position resolves to `0`; a `0` or `O` in a body position is simply an
|
|
49
|
+
invalid character.
|
|
50
|
+
- The short checksum is on by default (codec spec 22): one checksum symbol
|
|
51
|
+
through 5 characters, two beyond; generation 4 holds 19,683 ids (3 body +
|
|
52
|
+
1 checksum) instead of 729. Configure with `shortChecksumLength` and
|
|
53
|
+
`shortChecksumUntil`; `shortChecksumUntil: 0` turns it off.
|
|
54
|
+
- There is no left-padding; codes use exactly the length of the current
|
|
55
|
+
generation.
|
|
56
|
+
- The Feistel permutation stays on, applied per generation with the code
|
|
57
|
+
length mixed into the key derivation alongside the profile id. Codes within
|
|
58
|
+
each length look random even though issuance is a sequential counter.
|
|
59
|
+
Presentation only, not encryption — same caveat as the fixed tiers.
|
|
60
|
+
- Separators only appear once codes reach `separatorMinLength` characters
|
|
61
|
+
(6 in the shipped tier). Below that threshold there is no separator and no
|
|
62
|
+
grouping. Above it the split is the balanced rule of codec spec 19.5 —
|
|
63
|
+
a pure function of the code length (`XXX-XXX` at 6, `XXXX-XXX` at 7,
|
|
64
|
+
`XXXX-XXXX` at 8) — so expandable profiles carry no `grouping` field.
|
|
65
|
+
- All other profile options — visual/spoken safety levels, profanity modes,
|
|
66
|
+
blocklists — compose with expandable unchanged.
|
|
67
|
+
- The repetition filter is on by default here too: the tier ships
|
|
68
|
+
`maxRepetition: 4`, so a code with a run of four or more identical symbols
|
|
69
|
+
is never issued (any floor of 3 or more is configurable; 0 turns it off).
|
|
70
|
+
|
|
71
|
+
A keyed private-mapping variant `baseh-expandable-p-v1` mirrors the `-p`
|
|
72
|
+
fixed tiers:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { basehExpandablePV1 } from "@cloudyventures/baseh";
|
|
76
|
+
|
|
77
|
+
const codec = new Baseh(
|
|
78
|
+
basehExpandablePV1({ keyBytes, keyId: "prod-01" })
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Security posture is unchanged: a code is a reference alias, never an
|
|
83
|
+
authorization token. The smallest expandable generations are a small
|
|
84
|
+
namespace, so rate-limit public lookups and enforce authorization after
|
|
85
|
+
decode.
|
|
86
|
+
|
|
87
|
+
## Frozen tiers (fixed mode)
|
|
88
|
+
|
|
89
|
+
The classic frozen tiers are all `mode: "fixed"`: constant-width codes for
|
|
90
|
+
when you need a stable printed length. Four frozen tiers ship with the
|
|
91
|
+
package, built from the full alphanumeric set with cumulative visual and
|
|
92
|
+
spoken strips. All four encode 6 body symbols, are case-insensitive,
|
|
93
|
+
hyphen-delimit at the midpoint, run the default profanity blocklist, block
|
|
94
|
+
runs of four or more identical symbols (the repetition filter,
|
|
95
|
+
`maxRepetition: 4` — configurable to any floor of 3 or more, or 0 to turn it
|
|
96
|
+
off) and permute with the published frozen key.
|
|
97
|
+
|
|
98
|
+
| Tier | Helper | Body symbols | Checksum | Format | Capacity |
|
|
99
|
+
| ---- | ------ | ------------ | -------- | ------ | -------- |
|
|
100
|
+
| Minimum | `basehMinimumV1` | 36 | none | `XXX-XXX` | 2,176,782,336 |
|
|
101
|
+
| Light | `basehLightV1` | 31 | 2 | `XXXX-XXXX` | 887,503,681 |
|
|
102
|
+
| Medium | `basehMediumV1` | 28 | 2 | `XXXX-XXXX` | 481,890,304 |
|
|
103
|
+
| Heavy | `basehHeavyV1` | 26 | 2 | `XXXX-XXXX` | 308,915,776 |
|
|
104
|
+
|
|
105
|
+
Medium is the default fixed tier. The frozen key is public by design: it
|
|
106
|
+
hides sequence, not records. See the spec, section 7.5.
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { Baseh, basehExpandableV1 } from "@cloudyventures/baseh";
|
|
112
|
+
|
|
113
|
+
const codec = new Baseh(basehExpandableV1());
|
|
114
|
+
|
|
115
|
+
const code = codec.encode(123456n); // short code; grows as ids climb
|
|
116
|
+
|
|
117
|
+
const result = codec.decode(code);
|
|
118
|
+
result.id; // 123456n
|
|
119
|
+
result.canonicalCode; // canonical form
|
|
120
|
+
result.corrected; // true when input needed correction
|
|
121
|
+
|
|
122
|
+
codec.capacity; // capacity of the current generation
|
|
123
|
+
|
|
124
|
+
const check = codec.validate("00000000");
|
|
125
|
+
check.valid; // false
|
|
126
|
+
check.reason; // "INVALID_CHECKSUM"
|
|
127
|
+
|
|
128
|
+
// Spoken-confusion correction
|
|
129
|
+
codec.decode("TB14QDFU", { tryCorrection: true, confusionProfile: "light" });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Fixed mode works the same way, through a fixed tier helper:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { basehMediumV1 } from "@cloudyventures/baseh";
|
|
136
|
+
|
|
137
|
+
const fixed = new Baseh(basehMediumV1());
|
|
138
|
+
const code = fixed.encode(123456n); // fixed-width hyphenated code
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
IDs are `bigint`, so every capacity and ID operation is exact at any size.
|
|
142
|
+
|
|
143
|
+
## Permutation
|
|
144
|
+
|
|
145
|
+
The plain tiers permute with `FROZEN_KEY_BYTES` (the published frozen key).
|
|
146
|
+
The `P` variants take a caller-supplied key instead; keep that key in a
|
|
147
|
+
secret manager and never change it for a live profile:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { basehMediumPV1 } from "@cloudyventures/baseh";
|
|
151
|
+
|
|
152
|
+
const codec = new Baseh(
|
|
153
|
+
basehMediumPV1({ keyBytes, keyId: "prod-01" })
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Errors
|
|
158
|
+
|
|
159
|
+
All failures raise `BasehError` with a `code` from the spec:
|
|
160
|
+
`INVALID_PROFILE`, `OUT_OF_RANGE`, `PERMUTATION_FAILURE`, `INVALID_LENGTH`,
|
|
161
|
+
`INVALID_CHARACTER`, `INVALID_CHECKSUM`, `AMBIGUOUS_INPUT`,
|
|
162
|
+
`TOO_MANY_CANDIDATES` and `BLOCKED_CODE`. `validate` never raises on user
|
|
163
|
+
input.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
AGPL-3.0. Commercial licensing available; see
|
|
168
|
+
[COMMERCIAL.md](https://github.com/cloudyventures/baseh/blob/main/COMMERCIAL.md).
|
package/dist/basen.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export declare function encodeBaseN(value: bigint, alphabet: string, length: num
|
|
|
3
3
|
/** Spec 5.2. */
|
|
4
4
|
export declare function decodeBaseN(text: string, alphabet: string, index: Map<string, bigint>): bigint;
|
|
5
5
|
export declare function alphabetIndex(alphabet: string): Map<string, bigint>;
|
|
6
|
+
/** Integer power for bigint bases; shared by profile, codec and checksum. */
|
|
7
|
+
export declare function powBigInt(base: bigint, exp: number): bigint;
|
package/dist/basen.js
CHANGED
|
@@ -32,3 +32,10 @@ export function alphabetIndex(alphabet) {
|
|
|
32
32
|
[...alphabet].forEach((ch, i) => m.set(ch, BigInt(i)));
|
|
33
33
|
return m;
|
|
34
34
|
}
|
|
35
|
+
/** Integer power for bigint bases; shared by profile, codec and checksum. */
|
|
36
|
+
export function powBigInt(base, exp) {
|
|
37
|
+
let result = 1n;
|
|
38
|
+
for (let i = 0; i < exp; i += 1)
|
|
39
|
+
result *= base;
|
|
40
|
+
return result;
|
|
41
|
+
}
|
package/dist/blocklist.js
CHANGED
|
@@ -5,6 +5,9 @@ export const DEFAULT_BLOCKLIST = [
|
|
|
5
5
|
"SHT", "CNT", "TWT", "DCK", "AZZ", "BCH"
|
|
6
6
|
];
|
|
7
7
|
const WORD = /^[A-Za-z]{2,32}$/;
|
|
8
|
+
// JS `$` also matches before a trailing newline, so the charset test alone
|
|
9
|
+
// accepts "abc\n"; an explicit newline check restores Ruby's \A...\z strictness.
|
|
10
|
+
const NEWLINE = /[\r\n]/;
|
|
8
11
|
function fail(reason) {
|
|
9
12
|
throw new BasehError("INVALID_PROFILE", `Invalid baseH profile: ${reason}`, false);
|
|
10
13
|
}
|
|
@@ -14,7 +17,7 @@ export function effectiveBlocklist(profanity) {
|
|
|
14
17
|
const list = [...base, ...(profanity.extraWords ?? [])];
|
|
15
18
|
const out = [];
|
|
16
19
|
for (const word of list) {
|
|
17
|
-
if (typeof word !== "string" || !WORD.test(word)) {
|
|
20
|
+
if (typeof word !== "string" || !WORD.test(word) || NEWLINE.test(word)) {
|
|
18
21
|
fail("blocklist entries must be 2 through 32 ASCII letters");
|
|
19
22
|
}
|
|
20
23
|
const upper = word.toUpperCase();
|
package/dist/checksum.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { PreparedProfile } from "./profile.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Spec 6.2. Rolling polynomial checksum over symbol values.
|
|
4
4
|
* Returns the checksum value in [0, modulus).
|
|
5
|
+
* Spec 22: expandable generations may pass a shorter effective checksum
|
|
6
|
+
* length; the modulus is then S^length instead of the profile default.
|
|
5
7
|
*/
|
|
6
|
-
export declare function checksumValue(profile: PreparedProfile, body: string, bodyIndex: Map<string, bigint
|
|
8
|
+
export declare function checksumValue(profile: PreparedProfile, body: string, bodyIndex: Map<string, bigint>, checksumLength?: number): bigint;
|
|
7
9
|
/** Compute the expected checksum string for a normalized body. */
|
|
8
|
-
export declare function calculateChecksum(profile: PreparedProfile, body: string): string;
|
|
10
|
+
export declare function calculateChecksum(profile: PreparedProfile, body: string, checksumLength?: number, bodyIndex?: Map<string, bigint>): string;
|
package/dist/checksum.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { BasehError } from "./errors.js";
|
|
2
|
-
import { alphabetIndex, encodeBaseN } from "./basen.js";
|
|
2
|
+
import { alphabetIndex, encodeBaseN, powBigInt } from "./basen.js";
|
|
3
3
|
/**
|
|
4
4
|
* Spec 6.2. Rolling polynomial checksum over symbol values.
|
|
5
5
|
* Returns the checksum value in [0, modulus).
|
|
6
|
+
* Spec 22: expandable generations may pass a shorter effective checksum
|
|
7
|
+
* length; the modulus is then S^length instead of the profile default.
|
|
6
8
|
*/
|
|
7
|
-
export function checksumValue(profile, body, bodyIndex) {
|
|
8
|
-
const modulus = profile.
|
|
9
|
+
export function checksumValue(profile, body, bodyIndex, checksumLength = profile.checksumLength) {
|
|
10
|
+
const modulus = checksumLength === profile.checksumLength
|
|
11
|
+
? profile.checksumModulus
|
|
12
|
+
: powBigInt(BigInt(profile.checksumAlphabetNorm.length || 1), checksumLength);
|
|
9
13
|
let state = 17n;
|
|
10
14
|
for (let i = 0; i < profile.profileId.length; i += 1) {
|
|
11
15
|
state = (state * 37n + BigInt(profile.profileId.charCodeAt(i)) + 1n) % modulus;
|
|
@@ -21,10 +25,12 @@ export function checksumValue(profile, body, bodyIndex) {
|
|
|
21
25
|
return state;
|
|
22
26
|
}
|
|
23
27
|
/** Compute the expected checksum string for a normalized body. */
|
|
24
|
-
export function calculateChecksum(profile, body) {
|
|
25
|
-
if (
|
|
28
|
+
export function calculateChecksum(profile, body, checksumLength = profile.checksumLength, bodyIndex) {
|
|
29
|
+
if (checksumLength === 0)
|
|
26
30
|
return "";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
// Correction loops call this up to 64 times; callers with a prepared index
|
|
32
|
+
// pass it in rather than rebuilding one per call.
|
|
33
|
+
const index = bodyIndex ?? alphabetIndex(profile.bodyAlphabetNorm);
|
|
34
|
+
const value = checksumValue(profile, body, index, checksumLength);
|
|
35
|
+
return encodeBaseN(value, profile.checksumAlphabetNorm, checksumLength);
|
|
30
36
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Spec 5.1. Fixed-length base-N encode, most significant digit first. */
|
|
2
|
+
export declare function encodeBaseN(value: bigint, alphabet: string, length: number): string;
|
|
3
|
+
/** Spec 5.2. */
|
|
4
|
+
export declare function decodeBaseN(text: string, alphabet: string, index: Map<string, bigint>): bigint;
|
|
5
|
+
export declare function alphabetIndex(alphabet: string): Map<string, bigint>;
|
|
6
|
+
/** Integer power for bigint bases; shared by profile, codec and checksum. */
|
|
7
|
+
export declare function powBigInt(base: bigint, exp: number): bigint;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodeBaseN = encodeBaseN;
|
|
4
|
+
exports.decodeBaseN = decodeBaseN;
|
|
5
|
+
exports.alphabetIndex = alphabetIndex;
|
|
6
|
+
exports.powBigInt = powBigInt;
|
|
7
|
+
const errors_js_1 = require("./errors.js");
|
|
8
|
+
/** Spec 5.1. Fixed-length base-N encode, most significant digit first. */
|
|
9
|
+
function encodeBaseN(value, alphabet, length) {
|
|
10
|
+
const base = BigInt(alphabet.length);
|
|
11
|
+
const out = new Array(length);
|
|
12
|
+
let v = value;
|
|
13
|
+
for (let pos = length - 1; pos >= 0; pos -= 1) {
|
|
14
|
+
const digit = Number(v % base);
|
|
15
|
+
const ch = alphabet[digit];
|
|
16
|
+
if (ch === undefined)
|
|
17
|
+
throw new errors_js_1.BasehError("OUT_OF_RANGE", "digit outside alphabet");
|
|
18
|
+
out[pos] = ch;
|
|
19
|
+
v = v / base;
|
|
20
|
+
}
|
|
21
|
+
return out.join("");
|
|
22
|
+
}
|
|
23
|
+
/** Spec 5.2. */
|
|
24
|
+
function decodeBaseN(text, alphabet, index) {
|
|
25
|
+
const base = BigInt(alphabet.length);
|
|
26
|
+
let value = 0n;
|
|
27
|
+
for (const ch of text) {
|
|
28
|
+
const digit = index.get(ch);
|
|
29
|
+
if (digit === undefined) {
|
|
30
|
+
throw new errors_js_1.BasehError("INVALID_CHARACTER", `Symbol ${JSON.stringify(ch)} is not in the alphabet`);
|
|
31
|
+
}
|
|
32
|
+
value = value * base + digit;
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function alphabetIndex(alphabet) {
|
|
37
|
+
const m = new Map();
|
|
38
|
+
[...alphabet].forEach((ch, i) => m.set(ch, BigInt(i)));
|
|
39
|
+
return m;
|
|
40
|
+
}
|
|
41
|
+
/** Integer power for bigint bases; shared by profile, codec and checksum. */
|
|
42
|
+
function powBigInt(base, exp) {
|
|
43
|
+
let result = 1n;
|
|
44
|
+
for (let i = 0; i < exp; i += 1)
|
|
45
|
+
result *= base;
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type BasehProfanityMode = "none" | "no-vowels" | "blocklist";
|
|
2
|
+
/** Spec 18. Optional profanity safety configuration. */
|
|
3
|
+
export interface BasehProfanity {
|
|
4
|
+
mode: BasehProfanityMode;
|
|
5
|
+
/** Replaces the default list when present (mode "blocklist" only). */
|
|
6
|
+
words?: string[];
|
|
7
|
+
/** Appended to the effective list (mode "blocklist" only). */
|
|
8
|
+
extraWords?: string[];
|
|
9
|
+
}
|
|
10
|
+
/** Spec 18.2 default list. Deliberately small; applications extend it. */
|
|
11
|
+
export declare const DEFAULT_BLOCKLIST: readonly string[];
|
|
12
|
+
/** Spec 18.2: replacement semantics, then augmentation, uppercased and deduplicated. */
|
|
13
|
+
export declare function effectiveBlocklist(profanity: BasehProfanity): string[];
|
|
14
|
+
/** Spec 18.1: vowels removed for no-vowels mode, applied after case normalization. */
|
|
15
|
+
export declare function stripVowels(alphabetNorm: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_BLOCKLIST = void 0;
|
|
4
|
+
exports.effectiveBlocklist = effectiveBlocklist;
|
|
5
|
+
exports.stripVowels = stripVowels;
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
/** Spec 18.2 default list. Deliberately small; applications extend it. */
|
|
8
|
+
exports.DEFAULT_BLOCKLIST = [
|
|
9
|
+
"CRAP", "TWAT", "SHAG", "DAMN", "FCK", "FUC",
|
|
10
|
+
"SHT", "CNT", "TWT", "DCK", "AZZ", "BCH"
|
|
11
|
+
];
|
|
12
|
+
const WORD = /^[A-Za-z]{2,32}$/;
|
|
13
|
+
// JS `$` also matches before a trailing newline, so the charset test alone
|
|
14
|
+
// accepts "abc\n"; an explicit newline check restores Ruby's \A...\z strictness.
|
|
15
|
+
const NEWLINE = /[\r\n]/;
|
|
16
|
+
function fail(reason) {
|
|
17
|
+
throw new errors_js_1.BasehError("INVALID_PROFILE", `Invalid baseH profile: ${reason}`, false);
|
|
18
|
+
}
|
|
19
|
+
/** Spec 18.2: replacement semantics, then augmentation, uppercased and deduplicated. */
|
|
20
|
+
function effectiveBlocklist(profanity) {
|
|
21
|
+
const base = profanity.words ? [...profanity.words] : [...exports.DEFAULT_BLOCKLIST];
|
|
22
|
+
const list = [...base, ...(profanity.extraWords ?? [])];
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const word of list) {
|
|
25
|
+
if (typeof word !== "string" || !WORD.test(word) || NEWLINE.test(word)) {
|
|
26
|
+
fail("blocklist entries must be 2 through 32 ASCII letters");
|
|
27
|
+
}
|
|
28
|
+
const upper = word.toUpperCase();
|
|
29
|
+
if (!out.includes(upper))
|
|
30
|
+
out.push(upper);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** Spec 18.1: vowels removed for no-vowels mode, applied after case normalization. */
|
|
35
|
+
function stripVowels(alphabetNorm) {
|
|
36
|
+
return [...alphabetNorm].filter((c) => !"AEIOU".includes(c)).join("");
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PreparedProfile } from "./profile.js";
|
|
2
|
+
/**
|
|
3
|
+
* Spec 6.2. Rolling polynomial checksum over symbol values.
|
|
4
|
+
* Returns the checksum value in [0, modulus).
|
|
5
|
+
* Spec 22: expandable generations may pass a shorter effective checksum
|
|
6
|
+
* length; the modulus is then S^length instead of the profile default.
|
|
7
|
+
*/
|
|
8
|
+
export declare function checksumValue(profile: PreparedProfile, body: string, bodyIndex: Map<string, bigint>, checksumLength?: number): bigint;
|
|
9
|
+
/** Compute the expected checksum string for a normalized body. */
|
|
10
|
+
export declare function calculateChecksum(profile: PreparedProfile, body: string, checksumLength?: number, bodyIndex?: Map<string, bigint>): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checksumValue = checksumValue;
|
|
4
|
+
exports.calculateChecksum = calculateChecksum;
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
6
|
+
const basen_js_1 = require("./basen.js");
|
|
7
|
+
/**
|
|
8
|
+
* Spec 6.2. Rolling polynomial checksum over symbol values.
|
|
9
|
+
* Returns the checksum value in [0, modulus).
|
|
10
|
+
* Spec 22: expandable generations may pass a shorter effective checksum
|
|
11
|
+
* length; the modulus is then S^length instead of the profile default.
|
|
12
|
+
*/
|
|
13
|
+
function checksumValue(profile, body, bodyIndex, checksumLength = profile.checksumLength) {
|
|
14
|
+
const modulus = checksumLength === profile.checksumLength
|
|
15
|
+
? profile.checksumModulus
|
|
16
|
+
: (0, basen_js_1.powBigInt)(BigInt(profile.checksumAlphabetNorm.length || 1), checksumLength);
|
|
17
|
+
let state = 17n;
|
|
18
|
+
for (let i = 0; i < profile.profileId.length; i += 1) {
|
|
19
|
+
state = (state * 37n + BigInt(profile.profileId.charCodeAt(i)) + 1n) % modulus;
|
|
20
|
+
}
|
|
21
|
+
state = (state * 37n) % modulus;
|
|
22
|
+
for (let pos = 0; pos < body.length; pos += 1) {
|
|
23
|
+
const symValue = bodyIndex.get(body[pos]);
|
|
24
|
+
if (symValue === undefined) {
|
|
25
|
+
throw new errors_js_1.BasehError("INVALID_CHARACTER", `Body symbol ${JSON.stringify(body[pos])} is not in the body alphabet`);
|
|
26
|
+
}
|
|
27
|
+
state = (state * 37n + symValue + BigInt(pos + 1)) % modulus;
|
|
28
|
+
}
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
31
|
+
/** Compute the expected checksum string for a normalized body. */
|
|
32
|
+
function calculateChecksum(profile, body, checksumLength = profile.checksumLength, bodyIndex) {
|
|
33
|
+
if (checksumLength === 0)
|
|
34
|
+
return "";
|
|
35
|
+
// Correction loops call this up to 64 times; callers with a prepared index
|
|
36
|
+
// pass it in rather than rebuilding one per call.
|
|
37
|
+
const index = bodyIndex ?? (0, basen_js_1.alphabetIndex)(profile.bodyAlphabetNorm);
|
|
38
|
+
const value = checksumValue(profile, body, index, checksumLength);
|
|
39
|
+
return (0, basen_js_1.encodeBaseN)(value, profile.checksumAlphabetNorm, checksumLength);
|
|
40
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type BasehErrorCode } from "./errors.js";
|
|
2
|
+
import { type BasehProfile, type PreparedProfile } from "./profile.js";
|
|
3
|
+
export type ConfusionProfileName = "none" | "light" | "medium" | "heavy";
|
|
4
|
+
/** Built-in spoken-confusion candidate maps. Spec 3.3; pairs apply to body symbols only. */
|
|
5
|
+
export declare const CONFUSION_MAPS: Record<Exclude<ConfusionProfileName, "none">, Record<string, string[]>>;
|
|
6
|
+
export interface DecodeOptions {
|
|
7
|
+
acceptSpaces?: boolean;
|
|
8
|
+
tryCorrection?: boolean;
|
|
9
|
+
confusionProfile?: ConfusionProfileName;
|
|
10
|
+
maxCorrections?: 0 | 1;
|
|
11
|
+
}
|
|
12
|
+
export interface DecodeResult {
|
|
13
|
+
id: bigint;
|
|
14
|
+
canonicalCode: string;
|
|
15
|
+
corrected: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ValidateResult {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
canonicalCode?: string;
|
|
20
|
+
reason?: BasehErrorCode;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Spec 12.5. Live as-you-type feedback for a code entry field. `typing`
|
|
24
|
+
* carries the normalized typed symbols with separators inserted as far as
|
|
25
|
+
* the groups go, plus a progress fraction; `invalid` carries the
|
|
26
|
+
* BasehErrorCode from validate; `valid` is only ever reported for a
|
|
27
|
+
* complete code.
|
|
28
|
+
*/
|
|
29
|
+
export type InspectResult = {
|
|
30
|
+
state: "empty";
|
|
31
|
+
} | {
|
|
32
|
+
state: "typing";
|
|
33
|
+
typed: string;
|
|
34
|
+
progress: number;
|
|
35
|
+
} | {
|
|
36
|
+
state: "bad-char";
|
|
37
|
+
} | {
|
|
38
|
+
state: "too-long";
|
|
39
|
+
} | {
|
|
40
|
+
state: "invalid";
|
|
41
|
+
reason: BasehErrorCode;
|
|
42
|
+
} | {
|
|
43
|
+
state: "valid";
|
|
44
|
+
id: bigint;
|
|
45
|
+
canonicalCode: string;
|
|
46
|
+
};
|
|
47
|
+
/** Spec 3.1 normalization, steps 1-7. Returns the raw unformatted string. */
|
|
48
|
+
export declare function normalize(input: string, profile: PreparedProfile, acceptSpaces?: boolean): string;
|
|
49
|
+
export declare function formatRaw(raw: string, profile: PreparedProfile): string;
|
|
50
|
+
/**
|
|
51
|
+
* Spec 19.5. Balanced grouping: the split is a pure function of the total
|
|
52
|
+
* length — `g = max(2, ceil(L / 5))` groups differing in size by at most
|
|
53
|
+
* one, larger groups to the left. There is no configurable pattern in
|
|
54
|
+
* expandable mode (`grouping` must be empty, section 2.2).
|
|
55
|
+
*/
|
|
56
|
+
export declare function expandableGrouping(length: number): number[];
|
|
57
|
+
/**
|
|
58
|
+
* Spec 19.1/22.3. First id of generation L: the sum of each generation's
|
|
59
|
+
* capacity A^(k - effectiveK(k)) for k from minLength through L-1. The
|
|
60
|
+
* effective checksum length is per-generation (spec 22), so the sum is not
|
|
61
|
+
* a single geometric series when the short checksum is on.
|
|
62
|
+
*/
|
|
63
|
+
export declare function generationBase(profile: PreparedProfile, length: number): bigint;
|
|
64
|
+
/** Spec 19.1/22.3. Ids held by generation L: A^(L - effectiveK(L)). */
|
|
65
|
+
export declare function generationCapacity(profile: PreparedProfile, length: number): bigint;
|
|
66
|
+
/** Smallest generation whose range holds id, per spec 19.6. */
|
|
67
|
+
export declare function generationForId(profile: PreparedProfile, id: bigint): number;
|
|
68
|
+
/** Spec 10. Substitution-only candidate generation, capped and deduplicated. */
|
|
69
|
+
export declare function generateCandidates(body: string, confusionMap: Record<string, string[]>, maxEdits?: number): string[];
|
|
70
|
+
export declare class Baseh {
|
|
71
|
+
readonly profile: PreparedProfile;
|
|
72
|
+
private readonly bodyIndex;
|
|
73
|
+
constructor(profile: BasehProfile);
|
|
74
|
+
capacity(): bigint;
|
|
75
|
+
private permKey;
|
|
76
|
+
private checkBlocked;
|
|
77
|
+
/** Spec 8 (fixed mode). */
|
|
78
|
+
private encodeFixed;
|
|
79
|
+
/** Spec 19.6. */
|
|
80
|
+
private encodeExpandable;
|
|
81
|
+
/** Spec 8/19.6. */
|
|
82
|
+
encode(id: bigint | number): string;
|
|
83
|
+
/** Spec 9/19.7. */
|
|
84
|
+
decode(input: string, options?: DecodeOptions): DecodeResult;
|
|
85
|
+
/** Spec 12.4. Never throws on user input. */
|
|
86
|
+
validate(input: string, options?: DecodeOptions): ValidateResult;
|
|
87
|
+
/**
|
|
88
|
+
* Spec 12.5. Live as-you-type inspection. Gates on the typed length before
|
|
89
|
+
* validating, so spec 3.4 re-padding can never paint an incomplete fixed-mode
|
|
90
|
+
* code `valid` (or `invalid`): a short fixed input is `typing`, never
|
|
91
|
+
* checked. Never throws on user input.
|
|
92
|
+
*/
|
|
93
|
+
inspect(input: string): InspectResult;
|
|
94
|
+
}
|