@bcts/seedtool-cli 1.0.0-alpha.23 → 1.0.0-beta.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/main.mjs CHANGED
@@ -3,6 +3,7 @@ import { Command, Option } from "commander";
3
3
  import { SecureRandomNumberGenerator } from "@bcts/rand";
4
4
  import { SSKRGroupSpec, SSKRSecret, SSKRShare, SSKRSpec, Seed, sskrCombine, sskrGenerate } from "@bcts/components";
5
5
  import * as readline from "node:readline";
6
+ import * as fs from "node:fs";
6
7
  import { Envelope, SymmetricKey } from "@bcts/envelope";
7
8
  import { DATE, NAME, NOTE, SEED_TYPE } from "@bcts/known-values";
8
9
  import { CborDate, decodeCbor, expectBytes, toByteString, toTaggedValue } from "@bcts/dcbor";
@@ -12,7 +13,6 @@ import { BytewordsStyle, MultipartDecoder, MultipartEncoder, UR, decodeBytewords
12
13
  import { SSKR_SHARE, SSKR_SHARE_V1 } from "@bcts/tags";
13
14
  import { sha256 } from "@noble/hashes/sha2.js";
14
15
  import { hkdf } from "@noble/hashes/hkdf.js";
15
- import fs from "node:fs";
16
16
  //#region src/cli.ts
17
17
  /**
18
18
  * Copyright © 2023-2026 Blockchain Commons, LLC
@@ -62,12 +62,21 @@ var Cli = class Cli {
62
62
  /** The RNG source (internal state). */
63
63
  rng;
64
64
  /**
65
- * Get input from argument or read from stdin.
66
- * Matches Rust expect_input method.
65
+ * Get input from argument or lazily read it from stdin.
66
+ *
67
+ * Mirrors Rust's `Cli::expect_input` (seedtool-cli-rust/src/cli.rs:191-200):
68
+ * stdin is touched only when an input-consuming code path actually needs it.
69
+ * Modes like `--in random` or `-d <SEED>` (deterministic) never call this,
70
+ * so they exit cleanly even when stdin is a non-TTY but no input is piped.
71
+ *
72
+ * Reads the stdin file descriptor synchronously via `fs.readFileSync(0, ...)`
73
+ * and caches the trimmed result on `this.input` so subsequent calls don't
74
+ * re-read.
67
75
  */
68
76
  expectInput() {
69
77
  if (this.input !== void 0) return this.input;
70
- throw new Error("Input required but not provided. Use stdin or pass as argument.");
78
+ this.input = fs.readFileSync(0, "utf-8").trim();
79
+ return this.input;
71
80
  }
72
81
  /**
73
82
  * Get input from argument or read from stdin asynchronously.
@@ -386,18 +395,34 @@ function dataToHex(bytes) {
386
395
  }
387
396
  /**
388
397
  * Convert hex string to bytes.
389
- * Matches Rust hex_to_data function.
398
+ *
399
+ * Mirrors the error wording produced by Rust's `hex` crate
400
+ * (`FromHexError::OddLength` and `FromHexError::InvalidHexCharacter`),
401
+ * which seedtool-cli-rust surfaces unchanged through anyhow:
402
+ *
403
+ * "Odd number of digits"
404
+ * "Invalid character '{c}' at position {n}"
405
+ *
406
+ * The outer CLI layer adds the `Error: ` prefix to match Rust's anyhow
407
+ * output.
390
408
  */
391
409
  function hexToData(hex) {
392
- if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
410
+ if (hex.length % 2 !== 0) throw new Error("Odd number of digits");
393
411
  const bytes = new Uint8Array(hex.length / 2);
394
412
  for (let i = 0; i < hex.length; i += 2) {
395
- const byte = parseInt(hex.substring(i, i + 2), 16);
396
- if (isNaN(byte)) throw new Error(`Invalid hex character at position ${i}`);
397
- bytes[i / 2] = byte;
413
+ const hi = hexCharToNibble(hex, i);
414
+ const lo = hexCharToNibble(hex, i + 1);
415
+ bytes[i / 2] = hi << 4 | lo;
398
416
  }
399
417
  return bytes;
400
418
  }
419
+ function hexCharToNibble(hex, index) {
420
+ const c = hex.charCodeAt(index);
421
+ if (c >= 48 && c <= 57) return c - 48;
422
+ if (c >= 97 && c <= 102) return c - 97 + 10;
423
+ if (c >= 65 && c <= 70) return c - 65 + 10;
424
+ throw new Error(`Invalid character '${hex[index]}' at position ${index}`);
425
+ }
401
426
  /**
402
427
  * Convert byte values to a different base range [0, base-1].
403
428
  * Each byte (0-255) is scaled proportionally to the target base.
@@ -475,7 +500,7 @@ function digitsToData(inStr, low, high) {
475
500
  const result = [];
476
501
  for (const c of inStr) {
477
502
  const n = c.charCodeAt(0) - "0".charCodeAt(0);
478
- if (n < low || n > high) throw new Error(`Invalid digit: ${c}. Expected range [${low}-${high}].`);
503
+ if (n < low || n > high) throw new Error("Invalid digit.");
479
504
  result.push(n);
480
505
  }
481
506
  return new Uint8Array(result);
@@ -739,7 +764,7 @@ var MultipartFormat = class {
739
764
  decoder.receive(share);
740
765
  if (decoder.isComplete()) break;
741
766
  }
742
- if (!decoder.isComplete()) throw new Error("Insufficient multipart shares");
767
+ if (!decoder.isComplete()) throw new Error("Insufficient SSKR shares");
743
768
  const ur = decoder.message();
744
769
  if (ur === void 0 || ur === null) throw new Error("Failed to decode multipart message");
745
770
  const envelope = Envelope.fromUR(ur);
@@ -991,7 +1016,7 @@ const CARD_RANKS = "a23456789tjqk";
991
1016
  function parseRank(c) {
992
1017
  const lower = c.toLowerCase();
993
1018
  const index = CARD_RANKS.indexOf(lower);
994
- if (index === -1) throw new Error(`Invalid card rank: ${c}. Allowed: [A,2-9,T,J,Q,K]`);
1019
+ if (index === -1) throw new Error("Invalid card rank. Allowed: [A,2-9,T,J,Q,K]");
995
1020
  return index;
996
1021
  }
997
1022
  /**
@@ -1001,7 +1026,7 @@ function parseRank(c) {
1001
1026
  function parseSuit(c) {
1002
1027
  const lower = c.toLowerCase();
1003
1028
  const index = CARD_SUITS.indexOf(lower);
1004
- if (index === -1) throw new Error(`Invalid card suit: ${c}. Allowed: [C,D,H,S]`);
1029
+ if (index === -1) throw new Error("Invalid card rank. Allowed: [D,C,H,S]");
1005
1030
  return index;
1006
1031
  }
1007
1032
  /**
@@ -1200,7 +1225,74 @@ function selectOutputFormat(key) {
1200
1225
  * A tool for generating and transforming cryptographic seeds.
1201
1226
  * Ported from seedtool-cli-rust/src/main.rs
1202
1227
  */
1203
- const VERSION = "0.4.0";
1228
+ /**
1229
+ * Package version, sourced from `package.json` so the CLI's `--version` output
1230
+ * never drifts from the published version.
1231
+ */
1232
+ const VERSION = "1.0.0-beta.0";
1233
+ /**
1234
+ * Build an argParser that validates a value against the given choices and,
1235
+ * on failure, emits the clap-style error format used by Rust's seedtool-cli:
1236
+ *
1237
+ * error: invalid value 'X' for '--<long> <UPPER>'
1238
+ * [possible values: a, b, c]
1239
+ *
1240
+ * For more information, try '--help'.
1241
+ *
1242
+ * `metavar` is the value-name placeholder (e.g. INPUT_TYPE), normally derived
1243
+ * from the option's `<META>` declaration. Used in conjunction with `.choices()`
1244
+ * for help-text generation — argParser runs first, so on a bad value we exit
1245
+ * before commander's own choice-validation message can fire.
1246
+ */
1247
+ function clapChoiceParser(longName, metavar, choices) {
1248
+ return (value) => {
1249
+ if (choices.includes(value)) return value;
1250
+ process.stderr.write(`error: invalid value '${value}' for '--${longName} <${metavar}>'\n [possible values: ${choices.join(", ")}]\n\nFor more information, try '--help'.\n`);
1251
+ process.exit(2);
1252
+ };
1253
+ }
1254
+ const IN_CHOICES = [
1255
+ "random",
1256
+ "hex",
1257
+ "btw",
1258
+ "btwu",
1259
+ "btwm",
1260
+ "bits",
1261
+ "cards",
1262
+ "dice",
1263
+ "base6",
1264
+ "base10",
1265
+ "ints",
1266
+ "bip39",
1267
+ "sskr",
1268
+ "envelope",
1269
+ "multipart",
1270
+ "seed"
1271
+ ];
1272
+ const OUT_CHOICES = [
1273
+ "hex",
1274
+ "btw",
1275
+ "btwu",
1276
+ "btwm",
1277
+ "bits",
1278
+ "cards",
1279
+ "dice",
1280
+ "base6",
1281
+ "base10",
1282
+ "ints",
1283
+ "bip39",
1284
+ "sskr",
1285
+ "envelope",
1286
+ "multipart",
1287
+ "seed"
1288
+ ];
1289
+ const SSKR_FORMAT_CHOICES = [
1290
+ "envelope",
1291
+ "btw",
1292
+ "btwm",
1293
+ "btwu",
1294
+ "ur"
1295
+ ];
1204
1296
  function parseLowInt(value) {
1205
1297
  const num = parseInt(value, 10);
1206
1298
  if (isNaN(num) || num < 0 || num > 254) throw new Error("LOW must be between 0 and 254");
@@ -1228,52 +1320,12 @@ function parseGroupSpec(value, previous) {
1228
1320
  }
1229
1321
  function main() {
1230
1322
  const program = new Command();
1231
- 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([
1232
- "random",
1233
- "hex",
1234
- "btw",
1235
- "btwm",
1236
- "btwu",
1237
- "bits",
1238
- "cards",
1239
- "dice",
1240
- "base6",
1241
- "base10",
1242
- "ints",
1243
- "bip39",
1244
- "sskr",
1245
- "envelope",
1246
- "seed",
1247
- "multipart"
1248
- ]).default("random")).addOption(new Option("-o, --out <OUTPUT_TYPE>", "The output format.").choices([
1249
- "hex",
1250
- "btw",
1251
- "btwm",
1252
- "btwu",
1253
- "bits",
1254
- "cards",
1255
- "dice",
1256
- "base6",
1257
- "base10",
1258
- "ints",
1259
- "bip39",
1260
- "sskr",
1261
- "envelope",
1262
- "seed",
1263
- "multipart"
1264
- ]).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([
1265
- "envelope",
1266
- "btw",
1267
- "btwm",
1268
- "btwu",
1269
- "ur"
1270
- ]).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.");
1323
+ 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(`@bcts/seedtool-cli ${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([...IN_CHOICES]).argParser(clapChoiceParser("in", "INPUT_TYPE", IN_CHOICES)).default("random")).addOption(new Option("-o, --out <OUTPUT_TYPE>", "The output format").choices([...OUT_CHOICES]).argParser(clapChoiceParser("out", "OUTPUT_TYPE", OUT_CHOICES)).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>", "Output format").choices([...SSKR_FORMAT_CHOICES]).argParser(clapChoiceParser("sskr-format", "SSKR_FORMAT", SSKR_FORMAT_CHOICES)).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");
1271
1324
  program.parse();
1272
1325
  const options = program.opts();
1273
1326
  const args = program.args;
1274
1327
  const cli = new Cli();
1275
1328
  if (args.length > 0) cli.input = args[0];
1276
- else if (process.stdin.isTTY !== true) cli.input = fs.readFileSync(process.stdin.fd, "utf-8").trim();
1277
1329
  cli.count = parseInt(options.count, 10);
1278
1330
  cli.in = options.in;
1279
1331
  cli.out = options.out;
@@ -1309,7 +1361,8 @@ try {
1309
1361
  main();
1310
1362
  } catch (error) {
1311
1363
  const message = error instanceof Error ? error.message : String(error);
1312
- console.error(message);
1364
+ const prefixed = message.startsWith("Error: ") ? message : `Error: ${message}`;
1365
+ console.error(prefixed);
1313
1366
  process.exit(1);
1314
1367
  }
1315
1368
  //#endregion