@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/index.cjs +39 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +20 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +38 -12
- package/dist/index.mjs.map +1 -1
- package/dist/main.mjs +109 -56
- package/dist/main.mjs.map +1 -1
- package/package.json +9 -9
- package/src/cli.ts +13 -5
- package/src/formats/cards.ts +7 -2
- package/src/formats/multipart.ts +5 -1
- package/src/main.ts +103 -54
- package/src/util.ts +28 -8
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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("
|
|
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
|
|
396
|
-
|
|
397
|
-
bytes[i / 2] =
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1364
|
+
const prefixed = message.startsWith("Error: ") ? message : `Error: ${message}`;
|
|
1365
|
+
console.error(prefixed);
|
|
1313
1366
|
process.exit(1);
|
|
1314
1367
|
}
|
|
1315
1368
|
//#endregion
|