@cloudyventures/baseh 1.1.0 → 2.0.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/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/dist/profile.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { BasehError } from "./errors.js";
|
|
2
|
+
import { powBigInt } from "./basen.js";
|
|
2
3
|
import { effectiveBlocklist, stripVowels } from "./blocklist.js";
|
|
4
|
+
/**
|
|
5
|
+
* Spec 22. The checksum length that applies to a generation of the given
|
|
6
|
+
* total length: `shortChecksumLength` at or below `shortChecksumUntil`,
|
|
7
|
+
* `checksumLength` above it (and always in fixed mode). The feature is on
|
|
8
|
+
* exactly when `shortChecksumUntil` is non-zero; a `shortChecksumLength` of
|
|
9
|
+
* 0 then means the window's generations carry no checksum symbols at all.
|
|
10
|
+
*/
|
|
11
|
+
export function effectiveChecksumLength(profile, length) {
|
|
12
|
+
if (profile.mode === "expandable" && profile.shortChecksumUntil > 0 && length <= profile.shortChecksumUntil) {
|
|
13
|
+
return profile.shortChecksumLength;
|
|
14
|
+
}
|
|
15
|
+
return profile.checksumLength;
|
|
16
|
+
}
|
|
3
17
|
const ASCII_ONLY = /^[\x20-\x7e]*$/;
|
|
4
18
|
function fail(reason) {
|
|
5
19
|
throw new BasehError("INVALID_PROFILE", `Invalid baseH profile: ${reason}`, false);
|
|
@@ -10,12 +24,6 @@ function isAsciiChar(ch) {
|
|
|
10
24
|
function norm(profile, ch) {
|
|
11
25
|
return profile.caseSensitive ? ch : ch.toUpperCase();
|
|
12
26
|
}
|
|
13
|
-
function powBigInt(base, exp) {
|
|
14
|
-
let result = 1n;
|
|
15
|
-
for (let i = 0; i < exp; i += 1)
|
|
16
|
-
result *= base;
|
|
17
|
-
return result;
|
|
18
|
-
}
|
|
19
27
|
/**
|
|
20
28
|
* Validates a profile per spec section 2.2 and returns it with derived,
|
|
21
29
|
* pre-computed values. Throws BasehError INVALID_PROFILE on any violation.
|
|
@@ -29,6 +37,12 @@ export function prepareProfile(profile) {
|
|
|
29
37
|
}
|
|
30
38
|
if (!ASCII_ONLY.test(profile.profileId))
|
|
31
39
|
fail("profileId must be ASCII");
|
|
40
|
+
// Spec 2.2/19.9. A persisted or frozen profile declares its mode; profiles
|
|
41
|
+
// built before the mode field existed are fixed, so the frozen vectors keep
|
|
42
|
+
// matching byte for byte.
|
|
43
|
+
const mode = profile.mode ?? "fixed";
|
|
44
|
+
if (mode !== "fixed" && mode !== "expandable")
|
|
45
|
+
fail("mode must be fixed or expandable");
|
|
32
46
|
const caseSensitive = profile.caseSensitive === true;
|
|
33
47
|
const bodyAlphabet = profile.bodyAlphabet;
|
|
34
48
|
if (typeof bodyAlphabet !== "string" || bodyAlphabet.length < 2) {
|
|
@@ -40,21 +54,83 @@ export function prepareProfile(profile) {
|
|
|
40
54
|
}
|
|
41
55
|
const view = { caseSensitive };
|
|
42
56
|
let bodyNorm = [...bodyAlphabet].map((c) => norm(view, c)).join("");
|
|
57
|
+
// Spec 19.2: in expandable mode the zero ban strips 0 and O from the body
|
|
58
|
+
// alphabet silently, before any other validation, exactly like the
|
|
59
|
+
// no-vowels strip of section 18.1.
|
|
60
|
+
if (mode === "expandable") {
|
|
61
|
+
bodyNorm = [...bodyNorm].filter((c) => c !== "0" && c !== "O").join("");
|
|
62
|
+
}
|
|
43
63
|
if (new Set(bodyNorm).size !== bodyNorm.length) {
|
|
44
64
|
fail("body alphabet symbols must be unique after case normalization");
|
|
45
65
|
}
|
|
46
|
-
if (
|
|
47
|
-
profile.bodyLength
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
if (mode === "fixed") {
|
|
67
|
+
if (!Number.isInteger(profile.bodyLength) ||
|
|
68
|
+
profile.bodyLength < 1 ||
|
|
69
|
+
profile.bodyLength > 32) {
|
|
70
|
+
fail("bodyLength must be an integer from 1 through 32");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const minLength = profile.minLength ?? 4;
|
|
74
|
+
const separatorMinLength = profile.separatorMinLength ?? 0;
|
|
75
|
+
if (mode === "fixed" && separatorMinLength !== 0) {
|
|
76
|
+
fail("separatorMinLength must be 0 in fixed mode");
|
|
50
77
|
}
|
|
51
78
|
if (!Number.isInteger(profile.checksumLength) ||
|
|
52
79
|
profile.checksumLength < 0 ||
|
|
53
80
|
profile.checksumLength > 8) {
|
|
54
81
|
fail("checksumLength must be an integer from 0 through 8");
|
|
55
82
|
}
|
|
83
|
+
if (mode === "expandable") {
|
|
84
|
+
if (!Number.isInteger(minLength) || minLength < 1) {
|
|
85
|
+
fail("minLength must be an integer of at least 1");
|
|
86
|
+
}
|
|
87
|
+
if (minLength <= profile.checksumLength) {
|
|
88
|
+
fail("minLength must be greater than checksumLength");
|
|
89
|
+
}
|
|
90
|
+
if (!Number.isInteger(separatorMinLength) || separatorMinLength < 0) {
|
|
91
|
+
fail("separatorMinLength must be an integer of at least 0");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Spec 22. The short checksum is expandable-only. The window field is the
|
|
95
|
+
// switch: `shortChecksumUntil` of 0 or absent turns the feature off (the
|
|
96
|
+
// codebase convention, like maxRepetition), and the length field without
|
|
97
|
+
// a window is INVALID_PROFILE.
|
|
98
|
+
const shortChecksumLength = profile.shortChecksumLength ?? 0;
|
|
99
|
+
const shortChecksumUntil = profile.shortChecksumUntil ?? 0;
|
|
100
|
+
if (mode === "fixed") {
|
|
101
|
+
if (shortChecksumLength !== 0 || shortChecksumUntil !== 0) {
|
|
102
|
+
fail("shortChecksumLength and shortChecksumUntil are expandable-mode only");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (shortChecksumUntil !== 0) {
|
|
106
|
+
if (!Number.isInteger(profile.shortChecksumUntil) || shortChecksumUntil < minLength) {
|
|
107
|
+
fail("shortChecksumUntil must be an integer of at least minLength");
|
|
108
|
+
}
|
|
109
|
+
// Beyond 8 the window would swallow nearly every practical code, and
|
|
110
|
+
// long codes genuinely want two checksum symbols.
|
|
111
|
+
if (shortChecksumUntil > 8) {
|
|
112
|
+
fail("shortChecksumUntil must be at most 8");
|
|
113
|
+
}
|
|
114
|
+
if (!Number.isInteger(shortChecksumLength) ||
|
|
115
|
+
shortChecksumLength < 0 ||
|
|
116
|
+
shortChecksumLength >= profile.checksumLength) {
|
|
117
|
+
fail("shortChecksumLength must be an integer from 0 through checksumLength - 1");
|
|
118
|
+
}
|
|
119
|
+
if (minLength <= shortChecksumLength) {
|
|
120
|
+
fail("minLength must be greater than shortChecksumLength");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (shortChecksumLength !== 0) {
|
|
124
|
+
fail("shortChecksumLength requires shortChecksumUntil");
|
|
125
|
+
}
|
|
56
126
|
const checksumAlphabet = profile.checksumAlphabet ?? "";
|
|
57
|
-
|
|
127
|
+
let checksumNorm = [...checksumAlphabet].map((c) => norm(view, c)).join("");
|
|
128
|
+
if (mode === "expandable") {
|
|
129
|
+
// Spec 19.3: the checksum alphabet is derived, "0" followed by the body
|
|
130
|
+
// alphabet in order. The configured checksumAlphabet is not consulted.
|
|
131
|
+
checksumNorm = "";
|
|
132
|
+
}
|
|
133
|
+
else if (profile.checksumLength > 0) {
|
|
58
134
|
if (typeof checksumAlphabet !== "string" || checksumAlphabet.length < 2) {
|
|
59
135
|
fail("checksumAlphabet needs at least two symbols when checksumLength is positive");
|
|
60
136
|
}
|
|
@@ -62,10 +138,9 @@ export function prepareProfile(profile) {
|
|
|
62
138
|
if (!isAsciiChar(ch))
|
|
63
139
|
fail(`checksum alphabet symbol is not single ASCII: ${JSON.stringify(ch)}`);
|
|
64
140
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
fail("checksum alphabet symbols must be unique after case normalization");
|
|
141
|
+
if (new Set(checksumNorm).size !== checksumNorm.length) {
|
|
142
|
+
fail("checksum alphabet symbols must be unique after case normalization");
|
|
143
|
+
}
|
|
69
144
|
}
|
|
70
145
|
// Spec 18. no-vowels strips vowels before every downstream rule; blocklist
|
|
71
146
|
// only arms the encode-time scan.
|
|
@@ -79,11 +154,26 @@ export function prepareProfile(profile) {
|
|
|
79
154
|
if (bodyNorm.length < 2) {
|
|
80
155
|
fail("no-vowels mode leaves the body alphabet with fewer than two symbols");
|
|
81
156
|
}
|
|
82
|
-
if (profile.checksumLength > 0 && checksumNorm.length < 2) {
|
|
157
|
+
if (mode === "fixed" && profile.checksumLength > 0 && checksumNorm.length < 2) {
|
|
83
158
|
fail("no-vowels mode leaves the checksum alphabet with fewer than two symbols");
|
|
84
159
|
}
|
|
85
160
|
}
|
|
161
|
+
if (mode === "expandable") {
|
|
162
|
+
// Derived after every body strip (zero ban, no-vowels) so all downstream
|
|
163
|
+
// rules — modulus, separator collision, alias targets — see the final
|
|
164
|
+
// alphabets.
|
|
165
|
+
checksumNorm = "0" + bodyNorm;
|
|
166
|
+
}
|
|
167
|
+
if (bodyNorm.length < 2) {
|
|
168
|
+
fail("body alphabet needs at least two symbols after preparation");
|
|
169
|
+
}
|
|
86
170
|
const blocklist = profanity.mode === "blocklist" ? effectiveBlocklist(profanity) : [];
|
|
171
|
+
// Spec 21: 0 disables the filter; an active filter needs a floor of 3 —
|
|
172
|
+
// banning pairs (2) would destroy roughly 9% of every generation.
|
|
173
|
+
const maxRepetition = profile.maxRepetition ?? 0;
|
|
174
|
+
if (!Number.isInteger(maxRepetition) || maxRepetition < 0 || (maxRepetition > 0 && maxRepetition < 3)) {
|
|
175
|
+
fail("maxRepetition must be 0 (off) or an integer of at least 3");
|
|
176
|
+
}
|
|
87
177
|
const separator = profile.separator ?? "";
|
|
88
178
|
for (const ch of separator) {
|
|
89
179
|
if (bodyNorm.includes(ch) || checksumNorm.includes(ch)) {
|
|
@@ -100,12 +190,18 @@ export function prepareProfile(profile) {
|
|
|
100
190
|
fail(`alias target is not single ASCII: ${JSON.stringify(tgt)}`);
|
|
101
191
|
const sNorm = norm(view, src);
|
|
102
192
|
const tNorm = norm(view, tgt);
|
|
103
|
-
if (canonicalSet.has(sNorm)) {
|
|
104
|
-
fail(`alias source ${JSON.stringify(src)} is already a canonical symbol`);
|
|
105
|
-
}
|
|
106
193
|
if (!canonicalSet.has(tNorm)) {
|
|
107
194
|
fail(`alias target ${JSON.stringify(tgt)} is not a canonical symbol`);
|
|
108
195
|
}
|
|
196
|
+
// Spec 3.2: an alias must never map two distinct canonical symbols into
|
|
197
|
+
// one value. Fixed mode rejects a canonical alias source outright. In
|
|
198
|
+
// expandable mode the frozen tier (spec 17.1) carries aliases whose
|
|
199
|
+
// sources are canonical body symbols (T, N, W stay in the body
|
|
200
|
+
// alphabet); the canonical symbol wins at normalization, making those
|
|
201
|
+
// entries inert instead of destructive.
|
|
202
|
+
if (mode === "fixed" && canonicalSet.has(sNorm)) {
|
|
203
|
+
fail(`alias source ${JSON.stringify(src)} is already a canonical symbol`);
|
|
204
|
+
}
|
|
109
205
|
if (sNorm in aliasesNorm)
|
|
110
206
|
fail(`duplicate alias source ${JSON.stringify(sNorm)} after case normalization`);
|
|
111
207
|
if (tNorm in aliases || [...Object.keys(aliases)].some((k) => norm(view, k) === tNorm)) {
|
|
@@ -118,6 +214,12 @@ export function prepareProfile(profile) {
|
|
|
118
214
|
if (profile.grouping.length !== 0)
|
|
119
215
|
fail("grouping must be empty when separator is empty");
|
|
120
216
|
}
|
|
217
|
+
else if (mode === "expandable") {
|
|
218
|
+
// Spec 19.5: the balanced grouping rule is a pure function of the total
|
|
219
|
+
// length, so a configurable grouping is meaningless in expandable mode.
|
|
220
|
+
if (profile.grouping.length !== 0)
|
|
221
|
+
fail("grouping must be empty in expandable mode");
|
|
222
|
+
}
|
|
121
223
|
else if (total !== profile.bodyLength + profile.checksumLength) {
|
|
122
224
|
fail("group sizes must sum to bodyLength + checksumLength");
|
|
123
225
|
}
|
|
@@ -140,6 +242,9 @@ export function prepareProfile(profile) {
|
|
|
140
242
|
}
|
|
141
243
|
return {
|
|
142
244
|
...profile,
|
|
245
|
+
mode,
|
|
246
|
+
minLength,
|
|
247
|
+
separatorMinLength,
|
|
143
248
|
caseSensitive,
|
|
144
249
|
checksumAlphabet,
|
|
145
250
|
separator,
|
|
@@ -150,8 +255,11 @@ export function prepareProfile(profile) {
|
|
|
150
255
|
checksumAlphabetNorm: checksumNorm,
|
|
151
256
|
aliasesNorm,
|
|
152
257
|
checksumModulus: powBigInt(BigInt(checksumNorm.length || 1), profile.checksumLength),
|
|
153
|
-
capacity: powBigInt(BigInt(bodyNorm.length), profile.bodyLength),
|
|
154
|
-
blocklist
|
|
258
|
+
capacity: powBigInt(BigInt(bodyNorm.length), profile.bodyLength ?? 0),
|
|
259
|
+
blocklist,
|
|
260
|
+
maxRepetition,
|
|
261
|
+
shortChecksumLength,
|
|
262
|
+
shortChecksumUntil
|
|
155
263
|
};
|
|
156
264
|
}
|
|
157
265
|
function bodySum(grouping) {
|
package/dist/profiles.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { BasehProfile } from "./profile.js";
|
|
2
|
+
/**
|
|
3
|
+
* The frozen published permutation key. Public by design: it makes issued
|
|
4
|
+
* codes look non-sequential but offers no secrecy, since anyone can read it
|
|
5
|
+
* here. Never swap it on a live namespace; codes only decode with the key
|
|
6
|
+
* they were issued under. Use the -p variants to supply private key material.
|
|
7
|
+
*/
|
|
8
|
+
export declare const FROZEN_KEY_BYTES: Uint8Array;
|
|
2
9
|
export interface FrozenKeyOptions {
|
|
3
10
|
keyBytes: Uint8Array;
|
|
4
11
|
keyId?: string;
|
|
@@ -6,17 +13,21 @@ export interface FrozenKeyOptions {
|
|
|
6
13
|
}
|
|
7
14
|
/** Alphanumeric, no safety strips, no checksum, hyphen-delimited XXX-XXX. */
|
|
8
15
|
export declare function basehMinimumV1(): BasehProfile;
|
|
9
|
-
/** baseh-minimum with
|
|
16
|
+
/** baseh-minimum permuted with caller-supplied key material. */
|
|
10
17
|
export declare function basehMinimumPV1(options: FrozenKeyOptions): BasehProfile;
|
|
11
|
-
/** Visual light plus spoken light,
|
|
18
|
+
/** Visual light plus spoken light, two checksum symbols, hyphen-delimited. */
|
|
12
19
|
export declare function basehLightV1(): BasehProfile;
|
|
13
|
-
/** baseh-light with
|
|
20
|
+
/** baseh-light permuted with caller-supplied key material. */
|
|
14
21
|
export declare function basehLightPV1(options: FrozenKeyOptions): BasehProfile;
|
|
15
|
-
/** Visual medium plus spoken medium,
|
|
22
|
+
/** Visual medium plus spoken medium, two checksum symbols, hyphen-delimited. The default. */
|
|
16
23
|
export declare function basehMediumV1(): BasehProfile;
|
|
17
|
-
/** baseh-medium with
|
|
24
|
+
/** baseh-medium permuted with caller-supplied key material. */
|
|
18
25
|
export declare function basehMediumPV1(options: FrozenKeyOptions): BasehProfile;
|
|
19
|
-
/** Conservative alphabet plus spoken heavy,
|
|
26
|
+
/** Conservative alphabet plus spoken heavy, two checksum symbols, hyphen-delimited. */
|
|
20
27
|
export declare function basehHeavyV1(): BasehProfile;
|
|
21
|
-
/**
|
|
28
|
+
/** The frozen expandable tier; the recommended starting point for new namespaces. */
|
|
29
|
+
export declare function basehExpandableV1(): BasehProfile;
|
|
30
|
+
/** baseh-expandable permuted with caller-supplied key material. */
|
|
31
|
+
export declare function basehExpandablePV1(options: FrozenKeyOptions): BasehProfile;
|
|
32
|
+
/** baseh-heavy permuted with caller-supplied key material. */
|
|
22
33
|
export declare function basehHeavyPV1(options: FrozenKeyOptions): BasehProfile;
|
package/dist/profiles.js
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* visual and spoken strips; the spoken strips interact with the visual ones
|
|
4
4
|
* exactly as the web tools derive them, so the tool capacities match.
|
|
5
5
|
*
|
|
6
|
-
* Minimum 36 symbols, no checksum
|
|
7
|
-
* Light 31 symbols,
|
|
8
|
-
* Medium 28 symbols,
|
|
9
|
-
* Heavy 26 symbols,
|
|
6
|
+
* Minimum 36 symbols, no checksum, XXX-XXX 2,176,782,336 ids
|
|
7
|
+
* Light 31 symbols, 2 checksums, XXXX-XXXX 887,503,681 ids
|
|
8
|
+
* Medium 28 symbols, 2 checksums, XXXX-XXXX 481,890,304 ids (default)
|
|
9
|
+
* Heavy 26 symbols, 2 checksums, XXXX-XXXX 308,915,776 ids
|
|
10
10
|
*
|
|
11
|
-
* All four keep the typed O/I/L aliases where possible
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* All four keep the typed O/I/L aliases where possible, use a hyphen
|
|
12
|
+
* delimiter at the midpoint and run the default profanity blocklist. Every
|
|
13
|
+
* tier permutes with the frozen published key (FROZEN_KEY_BYTES below): the
|
|
14
|
+
* key is public, so the permutation obscures sequence but is not secrecy.
|
|
15
|
+
* The -p variants are identical but permute with caller-supplied key
|
|
16
|
+
* material instead.
|
|
15
17
|
*/
|
|
16
18
|
const OIL_ALIASES = { O: "0", I: "1", L: "1" };
|
|
17
19
|
const MINIMUM_BODY = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
@@ -21,6 +23,13 @@ const HEAVY_BODY = "0123456789ABCEFHJKMPQRVXYZ";
|
|
|
21
23
|
const LIGHT_CHECK = "234679ACEFGHJKMNPQRUVWXY";
|
|
22
24
|
const MEDIUM_CHECK = "234679ACDEFGHJKMPQRUVXY";
|
|
23
25
|
const HEAVY_CHECK = "234679ACEFHJKMPQRUVXY";
|
|
26
|
+
/**
|
|
27
|
+
* The frozen published permutation key. Public by design: it makes issued
|
|
28
|
+
* codes look non-sequential but offers no secrecy, since anyone can read it
|
|
29
|
+
* here. Never swap it on a live namespace; codes only decode with the key
|
|
30
|
+
* they were issued under. Use the -p variants to supply private key material.
|
|
31
|
+
*/
|
|
32
|
+
export const FROZEN_KEY_BYTES = new TextEncoder().encode("baseh-frozen-key-v1");
|
|
24
33
|
function keyedPermutation(options) {
|
|
25
34
|
return {
|
|
26
35
|
enabled: true,
|
|
@@ -33,6 +42,7 @@ function keyedPermutation(options) {
|
|
|
33
42
|
function tier(shape, permutation, pSuffix) {
|
|
34
43
|
return {
|
|
35
44
|
profileId: shape.profileId + (pSuffix ? "-p" : "") + "-v1",
|
|
45
|
+
mode: "fixed",
|
|
36
46
|
bodyAlphabet: shape.bodyAlphabet,
|
|
37
47
|
bodyLength: 6,
|
|
38
48
|
checksumAlphabet: shape.checksumAlphabet,
|
|
@@ -42,7 +52,8 @@ function tier(shape, permutation, pSuffix) {
|
|
|
42
52
|
grouping: shape.grouping,
|
|
43
53
|
aliases: { ...shape.aliases },
|
|
44
54
|
permutation,
|
|
45
|
-
profanity: { mode: "blocklist" }
|
|
55
|
+
profanity: { mode: "blocklist" },
|
|
56
|
+
maxRepetition: 4
|
|
46
57
|
};
|
|
47
58
|
}
|
|
48
59
|
const MINIMUM = {
|
|
@@ -58,18 +69,18 @@ const LIGHT = {
|
|
|
58
69
|
profileId: "baseh-light",
|
|
59
70
|
bodyAlphabet: LIGHT_BODY,
|
|
60
71
|
checksumAlphabet: LIGHT_CHECK,
|
|
61
|
-
checksumLength:
|
|
62
|
-
separator: "",
|
|
63
|
-
grouping: [],
|
|
72
|
+
checksumLength: 2,
|
|
73
|
+
separator: "-",
|
|
74
|
+
grouping: [4, 4],
|
|
64
75
|
aliases: { ...OIL_ALIASES, D: "B", T: "P" }
|
|
65
76
|
};
|
|
66
77
|
const MEDIUM = {
|
|
67
78
|
profileId: "baseh-medium",
|
|
68
79
|
bodyAlphabet: MEDIUM_BODY,
|
|
69
80
|
checksumAlphabet: MEDIUM_CHECK,
|
|
70
|
-
checksumLength:
|
|
71
|
-
separator: "",
|
|
72
|
-
grouping: [],
|
|
81
|
+
checksumLength: 2,
|
|
82
|
+
separator: "-",
|
|
83
|
+
grouping: [4, 4],
|
|
73
84
|
// B and S are dropped for looking like 8 and 5; since they can never be
|
|
74
85
|
// issued, a typed B is always an 8 and a typed S always a 5.
|
|
75
86
|
aliases: { ...OIL_ALIASES, B: "8", S: "5", T: "P", N: "M", W: "V" }
|
|
@@ -78,40 +89,91 @@ const HEAVY = {
|
|
|
78
89
|
profileId: "baseh-heavy",
|
|
79
90
|
bodyAlphabet: HEAVY_BODY,
|
|
80
91
|
checksumAlphabet: HEAVY_CHECK,
|
|
81
|
-
checksumLength:
|
|
82
|
-
separator: "",
|
|
83
|
-
grouping: [],
|
|
92
|
+
checksumLength: 2,
|
|
93
|
+
separator: "-",
|
|
94
|
+
grouping: [4, 4],
|
|
84
95
|
aliases: { ...OIL_ALIASES, D: "B", T: "P", N: "M", W: "V", S: "F", G: "C" }
|
|
85
96
|
};
|
|
97
|
+
/** Permutation every plain tier applies, built from the frozen published key. */
|
|
98
|
+
function frozenPermutation() {
|
|
99
|
+
return keyedPermutation({ keyBytes: FROZEN_KEY_BYTES, keyId: "frozen" });
|
|
100
|
+
}
|
|
86
101
|
/** Alphanumeric, no safety strips, no checksum, hyphen-delimited XXX-XXX. */
|
|
87
102
|
export function basehMinimumV1() {
|
|
88
|
-
return tier(MINIMUM,
|
|
103
|
+
return tier(MINIMUM, frozenPermutation(), false);
|
|
89
104
|
}
|
|
90
|
-
/** baseh-minimum with
|
|
105
|
+
/** baseh-minimum permuted with caller-supplied key material. */
|
|
91
106
|
export function basehMinimumPV1(options) {
|
|
92
107
|
return tier(MINIMUM, keyedPermutation(options), true);
|
|
93
108
|
}
|
|
94
|
-
/** Visual light plus spoken light,
|
|
109
|
+
/** Visual light plus spoken light, two checksum symbols, hyphen-delimited. */
|
|
95
110
|
export function basehLightV1() {
|
|
96
|
-
return tier(LIGHT,
|
|
111
|
+
return tier(LIGHT, frozenPermutation(), false);
|
|
97
112
|
}
|
|
98
|
-
/** baseh-light with
|
|
113
|
+
/** baseh-light permuted with caller-supplied key material. */
|
|
99
114
|
export function basehLightPV1(options) {
|
|
100
115
|
return tier(LIGHT, keyedPermutation(options), true);
|
|
101
116
|
}
|
|
102
|
-
/** Visual medium plus spoken medium,
|
|
117
|
+
/** Visual medium plus spoken medium, two checksum symbols, hyphen-delimited. The default. */
|
|
103
118
|
export function basehMediumV1() {
|
|
104
|
-
return tier(MEDIUM,
|
|
119
|
+
return tier(MEDIUM, frozenPermutation(), false);
|
|
105
120
|
}
|
|
106
|
-
/** baseh-medium with
|
|
121
|
+
/** baseh-medium permuted with caller-supplied key material. */
|
|
107
122
|
export function basehMediumPV1(options) {
|
|
108
123
|
return tier(MEDIUM, keyedPermutation(options), true);
|
|
109
124
|
}
|
|
110
|
-
/** Conservative alphabet plus spoken heavy,
|
|
125
|
+
/** Conservative alphabet plus spoken heavy, two checksum symbols, hyphen-delimited. */
|
|
111
126
|
export function basehHeavyV1() {
|
|
112
|
-
return tier(HEAVY,
|
|
127
|
+
return tier(HEAVY, frozenPermutation(), false);
|
|
128
|
+
}
|
|
129
|
+
// Spec 17.1. The expandable recommended default carries the same safety
|
|
130
|
+
// posture as baseh-medium-v1: the medium visual strips (O, I, L, B, S) and
|
|
131
|
+
// the medium spoken strips (T, N, W), so an issued code never emits a
|
|
132
|
+
// visual or spoken confusable. The zero ban of section 19.2 then removes 0
|
|
133
|
+
// (and O, already gone), leaving a 27-symbol body. The checksum alphabet
|
|
134
|
+
// derives as "0" plus the body (28 symbols).
|
|
135
|
+
const EXPANDABLE_BODY = "123456789ACDEFGHJKMPQRUVXYZ";
|
|
136
|
+
/**
|
|
137
|
+
* Spec 17.1. The frozen expandable tier: four characters while the namespace
|
|
138
|
+
* is small, gaining one symbol automatically as issuance climbs past each
|
|
139
|
+
* generation's capacity. The body alphabet is the medium safety set (the
|
|
140
|
+
* full alphanumeric set minus the visual look-alikes O, I, L, B, S and the
|
|
141
|
+
* spoken-confusable T, N, W), with the zero ban of spec 19.2 removing 0 (27
|
|
142
|
+
* symbols); the checksum alphabet derives as "0" plus the body (28 symbols).
|
|
143
|
+
* The short checksum of spec 22 is on: one checksum symbol through five
|
|
144
|
+
* characters (27^3 = 19,683 ids at length 4, 27^4 = 531,441 at length 5),
|
|
145
|
+
* two from six characters up. The hyphen appears from six characters up,
|
|
146
|
+
* split by the balanced grouping rule of spec 19.5.
|
|
147
|
+
*/
|
|
148
|
+
function expandableTier(permutation, pSuffix) {
|
|
149
|
+
return {
|
|
150
|
+
profileId: "baseh-expandable" + (pSuffix ? "-p" : "") + "-v1",
|
|
151
|
+
mode: "expandable",
|
|
152
|
+
bodyAlphabet: EXPANDABLE_BODY,
|
|
153
|
+
minLength: 4,
|
|
154
|
+
checksumAlphabet: "0" + EXPANDABLE_BODY,
|
|
155
|
+
checksumLength: 2,
|
|
156
|
+
shortChecksumLength: 1,
|
|
157
|
+
shortChecksumUntil: 5,
|
|
158
|
+
caseSensitive: false,
|
|
159
|
+
separator: "-",
|
|
160
|
+
separatorMinLength: 6,
|
|
161
|
+
grouping: [],
|
|
162
|
+
aliases: { ...OIL_ALIASES, B: "8", S: "5", T: "P", N: "M", W: "V" },
|
|
163
|
+
permutation,
|
|
164
|
+
profanity: { mode: "blocklist" },
|
|
165
|
+
maxRepetition: 4
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The frozen expandable tier; the recommended starting point for new namespaces. */
|
|
169
|
+
export function basehExpandableV1() {
|
|
170
|
+
return expandableTier(frozenPermutation(), false);
|
|
171
|
+
}
|
|
172
|
+
/** baseh-expandable permuted with caller-supplied key material. */
|
|
173
|
+
export function basehExpandablePV1(options) {
|
|
174
|
+
return expandableTier(keyedPermutation(options), true);
|
|
113
175
|
}
|
|
114
|
-
/** baseh-heavy with
|
|
176
|
+
/** baseh-heavy permuted with caller-supplied key material. */
|
|
115
177
|
export function basehHeavyPV1(options) {
|
|
116
178
|
return tier(HEAVY, keyedPermutation(options), true);
|
|
117
179
|
}
|
package/package.json
CHANGED
|
@@ -1,33 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudyventures/baseh",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "baseH: reversible, checksummed, human-safe short references for integers.",
|
|
5
5
|
"repository": "github:cloudyventures/baseh",
|
|
6
|
+
"homepage": "https://github.com/cloudyventures/baseh#readme",
|
|
7
|
+
"bugs": "https://github.com/cloudyventures/baseh/issues",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"baseh",
|
|
10
|
+
"encoding",
|
|
11
|
+
"checksum",
|
|
12
|
+
"human-readable",
|
|
13
|
+
"identifier",
|
|
14
|
+
"short-id",
|
|
15
|
+
"reference-code"
|
|
16
|
+
],
|
|
6
17
|
"type": "module",
|
|
7
|
-
"main": "./dist/index.js",
|
|
18
|
+
"main": "./dist/cjs/index.js",
|
|
19
|
+
"module": "./dist/index.js",
|
|
8
20
|
"types": "./dist/index.d.ts",
|
|
9
21
|
"exports": {
|
|
10
22
|
".": {
|
|
11
|
-
"
|
|
12
|
-
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/cjs/index.d.ts",
|
|
29
|
+
"default": "./dist/cjs/index.js"
|
|
30
|
+
}
|
|
13
31
|
}
|
|
14
32
|
},
|
|
15
33
|
"files": [
|
|
16
34
|
"dist"
|
|
17
35
|
],
|
|
18
36
|
"scripts": {
|
|
19
|
-
"build": "
|
|
37
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
38
|
+
"build:esm": "tsc -p tsconfig.build.json",
|
|
39
|
+
"build:cjs": "tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }) + '\\n')\"",
|
|
20
40
|
"test": "tsx --test test/*.test.ts",
|
|
21
|
-
"
|
|
41
|
+
"lint": "eslint src test",
|
|
42
|
+
"vectors": "tsx scripts/generate-vectors.ts",
|
|
43
|
+
"bench": "tsx scripts/bench.ts",
|
|
44
|
+
"prepack": "npm run build"
|
|
22
45
|
},
|
|
23
46
|
"dependencies": {
|
|
24
47
|
"@noble/hashes": "^1.8.0"
|
|
25
48
|
},
|
|
26
49
|
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^10.0.1",
|
|
27
51
|
"@types/node": "^24.0.0",
|
|
52
|
+
"eslint": "^10.8.0",
|
|
28
53
|
"fast-check": "^4.0.0",
|
|
29
54
|
"tsx": "^4.19.0",
|
|
30
|
-
"typescript": "^5.6.0"
|
|
55
|
+
"typescript": "^5.6.0",
|
|
56
|
+
"typescript-eslint": "^8.65.0"
|
|
31
57
|
},
|
|
32
58
|
"license": "AGPL-3.0-only"
|
|
33
59
|
}
|
package/dist/zero.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** Encode an identifier with the zero-config Medium profile. */
|
|
2
|
-
export declare function toCode(id: bigint | number | string): string;
|
|
3
|
-
/** Decode a code from the zero-config Medium profile back to its identifier. */
|
|
4
|
-
export declare function fromCode(code: string): bigint;
|
package/dist/zero.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Baseh } from "./codec.js";
|
|
2
|
-
import { basehMediumV1 } from "./profiles.js";
|
|
3
|
-
/**
|
|
4
|
-
* Zero-config pair over the frozen baseh-medium-v1 profile. No profile
|
|
5
|
-
* object, no key: just the two functions an application needs when it
|
|
6
|
-
* does not want to think about configuration.
|
|
7
|
-
*
|
|
8
|
-
* toCode(id) -> "7KM4Q2H"
|
|
9
|
-
* fromCode(code) -> id
|
|
10
|
-
*
|
|
11
|
-
* toCode accepts a bigint, a safe integer number or a decimal string.
|
|
12
|
-
* fromCode strips every whitespace character (edges and internal),
|
|
13
|
-
* accepts lowercase and the typed aliases (O, I, L) and returns the id
|
|
14
|
-
* as a bigint. Any invalid input throws BasehError, including the rare
|
|
15
|
-
* BLOCKED_CODE identifiers that spell a blocklisted word; no correction
|
|
16
|
-
* attempts are ever made.
|
|
17
|
-
*/
|
|
18
|
-
const ZERO = new Baseh(basehMediumV1());
|
|
19
|
-
function toBigInt(id) {
|
|
20
|
-
if (typeof id === "bigint")
|
|
21
|
-
return id;
|
|
22
|
-
if (typeof id === "number") {
|
|
23
|
-
if (!Number.isSafeInteger(id) || id < 0) {
|
|
24
|
-
throw new TypeError("toCode expects a non-negative safe integer, bigint or decimal string");
|
|
25
|
-
}
|
|
26
|
-
return BigInt(id);
|
|
27
|
-
}
|
|
28
|
-
if (typeof id === "string" && /^[0-9]+$/.test(id))
|
|
29
|
-
return BigInt(id);
|
|
30
|
-
throw new TypeError("toCode expects a non-negative safe integer, bigint or decimal string");
|
|
31
|
-
}
|
|
32
|
-
/** Encode an identifier with the zero-config Medium profile. */
|
|
33
|
-
export function toCode(id) {
|
|
34
|
-
return ZERO.encode(toBigInt(id));
|
|
35
|
-
}
|
|
36
|
-
/** Decode a code from the zero-config Medium profile back to its identifier. */
|
|
37
|
-
export function fromCode(code) {
|
|
38
|
-
return ZERO.decode(code.replace(/\s+/g, "")).id;
|
|
39
|
-
}
|