@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.
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripVowels = exports.effectiveBlocklist = exports.DEFAULT_BLOCKLIST = exports.inspect = exports.validate = exports.decode = exports.encode = exports.expandableGrouping = exports.generationForId = exports.generationCapacity = exports.generationBase = exports.FROZEN_KEY_BYTES = exports.basehExpandablePV1 = exports.basehExpandableV1 = exports.basehHeavyPV1 = exports.basehMediumPV1 = exports.basehLightPV1 = exports.basehMinimumPV1 = exports.basehHeavyV1 = exports.basehMediumV1 = exports.basehLightV1 = exports.basehMinimumV1 = exports.inversePermute = exports.permute = exports.checksumValue = exports.calculateChecksum = exports.alphabetIndex = exports.decodeBaseN = exports.encodeBaseN = exports.CONFUSION_MAPS = exports.generateCandidates = exports.formatRaw = exports.normalize = exports.Baseh = exports.effectiveChecksumLength = exports.prepareProfile = exports.BasehError = void 0;
4
+ var errors_js_1 = require("./errors.js");
5
+ Object.defineProperty(exports, "BasehError", { enumerable: true, get: function () { return errors_js_1.BasehError; } });
6
+ var profile_js_1 = require("./profile.js");
7
+ Object.defineProperty(exports, "prepareProfile", { enumerable: true, get: function () { return profile_js_1.prepareProfile; } });
8
+ Object.defineProperty(exports, "effectiveChecksumLength", { enumerable: true, get: function () { return profile_js_1.effectiveChecksumLength; } });
9
+ var codec_js_1 = require("./codec.js");
10
+ Object.defineProperty(exports, "Baseh", { enumerable: true, get: function () { return codec_js_1.Baseh; } });
11
+ Object.defineProperty(exports, "normalize", { enumerable: true, get: function () { return codec_js_1.normalize; } });
12
+ Object.defineProperty(exports, "formatRaw", { enumerable: true, get: function () { return codec_js_1.formatRaw; } });
13
+ Object.defineProperty(exports, "generateCandidates", { enumerable: true, get: function () { return codec_js_1.generateCandidates; } });
14
+ Object.defineProperty(exports, "CONFUSION_MAPS", { enumerable: true, get: function () { return codec_js_1.CONFUSION_MAPS; } });
15
+ var basen_js_1 = require("./basen.js");
16
+ Object.defineProperty(exports, "encodeBaseN", { enumerable: true, get: function () { return basen_js_1.encodeBaseN; } });
17
+ Object.defineProperty(exports, "decodeBaseN", { enumerable: true, get: function () { return basen_js_1.decodeBaseN; } });
18
+ Object.defineProperty(exports, "alphabetIndex", { enumerable: true, get: function () { return basen_js_1.alphabetIndex; } });
19
+ var checksum_js_1 = require("./checksum.js");
20
+ Object.defineProperty(exports, "calculateChecksum", { enumerable: true, get: function () { return checksum_js_1.calculateChecksum; } });
21
+ Object.defineProperty(exports, "checksumValue", { enumerable: true, get: function () { return checksum_js_1.checksumValue; } });
22
+ var feistel_js_1 = require("./feistel.js");
23
+ Object.defineProperty(exports, "permute", { enumerable: true, get: function () { return feistel_js_1.permute; } });
24
+ Object.defineProperty(exports, "inversePermute", { enumerable: true, get: function () { return feistel_js_1.inversePermute; } });
25
+ var profiles_js_1 = require("./profiles.js");
26
+ Object.defineProperty(exports, "basehMinimumV1", { enumerable: true, get: function () { return profiles_js_1.basehMinimumV1; } });
27
+ Object.defineProperty(exports, "basehLightV1", { enumerable: true, get: function () { return profiles_js_1.basehLightV1; } });
28
+ Object.defineProperty(exports, "basehMediumV1", { enumerable: true, get: function () { return profiles_js_1.basehMediumV1; } });
29
+ Object.defineProperty(exports, "basehHeavyV1", { enumerable: true, get: function () { return profiles_js_1.basehHeavyV1; } });
30
+ Object.defineProperty(exports, "basehMinimumPV1", { enumerable: true, get: function () { return profiles_js_1.basehMinimumPV1; } });
31
+ Object.defineProperty(exports, "basehLightPV1", { enumerable: true, get: function () { return profiles_js_1.basehLightPV1; } });
32
+ Object.defineProperty(exports, "basehMediumPV1", { enumerable: true, get: function () { return profiles_js_1.basehMediumPV1; } });
33
+ Object.defineProperty(exports, "basehHeavyPV1", { enumerable: true, get: function () { return profiles_js_1.basehHeavyPV1; } });
34
+ Object.defineProperty(exports, "basehExpandableV1", { enumerable: true, get: function () { return profiles_js_1.basehExpandableV1; } });
35
+ Object.defineProperty(exports, "basehExpandablePV1", { enumerable: true, get: function () { return profiles_js_1.basehExpandablePV1; } });
36
+ Object.defineProperty(exports, "FROZEN_KEY_BYTES", { enumerable: true, get: function () { return profiles_js_1.FROZEN_KEY_BYTES; } });
37
+ var codec_js_2 = require("./codec.js");
38
+ Object.defineProperty(exports, "generationBase", { enumerable: true, get: function () { return codec_js_2.generationBase; } });
39
+ Object.defineProperty(exports, "generationCapacity", { enumerable: true, get: function () { return codec_js_2.generationCapacity; } });
40
+ Object.defineProperty(exports, "generationForId", { enumerable: true, get: function () { return codec_js_2.generationForId; } });
41
+ Object.defineProperty(exports, "expandableGrouping", { enumerable: true, get: function () { return codec_js_2.expandableGrouping; } });
42
+ var facade_js_1 = require("./facade.js");
43
+ Object.defineProperty(exports, "encode", { enumerable: true, get: function () { return facade_js_1.encode; } });
44
+ Object.defineProperty(exports, "decode", { enumerable: true, get: function () { return facade_js_1.decode; } });
45
+ Object.defineProperty(exports, "validate", { enumerable: true, get: function () { return facade_js_1.validate; } });
46
+ Object.defineProperty(exports, "inspect", { enumerable: true, get: function () { return facade_js_1.inspect; } });
47
+ var blocklist_js_1 = require("./blocklist.js");
48
+ Object.defineProperty(exports, "DEFAULT_BLOCKLIST", { enumerable: true, get: function () { return blocklist_js_1.DEFAULT_BLOCKLIST; } });
49
+ Object.defineProperty(exports, "effectiveBlocklist", { enumerable: true, get: function () { return blocklist_js_1.effectiveBlocklist; } });
50
+ Object.defineProperty(exports, "stripVowels", { enumerable: true, get: function () { return blocklist_js_1.stripVowels; } });
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,91 @@
1
+ import { type BasehProfanity } from "./blocklist.js";
2
+ export type BasehPermutation = {
3
+ enabled: false;
4
+ } | {
5
+ enabled: true;
6
+ algorithm: "feistel-v1";
7
+ keyId: string;
8
+ keyBytes: Uint8Array;
9
+ rounds: number;
10
+ };
11
+ export interface BasehProfile {
12
+ profileId: string;
13
+ /**
14
+ * Spec 2.1/19.9. "fixed" keeps the classic constant-width behaviour;
15
+ * "expandable" gives variable-length codes driven by id magnitude
16
+ * (spec 19). Profiles that predate the mode field are fixed: the shared
17
+ * frozen vectors pin their byte-for-byte behaviour, so a missing mode is
18
+ * prepared as "fixed".
19
+ */
20
+ mode?: "fixed" | "expandable";
21
+ bodyAlphabet: string;
22
+ /** Fixed mode only; ignored in expandable mode. */
23
+ bodyLength?: number;
24
+ /** Expandable mode only; default 4. Must exceed checksumLength. */
25
+ minLength?: number;
26
+ checksumAlphabet: string;
27
+ checksumLength: number;
28
+ /**
29
+ * Spec 22. Expandable mode only. The checksum width used by generations at
30
+ * or below `shortChecksumUntil`; may be 0 (a zero-checksum window: those
31
+ * generations carry no checksum symbols and no typo detection). Without a
32
+ * window (`shortChecksumUntil` absent or 0) this must be absent or 0.
33
+ */
34
+ shortChecksumLength?: number;
35
+ /**
36
+ * Spec 22. The last generation (total length) that uses the short
37
+ * checksum; 0 or absent turns the feature off (the codebase convention,
38
+ * like maxRepetition). When set it must be an integer from `minLength`
39
+ * through 8.
40
+ */
41
+ shortChecksumUntil?: number;
42
+ caseSensitive: boolean;
43
+ separator: string;
44
+ /** Expandable mode only; default 0 (separator always applies). */
45
+ separatorMinLength?: number;
46
+ grouping: number[];
47
+ aliases: Record<string, string>;
48
+ permutation: BasehPermutation;
49
+ /** Spec 18. Defaults to mode "none". */
50
+ profanity?: BasehProfanity;
51
+ /**
52
+ * Spec 21. Maximum allowed run of the same symbol in a raw code. 0 (the
53
+ * default) disables the filter; otherwise it must be an integer of at
54
+ * least 3. A value above the code length is a legal no-op.
55
+ */
56
+ maxRepetition?: number;
57
+ }
58
+ /** Case-prepared derived data, computed once at construction. */
59
+ export interface PreparedProfile extends BasehProfile {
60
+ readonly mode: "fixed" | "expandable";
61
+ readonly minLength: number;
62
+ readonly separatorMinLength: number;
63
+ readonly bodyAlphabetNorm: string;
64
+ readonly checksumAlphabetNorm: string;
65
+ readonly aliasesNorm: Record<string, string>;
66
+ readonly checksumModulus: bigint;
67
+ /** Fixed-mode capacity A^bodyLength. Meaningless in expandable mode. */
68
+ readonly capacity: bigint;
69
+ /** Spec 18. Empty unless the profile uses mode "blocklist". */
70
+ readonly blocklist: string[];
71
+ /** Spec 21. 0 disables the repetition filter. */
72
+ readonly maxRepetition: number;
73
+ /** Spec 22. 0 disables the short checksum. */
74
+ readonly shortChecksumLength: number;
75
+ /** Spec 22. Last short-checksum generation; 0 when the feature is off. */
76
+ readonly shortChecksumUntil: number;
77
+ }
78
+ /**
79
+ * Spec 22. The checksum length that applies to a generation of the given
80
+ * total length: `shortChecksumLength` at or below `shortChecksumUntil`,
81
+ * `checksumLength` above it (and always in fixed mode). The feature is on
82
+ * exactly when `shortChecksumUntil` is non-zero; a `shortChecksumLength` of
83
+ * 0 then means the window's generations carry no checksum symbols at all.
84
+ */
85
+ export declare function effectiveChecksumLength(profile: PreparedProfile, length: number): number;
86
+ /**
87
+ * Validates a profile per spec section 2.2 and returns it with derived,
88
+ * pre-computed values. Throws BasehError INVALID_PROFILE on any violation.
89
+ * Call once at construction, never per encode/decode.
90
+ */
91
+ export declare function prepareProfile(profile: BasehProfile): PreparedProfile;
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.effectiveChecksumLength = effectiveChecksumLength;
4
+ exports.prepareProfile = prepareProfile;
5
+ const errors_js_1 = require("./errors.js");
6
+ const basen_js_1 = require("./basen.js");
7
+ const blocklist_js_1 = require("./blocklist.js");
8
+ /**
9
+ * Spec 22. The checksum length that applies to a generation of the given
10
+ * total length: `shortChecksumLength` at or below `shortChecksumUntil`,
11
+ * `checksumLength` above it (and always in fixed mode). The feature is on
12
+ * exactly when `shortChecksumUntil` is non-zero; a `shortChecksumLength` of
13
+ * 0 then means the window's generations carry no checksum symbols at all.
14
+ */
15
+ function effectiveChecksumLength(profile, length) {
16
+ if (profile.mode === "expandable" && profile.shortChecksumUntil > 0 && length <= profile.shortChecksumUntil) {
17
+ return profile.shortChecksumLength;
18
+ }
19
+ return profile.checksumLength;
20
+ }
21
+ const ASCII_ONLY = /^[\x20-\x7e]*$/;
22
+ function fail(reason) {
23
+ throw new errors_js_1.BasehError("INVALID_PROFILE", `Invalid baseH profile: ${reason}`, false);
24
+ }
25
+ function isAsciiChar(ch) {
26
+ return ch.length === 1 && ASCII_ONLY.test(ch);
27
+ }
28
+ function norm(profile, ch) {
29
+ return profile.caseSensitive ? ch : ch.toUpperCase();
30
+ }
31
+ /**
32
+ * Validates a profile per spec section 2.2 and returns it with derived,
33
+ * pre-computed values. Throws BasehError INVALID_PROFILE on any violation.
34
+ * Call once at construction, never per encode/decode.
35
+ */
36
+ function prepareProfile(profile) {
37
+ if (!profile || typeof profile !== "object")
38
+ fail("profile is required");
39
+ if (typeof profile.profileId !== "string" || profile.profileId.length === 0) {
40
+ fail("profileId must be non-empty");
41
+ }
42
+ if (!ASCII_ONLY.test(profile.profileId))
43
+ fail("profileId must be ASCII");
44
+ // Spec 2.2/19.9. A persisted or frozen profile declares its mode; profiles
45
+ // built before the mode field existed are fixed, so the frozen vectors keep
46
+ // matching byte for byte.
47
+ const mode = profile.mode ?? "fixed";
48
+ if (mode !== "fixed" && mode !== "expandable")
49
+ fail("mode must be fixed or expandable");
50
+ const caseSensitive = profile.caseSensitive === true;
51
+ const bodyAlphabet = profile.bodyAlphabet;
52
+ if (typeof bodyAlphabet !== "string" || bodyAlphabet.length < 2) {
53
+ fail("bodyAlphabet needs at least two symbols");
54
+ }
55
+ for (const ch of bodyAlphabet) {
56
+ if (!isAsciiChar(ch))
57
+ fail(`body alphabet symbol is not single ASCII: ${JSON.stringify(ch)}`);
58
+ }
59
+ const view = { caseSensitive };
60
+ let bodyNorm = [...bodyAlphabet].map((c) => norm(view, c)).join("");
61
+ // Spec 19.2: in expandable mode the zero ban strips 0 and O from the body
62
+ // alphabet silently, before any other validation, exactly like the
63
+ // no-vowels strip of section 18.1.
64
+ if (mode === "expandable") {
65
+ bodyNorm = [...bodyNorm].filter((c) => c !== "0" && c !== "O").join("");
66
+ }
67
+ if (new Set(bodyNorm).size !== bodyNorm.length) {
68
+ fail("body alphabet symbols must be unique after case normalization");
69
+ }
70
+ if (mode === "fixed") {
71
+ if (!Number.isInteger(profile.bodyLength) ||
72
+ profile.bodyLength < 1 ||
73
+ profile.bodyLength > 32) {
74
+ fail("bodyLength must be an integer from 1 through 32");
75
+ }
76
+ }
77
+ const minLength = profile.minLength ?? 4;
78
+ const separatorMinLength = profile.separatorMinLength ?? 0;
79
+ if (mode === "fixed" && separatorMinLength !== 0) {
80
+ fail("separatorMinLength must be 0 in fixed mode");
81
+ }
82
+ if (!Number.isInteger(profile.checksumLength) ||
83
+ profile.checksumLength < 0 ||
84
+ profile.checksumLength > 8) {
85
+ fail("checksumLength must be an integer from 0 through 8");
86
+ }
87
+ if (mode === "expandable") {
88
+ if (!Number.isInteger(minLength) || minLength < 1) {
89
+ fail("minLength must be an integer of at least 1");
90
+ }
91
+ if (minLength <= profile.checksumLength) {
92
+ fail("minLength must be greater than checksumLength");
93
+ }
94
+ if (!Number.isInteger(separatorMinLength) || separatorMinLength < 0) {
95
+ fail("separatorMinLength must be an integer of at least 0");
96
+ }
97
+ }
98
+ // Spec 22. The short checksum is expandable-only. The window field is the
99
+ // switch: `shortChecksumUntil` of 0 or absent turns the feature off (the
100
+ // codebase convention, like maxRepetition), and the length field without
101
+ // a window is INVALID_PROFILE.
102
+ const shortChecksumLength = profile.shortChecksumLength ?? 0;
103
+ const shortChecksumUntil = profile.shortChecksumUntil ?? 0;
104
+ if (mode === "fixed") {
105
+ if (shortChecksumLength !== 0 || shortChecksumUntil !== 0) {
106
+ fail("shortChecksumLength and shortChecksumUntil are expandable-mode only");
107
+ }
108
+ }
109
+ else if (shortChecksumUntil !== 0) {
110
+ if (!Number.isInteger(profile.shortChecksumUntil) || shortChecksumUntil < minLength) {
111
+ fail("shortChecksumUntil must be an integer of at least minLength");
112
+ }
113
+ // Beyond 8 the window would swallow nearly every practical code, and
114
+ // long codes genuinely want two checksum symbols.
115
+ if (shortChecksumUntil > 8) {
116
+ fail("shortChecksumUntil must be at most 8");
117
+ }
118
+ if (!Number.isInteger(shortChecksumLength) ||
119
+ shortChecksumLength < 0 ||
120
+ shortChecksumLength >= profile.checksumLength) {
121
+ fail("shortChecksumLength must be an integer from 0 through checksumLength - 1");
122
+ }
123
+ if (minLength <= shortChecksumLength) {
124
+ fail("minLength must be greater than shortChecksumLength");
125
+ }
126
+ }
127
+ else if (shortChecksumLength !== 0) {
128
+ fail("shortChecksumLength requires shortChecksumUntil");
129
+ }
130
+ const checksumAlphabet = profile.checksumAlphabet ?? "";
131
+ let checksumNorm = [...checksumAlphabet].map((c) => norm(view, c)).join("");
132
+ if (mode === "expandable") {
133
+ // Spec 19.3: the checksum alphabet is derived, "0" followed by the body
134
+ // alphabet in order. The configured checksumAlphabet is not consulted.
135
+ checksumNorm = "";
136
+ }
137
+ else if (profile.checksumLength > 0) {
138
+ if (typeof checksumAlphabet !== "string" || checksumAlphabet.length < 2) {
139
+ fail("checksumAlphabet needs at least two symbols when checksumLength is positive");
140
+ }
141
+ for (const ch of checksumAlphabet) {
142
+ if (!isAsciiChar(ch))
143
+ fail(`checksum alphabet symbol is not single ASCII: ${JSON.stringify(ch)}`);
144
+ }
145
+ if (new Set(checksumNorm).size !== checksumNorm.length) {
146
+ fail("checksum alphabet symbols must be unique after case normalization");
147
+ }
148
+ }
149
+ // Spec 18. no-vowels strips vowels before every downstream rule; blocklist
150
+ // only arms the encode-time scan.
151
+ const profanity = profile.profanity ?? { mode: "none" };
152
+ if (!["none", "no-vowels", "blocklist"].includes(profanity.mode)) {
153
+ fail("profanity mode must be none, no-vowels or blocklist");
154
+ }
155
+ if (profanity.mode === "no-vowels") {
156
+ bodyNorm = (0, blocklist_js_1.stripVowels)(bodyNorm);
157
+ checksumNorm = (0, blocklist_js_1.stripVowels)(checksumNorm);
158
+ if (bodyNorm.length < 2) {
159
+ fail("no-vowels mode leaves the body alphabet with fewer than two symbols");
160
+ }
161
+ if (mode === "fixed" && profile.checksumLength > 0 && checksumNorm.length < 2) {
162
+ fail("no-vowels mode leaves the checksum alphabet with fewer than two symbols");
163
+ }
164
+ }
165
+ if (mode === "expandable") {
166
+ // Derived after every body strip (zero ban, no-vowels) so all downstream
167
+ // rules — modulus, separator collision, alias targets — see the final
168
+ // alphabets.
169
+ checksumNorm = "0" + bodyNorm;
170
+ }
171
+ if (bodyNorm.length < 2) {
172
+ fail("body alphabet needs at least two symbols after preparation");
173
+ }
174
+ const blocklist = profanity.mode === "blocklist" ? (0, blocklist_js_1.effectiveBlocklist)(profanity) : [];
175
+ // Spec 21: 0 disables the filter; an active filter needs a floor of 3 —
176
+ // banning pairs (2) would destroy roughly 9% of every generation.
177
+ const maxRepetition = profile.maxRepetition ?? 0;
178
+ if (!Number.isInteger(maxRepetition) || maxRepetition < 0 || (maxRepetition > 0 && maxRepetition < 3)) {
179
+ fail("maxRepetition must be 0 (off) or an integer of at least 3");
180
+ }
181
+ const separator = profile.separator ?? "";
182
+ for (const ch of separator) {
183
+ if (bodyNorm.includes(ch) || checksumNorm.includes(ch)) {
184
+ fail("separator must not occur in either alphabet");
185
+ }
186
+ }
187
+ const aliases = profile.aliases ?? {};
188
+ const aliasesNorm = {};
189
+ const canonicalSet = new Set([...bodyNorm, ...checksumNorm]);
190
+ for (const [src, tgt] of Object.entries(aliases)) {
191
+ if (!isAsciiChar(src))
192
+ fail(`alias source is not single ASCII: ${JSON.stringify(src)}`);
193
+ if (!isAsciiChar(tgt))
194
+ fail(`alias target is not single ASCII: ${JSON.stringify(tgt)}`);
195
+ const sNorm = norm(view, src);
196
+ const tNorm = norm(view, tgt);
197
+ if (!canonicalSet.has(tNorm)) {
198
+ fail(`alias target ${JSON.stringify(tgt)} is not a canonical symbol`);
199
+ }
200
+ // Spec 3.2: an alias must never map two distinct canonical symbols into
201
+ // one value. Fixed mode rejects a canonical alias source outright. In
202
+ // expandable mode the frozen tier (spec 17.1) carries aliases whose
203
+ // sources are canonical body symbols (T, N, W stay in the body
204
+ // alphabet); the canonical symbol wins at normalization, making those
205
+ // entries inert instead of destructive.
206
+ if (mode === "fixed" && canonicalSet.has(sNorm)) {
207
+ fail(`alias source ${JSON.stringify(src)} is already a canonical symbol`);
208
+ }
209
+ if (sNorm in aliasesNorm)
210
+ fail(`duplicate alias source ${JSON.stringify(sNorm)} after case normalization`);
211
+ if (tNorm in aliases || [...Object.keys(aliases)].some((k) => norm(view, k) === tNorm)) {
212
+ fail(`alias chain forbidden: target ${tNorm} is also an alias source`);
213
+ }
214
+ aliasesNorm[sNorm] = tNorm;
215
+ }
216
+ const total = bodySum(profile.grouping);
217
+ if (separator.length === 0) {
218
+ if (profile.grouping.length !== 0)
219
+ fail("grouping must be empty when separator is empty");
220
+ }
221
+ else if (mode === "expandable") {
222
+ // Spec 19.5: the balanced grouping rule is a pure function of the total
223
+ // length, so a configurable grouping is meaningless in expandable mode.
224
+ if (profile.grouping.length !== 0)
225
+ fail("grouping must be empty in expandable mode");
226
+ }
227
+ else if (total !== profile.bodyLength + profile.checksumLength) {
228
+ fail("group sizes must sum to bodyLength + checksumLength");
229
+ }
230
+ const permutation = profile.permutation ?? { enabled: false };
231
+ if (permutation.enabled) {
232
+ if (permutation.algorithm !== "feistel-v1")
233
+ fail("unknown permutation algorithm");
234
+ if (typeof permutation.keyId !== "string" || permutation.keyId.length === 0) {
235
+ fail("permutation requires a keyId");
236
+ }
237
+ if (!(permutation.keyBytes instanceof Uint8Array) || permutation.keyBytes.length === 0) {
238
+ fail("permutation requires key material");
239
+ }
240
+ if (!Number.isInteger(permutation.rounds) ||
241
+ permutation.rounds < 4 ||
242
+ permutation.rounds > 16 ||
243
+ permutation.rounds % 2 !== 0) {
244
+ fail("Feistel rounds must be an even integer from 4 through 16");
245
+ }
246
+ }
247
+ return {
248
+ ...profile,
249
+ mode,
250
+ minLength,
251
+ separatorMinLength,
252
+ caseSensitive,
253
+ checksumAlphabet,
254
+ separator,
255
+ grouping: [...profile.grouping],
256
+ aliases: { ...aliases },
257
+ permutation,
258
+ bodyAlphabetNorm: bodyNorm,
259
+ checksumAlphabetNorm: checksumNorm,
260
+ aliasesNorm,
261
+ checksumModulus: (0, basen_js_1.powBigInt)(BigInt(checksumNorm.length || 1), profile.checksumLength),
262
+ capacity: (0, basen_js_1.powBigInt)(BigInt(bodyNorm.length), profile.bodyLength ?? 0),
263
+ blocklist,
264
+ maxRepetition,
265
+ shortChecksumLength,
266
+ shortChecksumUntil
267
+ };
268
+ }
269
+ function bodySum(grouping) {
270
+ if (!Array.isArray(grouping))
271
+ return -1;
272
+ let sum = 0;
273
+ for (const g of grouping) {
274
+ if (!Number.isInteger(g) || g < 1)
275
+ return -1;
276
+ sum += g;
277
+ }
278
+ return sum;
279
+ }
@@ -0,0 +1,33 @@
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;
9
+ export interface FrozenKeyOptions {
10
+ keyBytes: Uint8Array;
11
+ keyId?: string;
12
+ rounds?: number;
13
+ }
14
+ /** Alphanumeric, no safety strips, no checksum, hyphen-delimited XXX-XXX. */
15
+ export declare function basehMinimumV1(): BasehProfile;
16
+ /** baseh-minimum permuted with caller-supplied key material. */
17
+ export declare function basehMinimumPV1(options: FrozenKeyOptions): BasehProfile;
18
+ /** Visual light plus spoken light, two checksum symbols, hyphen-delimited. */
19
+ export declare function basehLightV1(): BasehProfile;
20
+ /** baseh-light permuted with caller-supplied key material. */
21
+ export declare function basehLightPV1(options: FrozenKeyOptions): BasehProfile;
22
+ /** Visual medium plus spoken medium, two checksum symbols, hyphen-delimited. The default. */
23
+ export declare function basehMediumV1(): BasehProfile;
24
+ /** baseh-medium permuted with caller-supplied key material. */
25
+ export declare function basehMediumPV1(options: FrozenKeyOptions): BasehProfile;
26
+ /** Conservative alphabet plus spoken heavy, two checksum symbols, hyphen-delimited. */
27
+ export declare function basehHeavyV1(): BasehProfile;
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. */
33
+ export declare function basehHeavyPV1(options: FrozenKeyOptions): BasehProfile;
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FROZEN_KEY_BYTES = void 0;
4
+ exports.basehMinimumV1 = basehMinimumV1;
5
+ exports.basehMinimumPV1 = basehMinimumPV1;
6
+ exports.basehLightV1 = basehLightV1;
7
+ exports.basehLightPV1 = basehLightPV1;
8
+ exports.basehMediumV1 = basehMediumV1;
9
+ exports.basehMediumPV1 = basehMediumPV1;
10
+ exports.basehHeavyV1 = basehHeavyV1;
11
+ exports.basehExpandableV1 = basehExpandableV1;
12
+ exports.basehExpandablePV1 = basehExpandablePV1;
13
+ exports.basehHeavyPV1 = basehHeavyPV1;
14
+ /**
15
+ * Frozen tiers. Each is built from the full alphanumeric set with cumulative
16
+ * visual and spoken strips; the spoken strips interact with the visual ones
17
+ * exactly as the web tools derive them, so the tool capacities match.
18
+ *
19
+ * Minimum 36 symbols, no checksum, XXX-XXX 2,176,782,336 ids
20
+ * Light 31 symbols, 2 checksums, XXXX-XXXX 887,503,681 ids
21
+ * Medium 28 symbols, 2 checksums, XXXX-XXXX 481,890,304 ids (default)
22
+ * Heavy 26 symbols, 2 checksums, XXXX-XXXX 308,915,776 ids
23
+ *
24
+ * All four keep the typed O/I/L aliases where possible, use a hyphen
25
+ * delimiter at the midpoint and run the default profanity blocklist. Every
26
+ * tier permutes with the frozen published key (FROZEN_KEY_BYTES below): the
27
+ * key is public, so the permutation obscures sequence but is not secrecy.
28
+ * The -p variants are identical but permute with caller-supplied key
29
+ * material instead.
30
+ */
31
+ const OIL_ALIASES = { O: "0", I: "1", L: "1" };
32
+ const MINIMUM_BODY = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
33
+ const LIGHT_BODY = "0123456789ABCEFGHJKMNPQRSUVWXYZ";
34
+ const MEDIUM_BODY = "0123456789ACDEFGHJKMPQRUVXYZ";
35
+ const HEAVY_BODY = "0123456789ABCEFHJKMPQRVXYZ";
36
+ const LIGHT_CHECK = "234679ACEFGHJKMNPQRUVWXY";
37
+ const MEDIUM_CHECK = "234679ACDEFGHJKMPQRUVXY";
38
+ const HEAVY_CHECK = "234679ACEFHJKMPQRUVXY";
39
+ /**
40
+ * The frozen published permutation key. Public by design: it makes issued
41
+ * codes look non-sequential but offers no secrecy, since anyone can read it
42
+ * here. Never swap it on a live namespace; codes only decode with the key
43
+ * they were issued under. Use the -p variants to supply private key material.
44
+ */
45
+ exports.FROZEN_KEY_BYTES = new TextEncoder().encode("baseh-frozen-key-v1");
46
+ function keyedPermutation(options) {
47
+ return {
48
+ enabled: true,
49
+ algorithm: "feistel-v1",
50
+ keyId: options.keyId ?? "default",
51
+ keyBytes: options.keyBytes,
52
+ rounds: options.rounds ?? 8
53
+ };
54
+ }
55
+ function tier(shape, permutation, pSuffix) {
56
+ return {
57
+ profileId: shape.profileId + (pSuffix ? "-p" : "") + "-v1",
58
+ mode: "fixed",
59
+ bodyAlphabet: shape.bodyAlphabet,
60
+ bodyLength: 6,
61
+ checksumAlphabet: shape.checksumAlphabet,
62
+ checksumLength: shape.checksumLength,
63
+ caseSensitive: false,
64
+ separator: shape.separator,
65
+ grouping: shape.grouping,
66
+ aliases: { ...shape.aliases },
67
+ permutation,
68
+ profanity: { mode: "blocklist" },
69
+ maxRepetition: 4
70
+ };
71
+ }
72
+ const MINIMUM = {
73
+ profileId: "baseh-minimum",
74
+ bodyAlphabet: MINIMUM_BODY,
75
+ checksumAlphabet: "",
76
+ checksumLength: 0,
77
+ separator: "-",
78
+ grouping: [3, 3],
79
+ aliases: {}
80
+ };
81
+ const LIGHT = {
82
+ profileId: "baseh-light",
83
+ bodyAlphabet: LIGHT_BODY,
84
+ checksumAlphabet: LIGHT_CHECK,
85
+ checksumLength: 2,
86
+ separator: "-",
87
+ grouping: [4, 4],
88
+ aliases: { ...OIL_ALIASES, D: "B", T: "P" }
89
+ };
90
+ const MEDIUM = {
91
+ profileId: "baseh-medium",
92
+ bodyAlphabet: MEDIUM_BODY,
93
+ checksumAlphabet: MEDIUM_CHECK,
94
+ checksumLength: 2,
95
+ separator: "-",
96
+ grouping: [4, 4],
97
+ // B and S are dropped for looking like 8 and 5; since they can never be
98
+ // issued, a typed B is always an 8 and a typed S always a 5.
99
+ aliases: { ...OIL_ALIASES, B: "8", S: "5", T: "P", N: "M", W: "V" }
100
+ };
101
+ const HEAVY = {
102
+ profileId: "baseh-heavy",
103
+ bodyAlphabet: HEAVY_BODY,
104
+ checksumAlphabet: HEAVY_CHECK,
105
+ checksumLength: 2,
106
+ separator: "-",
107
+ grouping: [4, 4],
108
+ aliases: { ...OIL_ALIASES, D: "B", T: "P", N: "M", W: "V", S: "F", G: "C" }
109
+ };
110
+ /** Permutation every plain tier applies, built from the frozen published key. */
111
+ function frozenPermutation() {
112
+ return keyedPermutation({ keyBytes: exports.FROZEN_KEY_BYTES, keyId: "frozen" });
113
+ }
114
+ /** Alphanumeric, no safety strips, no checksum, hyphen-delimited XXX-XXX. */
115
+ function basehMinimumV1() {
116
+ return tier(MINIMUM, frozenPermutation(), false);
117
+ }
118
+ /** baseh-minimum permuted with caller-supplied key material. */
119
+ function basehMinimumPV1(options) {
120
+ return tier(MINIMUM, keyedPermutation(options), true);
121
+ }
122
+ /** Visual light plus spoken light, two checksum symbols, hyphen-delimited. */
123
+ function basehLightV1() {
124
+ return tier(LIGHT, frozenPermutation(), false);
125
+ }
126
+ /** baseh-light permuted with caller-supplied key material. */
127
+ function basehLightPV1(options) {
128
+ return tier(LIGHT, keyedPermutation(options), true);
129
+ }
130
+ /** Visual medium plus spoken medium, two checksum symbols, hyphen-delimited. The default. */
131
+ function basehMediumV1() {
132
+ return tier(MEDIUM, frozenPermutation(), false);
133
+ }
134
+ /** baseh-medium permuted with caller-supplied key material. */
135
+ function basehMediumPV1(options) {
136
+ return tier(MEDIUM, keyedPermutation(options), true);
137
+ }
138
+ /** Conservative alphabet plus spoken heavy, two checksum symbols, hyphen-delimited. */
139
+ function basehHeavyV1() {
140
+ return tier(HEAVY, frozenPermutation(), false);
141
+ }
142
+ // Spec 17.1. The expandable recommended default carries the same safety
143
+ // posture as baseh-medium-v1: the medium visual strips (O, I, L, B, S) and
144
+ // the medium spoken strips (T, N, W), so an issued code never emits a
145
+ // visual or spoken confusable. The zero ban of section 19.2 then removes 0
146
+ // (and O, already gone), leaving a 27-symbol body. The checksum alphabet
147
+ // derives as "0" plus the body (28 symbols).
148
+ const EXPANDABLE_BODY = "123456789ACDEFGHJKMPQRUVXYZ";
149
+ /**
150
+ * Spec 17.1. The frozen expandable tier: four characters while the namespace
151
+ * is small, gaining one symbol automatically as issuance climbs past each
152
+ * generation's capacity. The body alphabet is the medium safety set (the
153
+ * full alphanumeric set minus the visual look-alikes O, I, L, B, S and the
154
+ * spoken-confusable T, N, W), with the zero ban of spec 19.2 removing 0 (27
155
+ * symbols); the checksum alphabet derives as "0" plus the body (28 symbols).
156
+ * The short checksum of spec 22 is on: one checksum symbol through five
157
+ * characters (27^3 = 19,683 ids at length 4, 27^4 = 531,441 at length 5),
158
+ * two from six characters up. The hyphen appears from six characters up,
159
+ * split by the balanced grouping rule of spec 19.5.
160
+ */
161
+ function expandableTier(permutation, pSuffix) {
162
+ return {
163
+ profileId: "baseh-expandable" + (pSuffix ? "-p" : "") + "-v1",
164
+ mode: "expandable",
165
+ bodyAlphabet: EXPANDABLE_BODY,
166
+ minLength: 4,
167
+ checksumAlphabet: "0" + EXPANDABLE_BODY,
168
+ checksumLength: 2,
169
+ shortChecksumLength: 1,
170
+ shortChecksumUntil: 5,
171
+ caseSensitive: false,
172
+ separator: "-",
173
+ separatorMinLength: 6,
174
+ grouping: [],
175
+ aliases: { ...OIL_ALIASES, B: "8", S: "5", T: "P", N: "M", W: "V" },
176
+ permutation,
177
+ profanity: { mode: "blocklist" },
178
+ maxRepetition: 4
179
+ };
180
+ }
181
+ /** The frozen expandable tier; the recommended starting point for new namespaces. */
182
+ function basehExpandableV1() {
183
+ return expandableTier(frozenPermutation(), false);
184
+ }
185
+ /** baseh-expandable permuted with caller-supplied key material. */
186
+ function basehExpandablePV1(options) {
187
+ return expandableTier(keyedPermutation(options), true);
188
+ }
189
+ /** baseh-heavy permuted with caller-supplied key material. */
190
+ function basehHeavyPV1(options) {
191
+ return tier(HEAVY, keyedPermutation(options), true);
192
+ }