@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/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 (!Number.isInteger(profile.bodyLength) ||
47
- profile.bodyLength < 1 ||
48
- profile.bodyLength > 32) {
49
- fail("bodyLength must be an integer from 1 through 32");
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
- if (profile.checksumLength > 0) {
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
- let checksumNorm = [...checksumAlphabet].map((c) => norm(view, c)).join("");
67
- if (new Set(checksumNorm).size !== checksumNorm.length) {
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) {
@@ -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 feistel-v1 permutation. */
16
+ /** baseh-minimum permuted with caller-supplied key material. */
10
17
  export declare function basehMinimumPV1(options: FrozenKeyOptions): BasehProfile;
11
- /** Visual light plus spoken light, one checksum symbol. */
18
+ /** Visual light plus spoken light, two checksum symbols, hyphen-delimited. */
12
19
  export declare function basehLightV1(): BasehProfile;
13
- /** baseh-light with feistel-v1 permutation. */
20
+ /** baseh-light permuted with caller-supplied key material. */
14
21
  export declare function basehLightPV1(options: FrozenKeyOptions): BasehProfile;
15
- /** Visual medium plus spoken medium, one checksum symbol. The default. */
22
+ /** Visual medium plus spoken medium, two checksum symbols, hyphen-delimited. The default. */
16
23
  export declare function basehMediumV1(): BasehProfile;
17
- /** baseh-medium with feistel-v1 permutation. */
24
+ /** baseh-medium permuted with caller-supplied key material. */
18
25
  export declare function basehMediumPV1(options: FrozenKeyOptions): BasehProfile;
19
- /** Conservative alphabet plus spoken heavy, one checksum symbol. */
26
+ /** Conservative alphabet plus spoken heavy, two checksum symbols, hyphen-delimited. */
20
27
  export declare function basehHeavyV1(): BasehProfile;
21
- /** baseh-heavy with feistel-v1 permutation. */
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 2,176,782,336 ids
7
- * Light 31 symbols, 1 checksum 887,503,681 ids
8
- * Medium 28 symbols, 1 checksum 481,890,304 ids (default)
9
- * Heavy 26 symbols, 1 checksum 308,915,776 ids
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 and run the default
12
- * profanity blocklist. Minimum also uses a hyphen delimiter; the rest have
13
- * none. The -p variants are identical but with feistel-v1 permutation and
14
- * require caller-supplied key material.
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: 1,
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: 1,
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: 1,
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, { enabled: false }, false);
103
+ return tier(MINIMUM, frozenPermutation(), false);
89
104
  }
90
- /** baseh-minimum with feistel-v1 permutation. */
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, one checksum symbol. */
109
+ /** Visual light plus spoken light, two checksum symbols, hyphen-delimited. */
95
110
  export function basehLightV1() {
96
- return tier(LIGHT, { enabled: false }, false);
111
+ return tier(LIGHT, frozenPermutation(), false);
97
112
  }
98
- /** baseh-light with feistel-v1 permutation. */
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, one checksum symbol. The default. */
117
+ /** Visual medium plus spoken medium, two checksum symbols, hyphen-delimited. The default. */
103
118
  export function basehMediumV1() {
104
- return tier(MEDIUM, { enabled: false }, false);
119
+ return tier(MEDIUM, frozenPermutation(), false);
105
120
  }
106
- /** baseh-medium with feistel-v1 permutation. */
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, one checksum symbol. */
125
+ /** Conservative alphabet plus spoken heavy, two checksum symbols, hyphen-delimited. */
111
126
  export function basehHeavyV1() {
112
- return tier(HEAVY, { enabled: false }, false);
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 feistel-v1 permutation. */
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": "1.1.0",
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
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
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": "tsc -p tsconfig.build.json",
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
- "vectors": "tsx scripts/generate-vectors.ts"
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
- }