@bcts/seedtool-cli 1.0.0-alpha.14

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/main.mjs ADDED
@@ -0,0 +1,1375 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from "commander";
3
+ import { SecureRandomNumberGenerator } from "@bcts/rand";
4
+ import { SSKRGroupSpec, SSKRSecret, SSKRShare, SSKRSpec, Seed, sskrCombine, sskrGenerate } from "@bcts/components";
5
+ import * as readline from "readline";
6
+ import { Envelope, SymmetricKey } from "@bcts/envelope";
7
+ import { DATE, NAME, NOTE, SEED_TYPE } from "@bcts/known-values";
8
+ import { CborDate, decodeCbor, expectBytes, toByteString, toTaggedValue } from "@bcts/dcbor";
9
+ import { entropyToMnemonic, mnemonicToEntropy, validateMnemonic } from "@scure/bip39";
10
+ import { wordlist } from "@scure/bip39/wordlists/english";
11
+ import { BytewordsStyle, MultipartDecoder, MultipartEncoder, UR, decodeBytewords, encodeBytewords } from "@bcts/uniform-resources";
12
+ import { SSKR_SHARE, SSKR_SHARE_V1 } from "@bcts/tags";
13
+ import { sha256 } from "@noble/hashes/sha256";
14
+ import { hkdf } from "@noble/hashes/hkdf";
15
+ import * as fs from "node:fs";
16
+
17
+ //#region src/cli.ts
18
+ /**
19
+ * CLI types and interfaces
20
+ * Ported from seedtool-cli-rust/src/cli.rs
21
+ */
22
+ /**
23
+ * Input format keys.
24
+ * Matches Rust InputFormatKey enum in format.rs
25
+ */
26
+ let InputFormatKey = /* @__PURE__ */ function(InputFormatKey$1) {
27
+ InputFormatKey$1["Random"] = "random";
28
+ InputFormatKey$1["Hex"] = "hex";
29
+ InputFormatKey$1["Btw"] = "btw";
30
+ InputFormatKey$1["Btwu"] = "btwu";
31
+ InputFormatKey$1["Btwm"] = "btwm";
32
+ InputFormatKey$1["Bits"] = "bits";
33
+ InputFormatKey$1["Cards"] = "cards";
34
+ InputFormatKey$1["Dice"] = "dice";
35
+ InputFormatKey$1["Base6"] = "base6";
36
+ InputFormatKey$1["Base10"] = "base10";
37
+ InputFormatKey$1["Ints"] = "ints";
38
+ InputFormatKey$1["Bip39"] = "bip39";
39
+ InputFormatKey$1["Sskr"] = "sskr";
40
+ InputFormatKey$1["Envelope"] = "envelope";
41
+ InputFormatKey$1["Multipart"] = "multipart";
42
+ InputFormatKey$1["Seed"] = "seed";
43
+ return InputFormatKey$1;
44
+ }({});
45
+ /**
46
+ * Output format keys.
47
+ * Matches Rust OutputFormatKey enum in format.rs
48
+ */
49
+ let OutputFormatKey = /* @__PURE__ */ function(OutputFormatKey$1) {
50
+ OutputFormatKey$1["Hex"] = "hex";
51
+ OutputFormatKey$1["Btw"] = "btw";
52
+ OutputFormatKey$1["Btwu"] = "btwu";
53
+ OutputFormatKey$1["Btwm"] = "btwm";
54
+ OutputFormatKey$1["Bits"] = "bits";
55
+ OutputFormatKey$1["Cards"] = "cards";
56
+ OutputFormatKey$1["Dice"] = "dice";
57
+ OutputFormatKey$1["Base6"] = "base6";
58
+ OutputFormatKey$1["Base10"] = "base10";
59
+ OutputFormatKey$1["Ints"] = "ints";
60
+ OutputFormatKey$1["Bip39"] = "bip39";
61
+ OutputFormatKey$1["Sskr"] = "sskr";
62
+ OutputFormatKey$1["Envelope"] = "envelope";
63
+ OutputFormatKey$1["Multipart"] = "multipart";
64
+ OutputFormatKey$1["Seed"] = "seed";
65
+ return OutputFormatKey$1;
66
+ }({});
67
+ /**
68
+ * SSKR output format keys.
69
+ * Matches Rust SSKRFormatKey enum in sskr.rs
70
+ */
71
+ let SSKRFormatKey = /* @__PURE__ */ function(SSKRFormatKey$1) {
72
+ SSKRFormatKey$1["Envelope"] = "envelope";
73
+ SSKRFormatKey$1["Btw"] = "btw";
74
+ SSKRFormatKey$1["Btwm"] = "btwm";
75
+ SSKRFormatKey$1["Btwu"] = "btwu";
76
+ SSKRFormatKey$1["Ur"] = "ur";
77
+ return SSKRFormatKey$1;
78
+ }({});
79
+ /**
80
+ * CLI state and configuration.
81
+ * Matches Rust Cli struct.
82
+ */
83
+ var Cli = class Cli {
84
+ /** The input to be transformed. If required and not present, will be read from stdin. */
85
+ input;
86
+ /** The number of output units (hex bytes, base-10 digits, etc.) */
87
+ count = 16;
88
+ /** The input format. Default: Random */
89
+ in = InputFormatKey.Random;
90
+ /** The output format. Default: Hex */
91
+ out = OutputFormatKey.Hex;
92
+ /** The lowest int returned (0-254). Default: 0 */
93
+ low = 0;
94
+ /** The highest int returned (1-255), low < high. Default: 9 */
95
+ high = 9;
96
+ /** The name of the seed. */
97
+ name;
98
+ /** The note associated with the seed. */
99
+ note;
100
+ /** The seed's creation date, in ISO-8601 format. May also be `now`. */
101
+ date;
102
+ /** For `multipart` output, max fragment length. Default: 500 */
103
+ maxFragmentLen = 500;
104
+ /** For `multipart` output, additional parts for fountain encoding. Default: 0 */
105
+ additionalParts = 0;
106
+ /** Group specifications for SSKR. */
107
+ groups = [];
108
+ /** The number of groups that must meet their threshold. Default: 1 */
109
+ groupThreshold = 1;
110
+ /** SSKR output format. Default: Envelope */
111
+ sskrFormat = SSKRFormatKey.Envelope;
112
+ /** Deterministic RNG seed string. */
113
+ deterministic;
114
+ /** The seed being processed (internal state). */
115
+ seed;
116
+ /** The RNG source (internal state). */
117
+ rng;
118
+ /**
119
+ * Get input from argument or read from stdin.
120
+ * Matches Rust expect_input method.
121
+ */
122
+ expectInput() {
123
+ if (this.input !== void 0) return this.input;
124
+ throw new Error("Input required but not provided. Use stdin or pass as argument.");
125
+ }
126
+ /**
127
+ * Get input from argument or read from stdin asynchronously.
128
+ * This is the async version for actual CLI use.
129
+ */
130
+ async expectInputAsync() {
131
+ if (this.input !== void 0) return this.input;
132
+ return new Promise((resolve, reject) => {
133
+ let data = "";
134
+ const rl = readline.createInterface({
135
+ input: process.stdin,
136
+ output: process.stdout,
137
+ terminal: false
138
+ });
139
+ rl.on("line", (line) => {
140
+ data += line + "\n";
141
+ });
142
+ rl.on("close", () => {
143
+ resolve(data.trim());
144
+ });
145
+ rl.on("error", (err) => {
146
+ reject(err);
147
+ });
148
+ });
149
+ }
150
+ /**
151
+ * Get the seed, throwing if not initialized.
152
+ * Matches Rust expect_seed method.
153
+ */
154
+ expectSeed() {
155
+ if (this.seed === void 0) throw new Error("Seed not initialized");
156
+ return this.seed;
157
+ }
158
+ /**
159
+ * Generate random data using the configured RNG.
160
+ * Matches Rust random_data method.
161
+ */
162
+ randomData(size) {
163
+ if (this.rng === void 0) throw new Error("RNG not initialized");
164
+ if (this.rng.type === "secure") return this.rng.rng.randomData(size);
165
+ else return this.rng.rng.deterministicRandomData(size);
166
+ }
167
+ /**
168
+ * Get the seed with CLI overrides applied (name, note, date).
169
+ * Matches Rust seed_with_overrides method.
170
+ */
171
+ seedWithOverrides() {
172
+ const seed = this.expectSeed().clone();
173
+ if (this.name !== void 0) seed.setName(this.name);
174
+ if (this.note !== void 0) seed.setNote(this.note);
175
+ if (this.date !== void 0) seed.setCreationDate(this.date);
176
+ return seed;
177
+ }
178
+ /**
179
+ * Convert the seed (with overrides) to an Envelope.
180
+ * Matches Rust to_envelope method.
181
+ */
182
+ toEnvelope() {
183
+ return this.seedWithOverrides().toEnvelope();
184
+ }
185
+ /**
186
+ * Build SSKR spec from CLI options.
187
+ * Matches Rust sskr_spec method.
188
+ */
189
+ sskrSpec() {
190
+ return SSKRSpec.new(this.groupThreshold, this.groups);
191
+ }
192
+ /**
193
+ * Clone the CLI state.
194
+ */
195
+ clone() {
196
+ const cli = new Cli();
197
+ cli.input = this.input;
198
+ cli.count = this.count;
199
+ cli.in = this.in;
200
+ cli.out = this.out;
201
+ cli.low = this.low;
202
+ cli.high = this.high;
203
+ cli.name = this.name;
204
+ cli.note = this.note;
205
+ cli.date = this.date;
206
+ cli.maxFragmentLen = this.maxFragmentLen;
207
+ cli.additionalParts = this.additionalParts;
208
+ cli.groups = [...this.groups];
209
+ cli.groupThreshold = this.groupThreshold;
210
+ cli.sskrFormat = this.sskrFormat;
211
+ cli.deterministic = this.deterministic;
212
+ cli.seed = this.seed?.clone();
213
+ cli.rng = this.rng;
214
+ return cli;
215
+ }
216
+ };
217
+
218
+ //#endregion
219
+ //#region src/seed.ts
220
+ /**
221
+ * Seed type for seedtool-cli
222
+ * Ported from seedtool-cli-rust/src/seed.rs
223
+ *
224
+ * This is a local Seed type that wraps the seed data with metadata
225
+ * and provides Envelope conversion. It differs from @bcts/components Seed
226
+ * in that it doesn't enforce minimum length (for CLI flexibility) and
227
+ * has direct Envelope conversion methods.
228
+ */
229
+ /**
230
+ * Seed with optional metadata.
231
+ * Matches Rust Seed struct in seed.rs.
232
+ */
233
+ var Seed$1 = class Seed$1 {
234
+ _data;
235
+ _name;
236
+ _note;
237
+ _creationDate;
238
+ /**
239
+ * Create a new Seed with the given data.
240
+ * Matches Rust Seed::new function.
241
+ */
242
+ constructor(data) {
243
+ this._data = new Uint8Array(data);
244
+ this._name = "";
245
+ this._note = "";
246
+ this._creationDate = void 0;
247
+ }
248
+ /**
249
+ * Create a new Seed with data and optional metadata.
250
+ * Matches Rust Seed::new_opt function.
251
+ */
252
+ static newOpt(data, name, note, creationDate) {
253
+ const seed = new Seed$1(data);
254
+ seed._name = name;
255
+ seed._note = note;
256
+ seed._creationDate = creationDate;
257
+ return seed;
258
+ }
259
+ /**
260
+ * Create a new Seed from raw data.
261
+ * Convenience factory method.
262
+ */
263
+ static new(data) {
264
+ return new Seed$1(data);
265
+ }
266
+ /**
267
+ * Get the seed data.
268
+ * Matches Rust seed.data() method.
269
+ */
270
+ data() {
271
+ return this._data;
272
+ }
273
+ /**
274
+ * Get the seed name.
275
+ * Matches Rust seed.name() method.
276
+ * Returns empty string if not set.
277
+ */
278
+ name() {
279
+ return this._name;
280
+ }
281
+ /**
282
+ * Set the seed name.
283
+ * Matches Rust seed.set_name() method.
284
+ */
285
+ setName(name) {
286
+ this._name = name;
287
+ }
288
+ /**
289
+ * Get the seed note.
290
+ * Matches Rust seed.note() method.
291
+ * Returns empty string if not set.
292
+ */
293
+ note() {
294
+ return this._note;
295
+ }
296
+ /**
297
+ * Set the seed note.
298
+ * Matches Rust seed.set_note() method.
299
+ */
300
+ setNote(note) {
301
+ this._note = note;
302
+ }
303
+ /**
304
+ * Get the creation date.
305
+ * Matches Rust seed.creation_date() method.
306
+ */
307
+ creationDate() {
308
+ return this._creationDate;
309
+ }
310
+ /**
311
+ * Set the creation date.
312
+ * Matches Rust seed.set_creation_date() method.
313
+ */
314
+ setCreationDate(date) {
315
+ this._creationDate = date;
316
+ }
317
+ /**
318
+ * Clone the seed.
319
+ */
320
+ clone() {
321
+ return Seed$1.newOpt(new Uint8Array(this._data), this._name, this._note, this._creationDate);
322
+ }
323
+ /**
324
+ * Convert to Envelope.
325
+ * Matches Rust impl From<Seed> for Envelope.
326
+ *
327
+ * Creates an envelope with:
328
+ * - Subject: byte string of seed data
329
+ * - Type assertion: 'Seed'
330
+ * - Optional date assertion
331
+ * - Optional name assertion (if not empty)
332
+ * - Optional note assertion (if not empty)
333
+ */
334
+ toEnvelope() {
335
+ let envelope = Envelope.new(toByteString(this._data));
336
+ envelope = envelope.addType(SEED_TYPE);
337
+ if (this._creationDate !== void 0) {
338
+ const cborDate = CborDate.fromDatetime(this._creationDate);
339
+ envelope = envelope.addAssertion(DATE, cborDate);
340
+ }
341
+ if (this._name.length > 0) envelope = envelope.addAssertion(NAME, this._name);
342
+ if (this._note.length > 0) envelope = envelope.addAssertion(NOTE, this._note);
343
+ return envelope;
344
+ }
345
+ /**
346
+ * Create a Seed from an Envelope.
347
+ * Matches Rust impl TryFrom<Envelope> for Seed.
348
+ */
349
+ static fromEnvelope(envelope) {
350
+ envelope.checkTypeValue(SEED_TYPE);
351
+ const leaf = envelope.subject().asLeaf();
352
+ if (leaf === void 0) throw new Error("Seed envelope must have a leaf subject");
353
+ const data = leaf.asByteString();
354
+ if (data === void 0) throw new Error("Seed envelope subject must be a byte string");
355
+ let name = "";
356
+ try {
357
+ const nameObj = envelope.objectForPredicate(NAME);
358
+ if (nameObj !== void 0) {
359
+ const nameStr = nameObj.asText();
360
+ if (nameStr !== void 0) name = nameStr;
361
+ }
362
+ } catch {}
363
+ let note = "";
364
+ try {
365
+ const noteObj = envelope.objectForPredicate(NOTE);
366
+ if (noteObj !== void 0) {
367
+ const noteStr = noteObj.asText();
368
+ if (noteStr !== void 0) note = noteStr;
369
+ }
370
+ } catch {}
371
+ let creationDate;
372
+ try {
373
+ const dateObj = envelope.objectForPredicate(DATE);
374
+ if (dateObj !== void 0) {
375
+ const leaf$1 = dateObj.asLeaf();
376
+ if (leaf$1 !== void 0) creationDate = CborDate.fromTaggedCbor(leaf$1).datetime();
377
+ }
378
+ } catch {
379
+ try {
380
+ const dateObj = envelope.objectForPredicate(DATE);
381
+ if (dateObj !== void 0) {
382
+ const dateStr = dateObj.asText();
383
+ if (dateStr !== void 0) creationDate = new Date(dateStr);
384
+ }
385
+ } catch {}
386
+ }
387
+ return Seed$1.newOpt(data, name, note, creationDate);
388
+ }
389
+ /**
390
+ * Convert to @bcts/components Seed.
391
+ * Matches Rust impl TryFrom<&Seed> for ComponentsSeed.
392
+ */
393
+ toComponentsSeed() {
394
+ return Seed.newOpt(this._data, this._name.length > 0 ? this._name : void 0, this._note.length > 0 ? this._note : void 0, this._creationDate);
395
+ }
396
+ /**
397
+ * Create from @bcts/components Seed.
398
+ * Matches Rust impl From<ComponentsSeed> for Seed.
399
+ */
400
+ static fromComponentsSeed(seed) {
401
+ return Seed$1.newOpt(seed.asBytes(), seed.name(), seed.note(), seed.creationDate());
402
+ }
403
+ /**
404
+ * Get string representation.
405
+ */
406
+ toString() {
407
+ return `Seed(${Array.from(this._data.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("")}..., ${this._data.length} bytes)`;
408
+ }
409
+ /**
410
+ * Check equality with another Seed.
411
+ */
412
+ equals(other) {
413
+ if (this._data.length !== other._data.length) return false;
414
+ for (let i = 0; i < this._data.length; i++) if (this._data[i] !== other._data[i]) return false;
415
+ if (this._name !== other._name) return false;
416
+ if (this._note !== other._note) return false;
417
+ if (this._creationDate?.getTime() !== other._creationDate?.getTime()) return false;
418
+ return true;
419
+ }
420
+ };
421
+
422
+ //#endregion
423
+ //#region src/util.ts
424
+ /**
425
+ * Utility functions for data conversion
426
+ * Ported from seedtool-cli-rust/src/util.rs
427
+ */
428
+ /**
429
+ * Convert bytes to hex string.
430
+ * Matches Rust data_to_hex function.
431
+ */
432
+ function dataToHex(bytes) {
433
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
434
+ }
435
+ /**
436
+ * Convert hex string to bytes.
437
+ * Matches Rust hex_to_data function.
438
+ */
439
+ function hexToData(hex) {
440
+ if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
441
+ const bytes = new Uint8Array(hex.length / 2);
442
+ for (let i = 0; i < hex.length; i += 2) {
443
+ const byte = parseInt(hex.substring(i, i + 2), 16);
444
+ if (isNaN(byte)) throw new Error(`Invalid hex character at position ${i}`);
445
+ bytes[i / 2] = byte;
446
+ }
447
+ return bytes;
448
+ }
449
+ /**
450
+ * Convert byte values to a different base range [0, base-1].
451
+ * Each byte (0-255) is scaled proportionally to the target base.
452
+ * Matches Rust data_to_base function.
453
+ *
454
+ * @param buf - Input bytes
455
+ * @param base - Target base (e.g., 6 for base-6)
456
+ * @returns Array of values in range [0, base-1]
457
+ */
458
+ function dataToBase(buf, base) {
459
+ const result = new Uint8Array(buf.length);
460
+ for (let i = 0; i < buf.length; i++) result[i] = Math.round(buf[i] / 255 * (base - 1));
461
+ return result;
462
+ }
463
+ /**
464
+ * Convert bytes to an alphabet string using a base and alphabet function.
465
+ * Matches Rust data_to_alphabet function.
466
+ *
467
+ * @param buf - Input bytes
468
+ * @param base - Target base
469
+ * @param toAlphabet - Function to convert index to character
470
+ * @returns String of alphabet characters
471
+ */
472
+ function dataToAlphabet(buf, base, toAlphabet) {
473
+ const data = dataToBase(buf, base);
474
+ return Array.from(data).map((b) => toAlphabet(b)).join("");
475
+ }
476
+ /**
477
+ * Parse whitespace-separated integers.
478
+ * Matches Rust parse_ints function.
479
+ *
480
+ * @param input - Space-separated integer string
481
+ * @returns Array of bytes
482
+ * @throws Error if any integer is out of range [0, 255]
483
+ */
484
+ function parseInts(input) {
485
+ const parts = input.trim().split(/\s+/);
486
+ const result = [];
487
+ for (const s of parts) {
488
+ if (s === "") continue;
489
+ const i = parseInt(s, 10);
490
+ if (isNaN(i)) throw new Error(`Invalid integer: ${s}`);
491
+ if (i < 0 || i > 255) throw new Error("Integer out of range. Allowed: [0-255]");
492
+ result.push(i);
493
+ }
494
+ return new Uint8Array(result);
495
+ }
496
+ /**
497
+ * Convert bytes to a string of integers in a given range.
498
+ * Matches Rust data_to_ints function.
499
+ *
500
+ * @param buf - Input bytes
501
+ * @param low - Lowest output value (0-254)
502
+ * @param high - Highest output value (1-255), low < high
503
+ * @param separator - String to separate values
504
+ * @returns String of integers
505
+ * @throws Error if range is invalid
506
+ */
507
+ function dataToInts(buf, low, high, separator) {
508
+ if (!(low < high && high <= 255)) throw new Error("Int conversion range must be in 0 <= low < high <= 255.");
509
+ const data = dataToBase(buf, high - low + 1);
510
+ return Array.from(data).map((b) => (b + low).toString()).join(separator);
511
+ }
512
+ /**
513
+ * Parse a string of digits in a given range to bytes.
514
+ * Matches Rust digits_to_data function.
515
+ *
516
+ * @param inStr - String of digits
517
+ * @param low - Lowest valid digit
518
+ * @param high - Highest valid digit
519
+ * @returns Array of digit values
520
+ * @throws Error if any digit is out of range
521
+ */
522
+ function digitsToData(inStr, low, high) {
523
+ const result = [];
524
+ for (const c of inStr) {
525
+ const n = c.charCodeAt(0) - "0".charCodeAt(0);
526
+ if (n < low || n > high) throw new Error(`Invalid digit: ${c}. Expected range [${low}-${high}].`);
527
+ result.push(n);
528
+ }
529
+ return new Uint8Array(result);
530
+ }
531
+
532
+ //#endregion
533
+ //#region src/formats/hex.ts
534
+ /**
535
+ * Hexadecimal format handler.
536
+ * Round-trippable: hex → seed → hex.
537
+ */
538
+ var HexFormat = class {
539
+ name() {
540
+ return "hex";
541
+ }
542
+ roundTrippable() {
543
+ return true;
544
+ }
545
+ processInput(state) {
546
+ const input = state.expectInput();
547
+ state.seed = Seed$1.new(hexToData(input));
548
+ return state;
549
+ }
550
+ processOutput(state) {
551
+ return dataToHex(state.expectSeed().data());
552
+ }
553
+ };
554
+
555
+ //#endregion
556
+ //#region src/formats/bip39.ts
557
+ /**
558
+ * BIP39 mnemonic format handler.
559
+ * Round-trippable: mnemonic → seed → mnemonic.
560
+ */
561
+ var Bip39Format = class {
562
+ name() {
563
+ return "bip39";
564
+ }
565
+ roundTrippable() {
566
+ return true;
567
+ }
568
+ processInput(state) {
569
+ const normalized = state.expectInput().toLowerCase().trim().replace(/\s+/g, " ");
570
+ if (!validateMnemonic(normalized, wordlist)) throw new Error("Invalid BIP39 mnemonic");
571
+ const entropy = mnemonicToEntropy(normalized, wordlist);
572
+ state.seed = Seed$1.new(entropy);
573
+ return state;
574
+ }
575
+ processOutput(state) {
576
+ return entropyToMnemonic(state.expectSeed().data(), wordlist);
577
+ }
578
+ };
579
+
580
+ //#endregion
581
+ //#region src/formats/sskr.ts
582
+ /**
583
+ * SSKR format handler.
584
+ * Round-trippable: sskr shares → seed → sskr shares.
585
+ * Supports multiple sub-formats: envelope, btw, btwm, btwu, ur.
586
+ */
587
+ var SSKRFormat = class {
588
+ name() {
589
+ return "sskr";
590
+ }
591
+ roundTrippable() {
592
+ return true;
593
+ }
594
+ processInput(state) {
595
+ state.seed = parseSskrSeed(state.expectInput());
596
+ return state;
597
+ }
598
+ processOutput(state) {
599
+ const spec = state.sskrSpec();
600
+ const seed = state.expectSeed();
601
+ const format = state.sskrFormat;
602
+ return outputSskrSeed(seed, spec, format);
603
+ }
604
+ };
605
+ function outputSskrSeed(seed, spec, format) {
606
+ switch (format) {
607
+ case "envelope": {
608
+ const envelope = seed.toEnvelope();
609
+ const contentKey = SymmetricKey.new();
610
+ return envelope.wrap().encryptSubject(contentKey).sskrSplitFlattened(spec, contentKey).map((envelope$1) => envelope$1.urString()).join("\n");
611
+ }
612
+ case "btw": return makeBytewordsShares(spec, seed, BytewordsStyle.Standard);
613
+ case "btwm": return makeBytewordsShares(spec, seed, BytewordsStyle.Minimal);
614
+ case "btwu": return makeBytewordsShares(spec, seed, BytewordsStyle.Uri);
615
+ case "ur": return makeShares(spec, seed).map((share) => {
616
+ return UR.fromCbor("sskr", toByteString(share.asBytes())).toString();
617
+ }).join("\n");
618
+ }
619
+ }
620
+ function makeShares(spec, seed) {
621
+ const shareGroups = sskrGenerate(spec, SSKRSecret.new(seed.data()));
622
+ const flatShares = [];
623
+ for (const group of shareGroups) for (const shareData of group) flatShares.push(SSKRShare.fromData(shareData));
624
+ return flatShares;
625
+ }
626
+ function makeBytewordsShares(spec, seed, style) {
627
+ return makeShares(spec, seed).map((share) => toTaggedValue(SSKR_SHARE.value, toByteString(share.asBytes()))).map((share) => encodeBytewords(share.toData(), style)).join("\n");
628
+ }
629
+ function parseEnvelopes(input) {
630
+ try {
631
+ const shareStrings = input.split(/\s+/).filter((s) => s.length > 0);
632
+ const shareEnvelopes = [];
633
+ for (const str of shareStrings) try {
634
+ const envelope = Envelope.fromURString(str);
635
+ shareEnvelopes.push(envelope);
636
+ } catch {}
637
+ if (shareEnvelopes.length === 0) return null;
638
+ const recoveredEnvelope = Envelope.sskrJoin(shareEnvelopes).unwrap();
639
+ return Seed$1.fromEnvelope(recoveredEnvelope);
640
+ } catch {
641
+ return null;
642
+ }
643
+ }
644
+ function fromUntaggedCborShares(untaggedCborShares) {
645
+ try {
646
+ const recoveredSecret = sskrCombine(untaggedCborShares);
647
+ return Seed$1.new(recoveredSecret.getData());
648
+ } catch {
649
+ return null;
650
+ }
651
+ }
652
+ function fromTaggedCborShares(taggedCborDataShares) {
653
+ try {
654
+ const untaggedShares = [];
655
+ for (const data of taggedCborDataShares) {
656
+ const cbor = decodeCbor(data);
657
+ const tagged = cbor;
658
+ if (tagged.tag !== SSKR_SHARE.value && tagged.tag !== SSKR_SHARE_V1.value) return null;
659
+ const bytes = expectBytes(tagged.value || cbor);
660
+ untaggedShares.push(bytes);
661
+ }
662
+ return fromUntaggedCborShares(untaggedShares);
663
+ } catch {
664
+ return null;
665
+ }
666
+ }
667
+ function parseBytewords(input, style) {
668
+ try {
669
+ let shareStrings;
670
+ if (style === BytewordsStyle.Standard) shareStrings = input.split("\n").filter((s) => s.length > 0);
671
+ else shareStrings = input.split(/\s+/).filter((s) => s.length > 0);
672
+ const cborDataShares = [];
673
+ for (const s of shareStrings) try {
674
+ const decoded = decodeBytewords(s, style);
675
+ cborDataShares.push(decoded);
676
+ } catch {}
677
+ if (cborDataShares.length === 0) return null;
678
+ return fromTaggedCborShares(cborDataShares);
679
+ } catch {
680
+ return null;
681
+ }
682
+ }
683
+ function parseUr(input, expectedTagValue, allowTaggedCbor) {
684
+ try {
685
+ const expectedType = expectedTagValue === SSKR_SHARE.value ? "sskr" : "crypto-sskr";
686
+ const shareStrings = input.split(/\s+/).filter((s) => s.length > 0);
687
+ const urs = [];
688
+ for (const str of shareStrings) try {
689
+ const ur = UR.fromURString(str);
690
+ urs.push(ur);
691
+ } catch {}
692
+ if (urs.length === 0) return null;
693
+ for (const ur of urs) if (ur.type() !== expectedType) return null;
694
+ const untaggedCborShares = [];
695
+ for (const ur of urs) {
696
+ let cbor = ur.cbor();
697
+ if (allowTaggedCbor) try {
698
+ const tagged = decodeCbor(cbor.toData());
699
+ if (tagged.tag === SSKR_SHARE.value || tagged.tag === SSKR_SHARE_V1.value) cbor = tagged.value;
700
+ } catch {}
701
+ const bytes = expectBytes(cbor);
702
+ untaggedCborShares.push(bytes);
703
+ }
704
+ return fromUntaggedCborShares(untaggedCborShares);
705
+ } catch {
706
+ return null;
707
+ }
708
+ }
709
+ function parseSskrSeed(input) {
710
+ const envelopeResult = parseEnvelopes(input);
711
+ if (envelopeResult !== null) return envelopeResult;
712
+ const btwResult = parseBytewords(input, BytewordsStyle.Standard);
713
+ if (btwResult !== null) return btwResult;
714
+ const btwmResult = parseBytewords(input, BytewordsStyle.Minimal);
715
+ if (btwmResult !== null) return btwmResult;
716
+ const btwuResult = parseBytewords(input, BytewordsStyle.Uri);
717
+ if (btwuResult !== null) return btwuResult;
718
+ const urResult = parseUr(input, SSKR_SHARE.value, false);
719
+ if (urResult !== null) return urResult;
720
+ const urLegacyResult = parseUr(input, SSKR_SHARE_V1.value, true);
721
+ if (urLegacyResult !== null) return urLegacyResult;
722
+ throw new Error("Insufficient or invalid SSKR shares.");
723
+ }
724
+
725
+ //#endregion
726
+ //#region src/formats/envelope.ts
727
+ /**
728
+ * Gordian Envelope format handler.
729
+ * Round-trippable: envelope UR → seed → envelope UR.
730
+ */
731
+ var EnvelopeFormat = class {
732
+ name() {
733
+ return "envelope";
734
+ }
735
+ roundTrippable() {
736
+ return true;
737
+ }
738
+ processInput(state) {
739
+ const input = state.expectInput();
740
+ const envelope = Envelope.fromURString(input);
741
+ state.seed = Seed$1.fromEnvelope(envelope);
742
+ return state;
743
+ }
744
+ processOutput(state) {
745
+ return state.toEnvelope().urString();
746
+ }
747
+ };
748
+
749
+ //#endregion
750
+ //#region src/formats/seed-format.ts
751
+ /**
752
+ * Seed UR format handler.
753
+ * Round-trippable: seed UR → seed → seed UR.
754
+ * Uses the ur:seed format from bc-components.
755
+ */
756
+ var SeedFormat = class {
757
+ name() {
758
+ return "seed";
759
+ }
760
+ roundTrippable() {
761
+ return true;
762
+ }
763
+ processInput(state) {
764
+ const input = state.expectInput();
765
+ const componentsSeed = Seed.fromURString(input);
766
+ state.seed = Seed$1.fromComponentsSeed(componentsSeed);
767
+ return state;
768
+ }
769
+ processOutput(state) {
770
+ return state.seedWithOverrides().toComponentsSeed().urString();
771
+ }
772
+ };
773
+
774
+ //#endregion
775
+ //#region src/formats/multipart.ts
776
+ /**
777
+ * Multipart UR format handler.
778
+ * Round-trippable: multipart URs → seed → multipart URs.
779
+ * Uses fountain encoding for reliable transmission.
780
+ */
781
+ var MultipartFormat = class {
782
+ name() {
783
+ return "multipart";
784
+ }
785
+ roundTrippable() {
786
+ return true;
787
+ }
788
+ processInput(state) {
789
+ const shares = state.expectInput().split(/\s+/).filter((s) => s.length > 0);
790
+ const decoder = new MultipartDecoder();
791
+ for (const share of shares) {
792
+ decoder.receive(share);
793
+ if (decoder.isComplete()) break;
794
+ }
795
+ if (!decoder.isComplete()) throw new Error("Insufficient multipart shares");
796
+ const ur = decoder.message();
797
+ if (ur === void 0) throw new Error("Failed to decode multipart message");
798
+ const envelope = Envelope.fromUR(ur);
799
+ state.seed = Seed$1.fromEnvelope(envelope);
800
+ return state;
801
+ }
802
+ processOutput(state) {
803
+ const encoder = new MultipartEncoder(state.toEnvelope().ur(), state.maxFragmentLen);
804
+ const partsCount = encoder.partsCount() + state.additionalParts;
805
+ const parts = [];
806
+ for (let i = 0; i < partsCount; i++) parts.push(encoder.nextPart());
807
+ return parts.join("\n");
808
+ }
809
+ };
810
+
811
+ //#endregion
812
+ //#region src/formats/random.ts
813
+ /**
814
+ * Random seed generation format.
815
+ * Input-only: generates a new random seed.
816
+ */
817
+ var RandomFormat = class {
818
+ name() {
819
+ return "random";
820
+ }
821
+ roundTrippable() {
822
+ return true;
823
+ }
824
+ processInput(state) {
825
+ const data = state.randomData(state.count);
826
+ state.seed = Seed$1.new(data);
827
+ return state;
828
+ }
829
+ };
830
+
831
+ //#endregion
832
+ //#region src/random.ts
833
+ /**
834
+ * Random number generation utilities
835
+ * Ported from seedtool-cli-rust/src/random.rs
836
+ */
837
+ /** SHA256 output size in bytes */
838
+ const SHA256_SIZE = 32;
839
+ /**
840
+ * Deterministic random number generator.
841
+ * Matches Rust DeterministicRandomNumberGenerator struct.
842
+ *
843
+ * Uses HKDF-HMAC-SHA256 to generate deterministic random data
844
+ * from a seed, with an incrementing salt for each call.
845
+ */
846
+ var DeterministicRandomNumberGenerator = class DeterministicRandomNumberGenerator {
847
+ seed;
848
+ salt;
849
+ /**
850
+ * Create a new deterministic RNG from a 32-byte seed.
851
+ */
852
+ constructor(seed) {
853
+ if (seed.length !== SHA256_SIZE) throw new Error(`Seed must be ${SHA256_SIZE} bytes, got ${seed.length}`);
854
+ this.seed = new Uint8Array(seed);
855
+ this.salt = 0n;
856
+ }
857
+ /**
858
+ * Create a new deterministic RNG from a seed string.
859
+ * The string is hashed with SHA256 to produce the seed.
860
+ * Matches Rust new_with_seed function.
861
+ */
862
+ static newWithSeed(seedString) {
863
+ return new DeterministicRandomNumberGenerator(sha256(new TextEncoder().encode(seedString)));
864
+ }
865
+ /**
866
+ * Generate deterministic random data.
867
+ * Matches Rust deterministic_random_data method.
868
+ *
869
+ * Each call increments the salt and uses HKDF to derive
870
+ * the requested number of bytes.
871
+ */
872
+ deterministicRandomData(size) {
873
+ this.salt += 1n;
874
+ const saltBytes = new Uint8Array(8);
875
+ const view = new DataView(saltBytes.buffer);
876
+ const low = Number(this.salt & 4294967295n);
877
+ const high = Number(this.salt >> 32n & 4294967295n);
878
+ view.setUint32(0, low, true);
879
+ view.setUint32(4, high, true);
880
+ return hkdfHmacSha256(this.seed, saltBytes, size);
881
+ }
882
+ /**
883
+ * Clone the RNG state.
884
+ */
885
+ clone() {
886
+ const rng = new DeterministicRandomNumberGenerator(this.seed);
887
+ rng.salt = this.salt;
888
+ return rng;
889
+ }
890
+ };
891
+ /**
892
+ * HKDF-HMAC-SHA256 key derivation.
893
+ * Matches Rust hkdf_hmac_sha256 function from bc-crypto.
894
+ */
895
+ function hkdfHmacSha256(ikm, salt, length) {
896
+ return hkdf(sha256, ikm, salt, new Uint8Array(0), length);
897
+ }
898
+ /**
899
+ * Generate deterministic random data from entropy using SHA256.
900
+ * If n <= 32, returns the first n bytes of SHA256(entropy).
901
+ * Matches Rust sha256_deterministic_random function.
902
+ *
903
+ * @param entropy - The entropy bytes to hash
904
+ * @param n - Number of bytes to return (must be <= 32)
905
+ * @throws Error if n > 32
906
+ */
907
+ function sha256DeterministicRandom(entropy, n) {
908
+ const seed = sha256(entropy);
909
+ if (n <= seed.length) return seed.slice(0, n);
910
+ else throw new Error("Random number generator limits reached.");
911
+ }
912
+ /**
913
+ * Generate deterministic random data from a string using SHA256.
914
+ * Matches Rust sha256_deterministic_random_string function.
915
+ *
916
+ * @param str - The string to hash
917
+ * @param n - Number of bytes to return (must be <= 32)
918
+ * @throws Error if n > 32
919
+ */
920
+ function sha256DeterministicRandomString(str, n) {
921
+ return sha256DeterministicRandom(new TextEncoder().encode(str), n);
922
+ }
923
+ /**
924
+ * Generate deterministic random data from entropy using HKDF.
925
+ * This can generate any length output.
926
+ * Matches Rust deterministic_random function.
927
+ *
928
+ * @param entropy - The entropy bytes
929
+ * @param n - Number of bytes to return
930
+ */
931
+ function deterministicRandom(entropy, n) {
932
+ return hkdfHmacSha256(sha256(entropy), new Uint8Array(0), n);
933
+ }
934
+
935
+ //#endregion
936
+ //#region src/formats/base6.ts
937
+ /**
938
+ * Base-6 format handler.
939
+ * NOT round-trippable: input is entropy source.
940
+ * Digits 0-5 (compatible with https://iancoleman.io/bip39/).
941
+ */
942
+ var Base6Format = class {
943
+ name() {
944
+ return "base6";
945
+ }
946
+ roundTrippable() {
947
+ return false;
948
+ }
949
+ processInput(state) {
950
+ const input = state.expectInput();
951
+ digitsToData(input, 0, 5);
952
+ const data = sha256DeterministicRandomString(input, state.count);
953
+ state.seed = Seed$1.new(data);
954
+ return state;
955
+ }
956
+ processOutput(state) {
957
+ return dataToInts(state.expectSeed().data(), 0, 5, "");
958
+ }
959
+ };
960
+
961
+ //#endregion
962
+ //#region src/formats/base10.ts
963
+ /**
964
+ * Base-10 format handler.
965
+ * NOT round-trippable: input is entropy source.
966
+ * Digits 0-9 (compatible with https://iancoleman.io/bip39/).
967
+ */
968
+ var Base10Format = class {
969
+ name() {
970
+ return "base10";
971
+ }
972
+ roundTrippable() {
973
+ return false;
974
+ }
975
+ processInput(state) {
976
+ const input = state.expectInput();
977
+ digitsToData(input, 0, 9);
978
+ const data = sha256DeterministicRandomString(input, state.count);
979
+ state.seed = Seed$1.new(data);
980
+ return state;
981
+ }
982
+ processOutput(state) {
983
+ return dataToInts(state.expectSeed().data(), 0, 9, "");
984
+ }
985
+ };
986
+
987
+ //#endregion
988
+ //#region src/formats/bits.ts
989
+ /**
990
+ * Binary format handler.
991
+ * NOT round-trippable: input is entropy source.
992
+ * Bits 0-1 (compatible with https://iancoleman.io/bip39/).
993
+ */
994
+ var BitsFormat = class {
995
+ name() {
996
+ return "bits";
997
+ }
998
+ roundTrippable() {
999
+ return false;
1000
+ }
1001
+ processInput(state) {
1002
+ const input = state.expectInput();
1003
+ digitsToData(input, 0, 1);
1004
+ const data = sha256DeterministicRandomString(input, state.count);
1005
+ state.seed = Seed$1.new(data);
1006
+ return state;
1007
+ }
1008
+ processOutput(state) {
1009
+ return dataToInts(state.expectSeed().data(), 0, 1, "");
1010
+ }
1011
+ };
1012
+
1013
+ //#endregion
1014
+ //#region src/formats/dice.ts
1015
+ /**
1016
+ * Dice roll format handler.
1017
+ * NOT round-trippable: input is entropy source.
1018
+ * Digits 1-6 (compatible with https://iancoleman.io/bip39/).
1019
+ */
1020
+ var DiceFormat = class {
1021
+ name() {
1022
+ return "dice";
1023
+ }
1024
+ roundTrippable() {
1025
+ return false;
1026
+ }
1027
+ processInput(state) {
1028
+ const input = state.expectInput();
1029
+ digitsToData(input, 1, 6);
1030
+ const data = sha256DeterministicRandomString(input, state.count);
1031
+ state.seed = Seed$1.new(data);
1032
+ return state;
1033
+ }
1034
+ processOutput(state) {
1035
+ return dataToInts(state.expectSeed().data(), 1, 6, "");
1036
+ }
1037
+ };
1038
+
1039
+ //#endregion
1040
+ //#region src/formats/cards.ts
1041
+ const CARD_SUITS = "cdhs";
1042
+ const CARD_RANKS = "a23456789tjqk";
1043
+ /**
1044
+ * Parse a card rank character.
1045
+ * Returns index 0-12.
1046
+ */
1047
+ function parseRank(c) {
1048
+ const lower = c.toLowerCase();
1049
+ const index = CARD_RANKS.indexOf(lower);
1050
+ if (index === -1) throw new Error(`Invalid card rank: ${c}. Allowed: [A,2-9,T,J,Q,K]`);
1051
+ return index;
1052
+ }
1053
+ /**
1054
+ * Parse a card suit character.
1055
+ * Returns index 0-3.
1056
+ */
1057
+ function parseSuit(c) {
1058
+ const lower = c.toLowerCase();
1059
+ const index = CARD_SUITS.indexOf(lower);
1060
+ if (index === -1) throw new Error(`Invalid card suit: ${c}. Allowed: [C,D,H,S]`);
1061
+ return index;
1062
+ }
1063
+ /**
1064
+ * Convert card string to byte array.
1065
+ * Each pair of characters represents one card.
1066
+ */
1067
+ function cardsToData(cards) {
1068
+ const len = cards.length;
1069
+ if (len % 2 !== 0) throw new Error("Cards string must have even number of characters.");
1070
+ const count = len / 2;
1071
+ const result = new Uint8Array(count);
1072
+ for (let i = 0; i < count; i++) {
1073
+ const rank = parseRank(cards[i * 2]);
1074
+ result[i] = parseSuit(cards[i * 2 + 1]) * 13 + rank;
1075
+ }
1076
+ return result;
1077
+ }
1078
+ /**
1079
+ * Convert a card index (0-51) to card string.
1080
+ */
1081
+ function toCard(n) {
1082
+ if (n > 51) throw new Error(`Card index out of range: ${n}`);
1083
+ const rank = n % 13;
1084
+ const suit = Math.floor(n / 13);
1085
+ return CARD_RANKS[rank] + CARD_SUITS[suit];
1086
+ }
1087
+ /**
1088
+ * Playing cards format handler.
1089
+ * NOT round-trippable: input is entropy source.
1090
+ * Card notation: rank (A,2-9,T,J,Q,K) + suit (C,D,H,S).
1091
+ */
1092
+ var CardsFormat = class {
1093
+ name() {
1094
+ return "cards";
1095
+ }
1096
+ roundTrippable() {
1097
+ return false;
1098
+ }
1099
+ processInput(state) {
1100
+ const data = deterministicRandom(cardsToData(state.expectInput()), state.count);
1101
+ state.seed = Seed$1.new(data);
1102
+ return state;
1103
+ }
1104
+ processOutput(state) {
1105
+ return dataToAlphabet(state.expectSeed().data(), 52, toCard);
1106
+ }
1107
+ };
1108
+
1109
+ //#endregion
1110
+ //#region src/formats/ints.ts
1111
+ /**
1112
+ * Integer list format handler.
1113
+ * NOT round-trippable: input is entropy source.
1114
+ * Configurable range via --low and --high CLI options.
1115
+ */
1116
+ var IntsFormat = class {
1117
+ name() {
1118
+ return "ints";
1119
+ }
1120
+ roundTrippable() {
1121
+ return false;
1122
+ }
1123
+ processInput(state) {
1124
+ const data = deterministicRandom(parseInts(state.expectInput()), state.count);
1125
+ state.seed = Seed$1.new(data);
1126
+ return state;
1127
+ }
1128
+ processOutput(state) {
1129
+ return dataToInts(state.expectSeed().data(), state.low, state.high, " ");
1130
+ }
1131
+ };
1132
+
1133
+ //#endregion
1134
+ //#region src/formats/bytewords-standard.ts
1135
+ /**
1136
+ * Bytewords Standard format handler.
1137
+ * Round-trippable: bytewords → seed → bytewords.
1138
+ * Uses full words separated by spaces.
1139
+ */
1140
+ var BytewordsStandardFormat = class {
1141
+ name() {
1142
+ return "btw";
1143
+ }
1144
+ roundTrippable() {
1145
+ return true;
1146
+ }
1147
+ processInput(state) {
1148
+ const data = decodeBytewords(state.expectInput(), BytewordsStyle.Standard);
1149
+ state.seed = Seed$1.new(data);
1150
+ return state;
1151
+ }
1152
+ processOutput(state) {
1153
+ return encodeBytewords(state.expectSeed().data(), BytewordsStyle.Standard);
1154
+ }
1155
+ };
1156
+
1157
+ //#endregion
1158
+ //#region src/formats/bytewords-minimal.ts
1159
+ /**
1160
+ * Bytewords Minimal format handler.
1161
+ * Round-trippable: bytewords → seed → bytewords.
1162
+ * Uses 2-letter abbreviations without separators.
1163
+ */
1164
+ var BytewordsMinimalFormat = class {
1165
+ name() {
1166
+ return "btwm";
1167
+ }
1168
+ roundTrippable() {
1169
+ return true;
1170
+ }
1171
+ processInput(state) {
1172
+ const data = decodeBytewords(state.expectInput(), BytewordsStyle.Minimal);
1173
+ state.seed = Seed$1.new(data);
1174
+ return state;
1175
+ }
1176
+ processOutput(state) {
1177
+ return encodeBytewords(state.expectSeed().data(), BytewordsStyle.Minimal);
1178
+ }
1179
+ };
1180
+
1181
+ //#endregion
1182
+ //#region src/formats/bytewords-uri.ts
1183
+ /**
1184
+ * Bytewords URI format handler.
1185
+ * Round-trippable: bytewords → seed → bytewords.
1186
+ * Uses full words separated by hyphens (URI-safe).
1187
+ */
1188
+ var BytewordsUriFormat = class {
1189
+ name() {
1190
+ return "btwu";
1191
+ }
1192
+ roundTrippable() {
1193
+ return true;
1194
+ }
1195
+ processInput(state) {
1196
+ const data = decodeBytewords(state.expectInput(), BytewordsStyle.Uri);
1197
+ state.seed = Seed$1.new(data);
1198
+ return state;
1199
+ }
1200
+ processOutput(state) {
1201
+ return encodeBytewords(state.expectSeed().data(), BytewordsStyle.Uri);
1202
+ }
1203
+ };
1204
+
1205
+ //#endregion
1206
+ //#region src/formats/format.ts
1207
+ /**
1208
+ * Select input format by key.
1209
+ * Matches Rust select_input_format function.
1210
+ */
1211
+ function selectInputFormat(key) {
1212
+ switch (key) {
1213
+ case "random": return new RandomFormat();
1214
+ case "hex": return new HexFormat();
1215
+ case "btw": return new BytewordsStandardFormat();
1216
+ case "btwu": return new BytewordsUriFormat();
1217
+ case "btwm": return new BytewordsMinimalFormat();
1218
+ case "bits": return new BitsFormat();
1219
+ case "cards": return new CardsFormat();
1220
+ case "dice": return new DiceFormat();
1221
+ case "base6": return new Base6Format();
1222
+ case "base10": return new Base10Format();
1223
+ case "ints": return new IntsFormat();
1224
+ case "bip39": return new Bip39Format();
1225
+ case "sskr": return new SSKRFormat();
1226
+ case "envelope": return new EnvelopeFormat();
1227
+ case "multipart": return new MultipartFormat();
1228
+ case "seed": return new SeedFormat();
1229
+ default: throw new Error(`Unknown input format: ${key}`);
1230
+ }
1231
+ }
1232
+ /**
1233
+ * Select output format by key.
1234
+ * Matches Rust select_output_format function.
1235
+ */
1236
+ function selectOutputFormat(key) {
1237
+ switch (key) {
1238
+ case "hex": return new HexFormat();
1239
+ case "btw": return new BytewordsStandardFormat();
1240
+ case "btwu": return new BytewordsUriFormat();
1241
+ case "btwm": return new BytewordsMinimalFormat();
1242
+ case "bits": return new BitsFormat();
1243
+ case "cards": return new CardsFormat();
1244
+ case "dice": return new DiceFormat();
1245
+ case "base6": return new Base6Format();
1246
+ case "base10": return new Base10Format();
1247
+ case "ints": return new IntsFormat();
1248
+ case "bip39": return new Bip39Format();
1249
+ case "sskr": return new SSKRFormat();
1250
+ case "envelope": return new EnvelopeFormat();
1251
+ case "multipart": return new MultipartFormat();
1252
+ case "seed": return new SeedFormat();
1253
+ default: throw new Error(`Unknown output format: ${key}`);
1254
+ }
1255
+ }
1256
+
1257
+ //#endregion
1258
+ //#region src/main.ts
1259
+ /**
1260
+ * A tool for generating and transforming cryptographic seeds.
1261
+ * Ported from seedtool-cli-rust/src/main.rs
1262
+ */
1263
+ const VERSION = "0.4.0";
1264
+ function parseLowInt(value) {
1265
+ const num = parseInt(value, 10);
1266
+ if (isNaN(num) || num < 0 || num > 254) throw new Error("LOW must be between 0 and 254");
1267
+ return num;
1268
+ }
1269
+ function parseHighInt(value) {
1270
+ const num = parseInt(value, 10);
1271
+ if (isNaN(num) || num < 1 || num > 255) throw new Error("HIGH must be between 1 and 255");
1272
+ return num;
1273
+ }
1274
+ function parseGroupThreshold(value) {
1275
+ const num = parseInt(value, 10);
1276
+ if (isNaN(num) || num < 1 || num > 16) throw new Error("THRESHOLD must be between 1 and 16");
1277
+ return num;
1278
+ }
1279
+ function parseCliDate(value) {
1280
+ if (value === "now") return /* @__PURE__ */ new Date();
1281
+ const date = new Date(value);
1282
+ if (isNaN(date.getTime())) throw new Error(`Invalid date: ${value}. Use ISO-8601 format or 'now'.`);
1283
+ return date;
1284
+ }
1285
+ function parseGroupSpec(value, previous) {
1286
+ const spec = SSKRGroupSpec.parse(value);
1287
+ return [...previous, spec];
1288
+ }
1289
+ async function main() {
1290
+ const program = new Command();
1291
+ program.name("seedtool").description("A tool for generating and transforming cryptographic seeds.\n\nby Wolf McNally and Christopher Allen\n\nReport bugs to ChristopherA@BlockchainCommons.com.\n© 2024 Blockchain Commons.").version(VERSION).argument("[INPUT]", "The input to be transformed. If required and not present, it will be read from stdin.").option("-c, --count <COUNT>", "The number of output units (hex bytes, base-10 digits, etc.)", "16").addOption(new Option("-i, --in <INPUT_TYPE>", "The input format. If not specified, a new random seed is generated using a secure random number generator.").choices([
1292
+ "random",
1293
+ "hex",
1294
+ "btw",
1295
+ "btwm",
1296
+ "btwu",
1297
+ "bits",
1298
+ "cards",
1299
+ "dice",
1300
+ "base6",
1301
+ "base10",
1302
+ "ints",
1303
+ "bip39",
1304
+ "sskr",
1305
+ "envelope",
1306
+ "seed",
1307
+ "multipart"
1308
+ ]).default("random")).addOption(new Option("-o, --out <OUTPUT_TYPE>", "The output format.").choices([
1309
+ "hex",
1310
+ "btw",
1311
+ "btwm",
1312
+ "btwu",
1313
+ "bits",
1314
+ "cards",
1315
+ "dice",
1316
+ "base6",
1317
+ "base10",
1318
+ "ints",
1319
+ "bip39",
1320
+ "sskr",
1321
+ "envelope",
1322
+ "seed",
1323
+ "multipart"
1324
+ ]).default("hex")).option("--low <LOW>", "The lowest int returned (0-254)", parseLowInt, 0).option("--high <HIGH>", "The highest int returned (1-255), low < high", parseHighInt, 9).option("--name <NAME>", "The name of the seed.").option("--note <NOTE>", "The note associated with the seed.").option("--date <DATE>", "The seed's creation date, in ISO-8601 format. May also be `now`.").option("--max-fragment-len <MAX_FRAG_LEN>", "For `multipart` output, the UR will be segmented into parts with fragments no larger than MAX_FRAG_LEN", "500").option("--additional-parts <NUM_PARTS>", "For `multipart` output, the number of additional parts above the minimum to generate using fountain encoding.", "0").option("-g, --groups <M-of-N>", "Group specifications. May appear more than once. M must be < N", parseGroupSpec, []).option("-t, --group-threshold <THRESHOLD>", "The number of groups that must meet their threshold. Must be <= the number of group specifications.", parseGroupThreshold, 1).addOption(new Option("-s, --sskr-format <SSKR_FORMAT>", "SSKR output format.").choices([
1325
+ "envelope",
1326
+ "btw",
1327
+ "btwm",
1328
+ "btwu",
1329
+ "ur"
1330
+ ]).default("envelope")).option("-d, --deterministic <SEED_STRING>", "Use a deterministic random number generator with the given seed string. Output generated from this seed will be the same every time, so generated seeds are only as secure as the seed string.");
1331
+ program.parse();
1332
+ const options = program.opts();
1333
+ const args = program.args;
1334
+ const cli = new Cli();
1335
+ if (args.length > 0) cli.input = args[0];
1336
+ else if (!process.stdin.isTTY) cli.input = fs.readFileSync(0, "utf-8").trim();
1337
+ cli.count = parseInt(options.count, 10);
1338
+ cli.in = options.in;
1339
+ cli.out = options.out;
1340
+ cli.low = options.low;
1341
+ cli.high = options.high;
1342
+ cli.name = options.name;
1343
+ cli.note = options.note;
1344
+ if (options.date) cli.date = parseCliDate(options.date);
1345
+ cli.maxFragmentLen = parseInt(options.maxFragmentLen, 10);
1346
+ cli.additionalParts = parseInt(options.additionalParts, 10);
1347
+ cli.groups = options.groups;
1348
+ cli.groupThreshold = options.groupThreshold;
1349
+ cli.sskrFormat = options.sskrFormat;
1350
+ if (options.deterministic) cli.rng = {
1351
+ type: "deterministic",
1352
+ rng: DeterministicRandomNumberGenerator.newWithSeed(options.deterministic)
1353
+ };
1354
+ else cli.rng = {
1355
+ type: "secure",
1356
+ rng: new SecureRandomNumberGenerator()
1357
+ };
1358
+ const inputFormat = selectInputFormat(cli.in);
1359
+ const outputFormat = selectOutputFormat(cli.out);
1360
+ if (!outputFormat.roundTrippable() && inputFormat.name() !== "random") {
1361
+ console.error(`Input for output form "${outputFormat.name()}" must be random.`);
1362
+ process.exit(1);
1363
+ }
1364
+ const processedCli = inputFormat.processInput(cli);
1365
+ const output = outputFormat.processOutput(processedCli);
1366
+ console.log(output);
1367
+ }
1368
+ main().catch((error) => {
1369
+ console.error(error.message);
1370
+ process.exit(1);
1371
+ });
1372
+
1373
+ //#endregion
1374
+ export { };
1375
+ //# sourceMappingURL=main.mjs.map