@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/src/random.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Random number generation utilities
3
+ * Ported from seedtool-cli-rust/src/random.rs
4
+ */
5
+
6
+ import { sha256 } from "@noble/hashes/sha256";
7
+ import { hkdf } from "@noble/hashes/hkdf";
8
+
9
+ /** SHA256 output size in bytes */
10
+ const SHA256_SIZE = 32;
11
+
12
+ /**
13
+ * Deterministic random number generator.
14
+ * Matches Rust DeterministicRandomNumberGenerator struct.
15
+ *
16
+ * Uses HKDF-HMAC-SHA256 to generate deterministic random data
17
+ * from a seed, with an incrementing salt for each call.
18
+ */
19
+ export class DeterministicRandomNumberGenerator {
20
+ private seed: Uint8Array;
21
+ private salt: bigint;
22
+
23
+ /**
24
+ * Create a new deterministic RNG from a 32-byte seed.
25
+ */
26
+ constructor(seed: Uint8Array) {
27
+ if (seed.length !== SHA256_SIZE) {
28
+ throw new Error(`Seed must be ${SHA256_SIZE} bytes, got ${seed.length}`);
29
+ }
30
+ this.seed = new Uint8Array(seed);
31
+ this.salt = 0n;
32
+ }
33
+
34
+ /**
35
+ * Create a new deterministic RNG from a seed string.
36
+ * The string is hashed with SHA256 to produce the seed.
37
+ * Matches Rust new_with_seed function.
38
+ */
39
+ static newWithSeed(seedString: string): DeterministicRandomNumberGenerator {
40
+ const encoder = new TextEncoder();
41
+ const seed = sha256(encoder.encode(seedString));
42
+ return new DeterministicRandomNumberGenerator(seed);
43
+ }
44
+
45
+ /**
46
+ * Generate deterministic random data.
47
+ * Matches Rust deterministic_random_data method.
48
+ *
49
+ * Each call increments the salt and uses HKDF to derive
50
+ * the requested number of bytes.
51
+ */
52
+ deterministicRandomData(size: number): Uint8Array {
53
+ this.salt += 1n;
54
+
55
+ // Convert salt to little-endian bytes
56
+ const saltBytes = new Uint8Array(8);
57
+ const view = new DataView(saltBytes.buffer);
58
+ // Split into low and high 32-bit parts for BigInt handling
59
+ const low = Number(this.salt & 0xffffffffn);
60
+ const high = Number((this.salt >> 32n) & 0xffffffffn);
61
+ view.setUint32(0, low, true); // little-endian
62
+ view.setUint32(4, high, true);
63
+
64
+ return hkdfHmacSha256(this.seed, saltBytes, size);
65
+ }
66
+
67
+ /**
68
+ * Clone the RNG state.
69
+ */
70
+ clone(): DeterministicRandomNumberGenerator {
71
+ const rng = new DeterministicRandomNumberGenerator(this.seed);
72
+ rng.salt = this.salt;
73
+ return rng;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * HKDF-HMAC-SHA256 key derivation.
79
+ * Matches Rust hkdf_hmac_sha256 function from bc-crypto.
80
+ */
81
+ export function hkdfHmacSha256(ikm: Uint8Array, salt: Uint8Array, length: number): Uint8Array {
82
+ // @noble/hashes hkdf takes (hash, ikm, salt, info, length)
83
+ // Use empty info for our use case
84
+ return hkdf(sha256, ikm, salt, new Uint8Array(0), length);
85
+ }
86
+
87
+ /**
88
+ * Generate deterministic random data from entropy using SHA256.
89
+ * If n <= 32, returns the first n bytes of SHA256(entropy).
90
+ * Matches Rust sha256_deterministic_random function.
91
+ *
92
+ * @param entropy - The entropy bytes to hash
93
+ * @param n - Number of bytes to return (must be <= 32)
94
+ * @throws Error if n > 32
95
+ */
96
+ export function sha256DeterministicRandom(entropy: Uint8Array, n: number): Uint8Array {
97
+ const seed = sha256(entropy);
98
+ if (n <= seed.length) {
99
+ return seed.slice(0, n);
100
+ } else {
101
+ throw new Error("Random number generator limits reached.");
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Generate deterministic random data from a string using SHA256.
107
+ * Matches Rust sha256_deterministic_random_string function.
108
+ *
109
+ * @param str - The string to hash
110
+ * @param n - Number of bytes to return (must be <= 32)
111
+ * @throws Error if n > 32
112
+ */
113
+ export function sha256DeterministicRandomString(str: string, n: number): Uint8Array {
114
+ const encoder = new TextEncoder();
115
+ const entropy = encoder.encode(str);
116
+ return sha256DeterministicRandom(entropy, n);
117
+ }
118
+
119
+ /**
120
+ * Generate deterministic random data from entropy using HKDF.
121
+ * This can generate any length output.
122
+ * Matches Rust deterministic_random function.
123
+ *
124
+ * @param entropy - The entropy bytes
125
+ * @param n - Number of bytes to return
126
+ */
127
+ export function deterministicRandom(entropy: Uint8Array, n: number): Uint8Array {
128
+ const seed = sha256(entropy);
129
+ return hkdfHmacSha256(seed, new Uint8Array(0), n);
130
+ }
package/src/seed.ts ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Seed type for seedtool-cli
3
+ * Ported from seedtool-cli-rust/src/seed.rs
4
+ *
5
+ * This is a local Seed type that wraps the seed data with metadata
6
+ * and provides Envelope conversion. It differs from @bcts/components Seed
7
+ * in that it doesn't enforce minimum length (for CLI flexibility) and
8
+ * has direct Envelope conversion methods.
9
+ */
10
+
11
+ import { Envelope } from "@bcts/envelope";
12
+ import { Seed as ComponentsSeed } from "@bcts/components";
13
+ import { NAME, NOTE, DATE, SEED_TYPE } from "@bcts/known-values";
14
+ import { toByteString, CborDate } from "@bcts/dcbor";
15
+
16
+ /**
17
+ * Seed with optional metadata.
18
+ * Matches Rust Seed struct in seed.rs.
19
+ */
20
+ export class Seed {
21
+ private _data: Uint8Array;
22
+ private _name: string;
23
+ private _note: string;
24
+ private _creationDate?: Date;
25
+
26
+ /**
27
+ * Create a new Seed with the given data.
28
+ * Matches Rust Seed::new function.
29
+ */
30
+ constructor(data: Uint8Array) {
31
+ this._data = new Uint8Array(data);
32
+ this._name = "";
33
+ this._note = "";
34
+ this._creationDate = undefined;
35
+ }
36
+
37
+ /**
38
+ * Create a new Seed with data and optional metadata.
39
+ * Matches Rust Seed::new_opt function.
40
+ */
41
+ static newOpt(data: Uint8Array, name: string, note: string, creationDate?: Date): Seed {
42
+ const seed = new Seed(data);
43
+ seed._name = name;
44
+ seed._note = note;
45
+ seed._creationDate = creationDate;
46
+ return seed;
47
+ }
48
+
49
+ /**
50
+ * Create a new Seed from raw data.
51
+ * Convenience factory method.
52
+ */
53
+ static new(data: Uint8Array): Seed {
54
+ return new Seed(data);
55
+ }
56
+
57
+ // ============================================================================
58
+ // Accessors
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Get the seed data.
63
+ * Matches Rust seed.data() method.
64
+ */
65
+ data(): Uint8Array {
66
+ return this._data;
67
+ }
68
+
69
+ /**
70
+ * Get the seed name.
71
+ * Matches Rust seed.name() method.
72
+ * Returns empty string if not set.
73
+ */
74
+ name(): string {
75
+ return this._name;
76
+ }
77
+
78
+ /**
79
+ * Set the seed name.
80
+ * Matches Rust seed.set_name() method.
81
+ */
82
+ setName(name: string): void {
83
+ this._name = name;
84
+ }
85
+
86
+ /**
87
+ * Get the seed note.
88
+ * Matches Rust seed.note() method.
89
+ * Returns empty string if not set.
90
+ */
91
+ note(): string {
92
+ return this._note;
93
+ }
94
+
95
+ /**
96
+ * Set the seed note.
97
+ * Matches Rust seed.set_note() method.
98
+ */
99
+ setNote(note: string): void {
100
+ this._note = note;
101
+ }
102
+
103
+ /**
104
+ * Get the creation date.
105
+ * Matches Rust seed.creation_date() method.
106
+ */
107
+ creationDate(): Date | undefined {
108
+ return this._creationDate;
109
+ }
110
+
111
+ /**
112
+ * Set the creation date.
113
+ * Matches Rust seed.set_creation_date() method.
114
+ */
115
+ setCreationDate(date: Date | undefined): void {
116
+ this._creationDate = date;
117
+ }
118
+
119
+ // ============================================================================
120
+ // Cloning
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Clone the seed.
125
+ */
126
+ clone(): Seed {
127
+ return Seed.newOpt(new Uint8Array(this._data), this._name, this._note, this._creationDate);
128
+ }
129
+
130
+ // ============================================================================
131
+ // Envelope Conversion
132
+ // ============================================================================
133
+
134
+ /**
135
+ * Convert to Envelope.
136
+ * Matches Rust impl From<Seed> for Envelope.
137
+ *
138
+ * Creates an envelope with:
139
+ * - Subject: byte string of seed data
140
+ * - Type assertion: 'Seed'
141
+ * - Optional date assertion
142
+ * - Optional name assertion (if not empty)
143
+ * - Optional note assertion (if not empty)
144
+ */
145
+ toEnvelope(): Envelope {
146
+ // Create envelope with seed data as byte string subject
147
+ let envelope = Envelope.new(toByteString(this._data));
148
+
149
+ // Add type assertion
150
+ envelope = envelope.addType(SEED_TYPE);
151
+
152
+ // Add optional date assertion (using CBOR Date tag 1)
153
+ if (this._creationDate !== undefined) {
154
+ const cborDate = CborDate.fromDatetime(this._creationDate);
155
+ envelope = envelope.addAssertion(DATE, cborDate);
156
+ }
157
+
158
+ // Add optional name assertion (only if not empty)
159
+ if (this._name.length > 0) {
160
+ envelope = envelope.addAssertion(NAME, this._name);
161
+ }
162
+
163
+ // Add optional note assertion (only if not empty)
164
+ if (this._note.length > 0) {
165
+ envelope = envelope.addAssertion(NOTE, this._note);
166
+ }
167
+
168
+ return envelope;
169
+ }
170
+
171
+ /**
172
+ * Create a Seed from an Envelope.
173
+ * Matches Rust impl TryFrom<Envelope> for Seed.
174
+ */
175
+ static fromEnvelope(envelope: Envelope): Seed {
176
+ // Check type
177
+ envelope.checkTypeValue(SEED_TYPE);
178
+
179
+ // Extract data from subject (byte string)
180
+ const subject = envelope.subject();
181
+ const leaf = subject.asLeaf();
182
+ if (leaf === undefined) {
183
+ throw new Error("Seed envelope must have a leaf subject");
184
+ }
185
+ const data = leaf.asByteString();
186
+ if (data === undefined) {
187
+ throw new Error("Seed envelope subject must be a byte string");
188
+ }
189
+
190
+ // Extract optional name
191
+ let name = "";
192
+ try {
193
+ const nameObj = envelope.objectForPredicate(NAME);
194
+ if (nameObj !== undefined) {
195
+ const nameStr = nameObj.asText();
196
+ if (nameStr !== undefined) {
197
+ name = nameStr;
198
+ }
199
+ }
200
+ } catch {
201
+ // Name is optional
202
+ }
203
+
204
+ // Extract optional note
205
+ let note = "";
206
+ try {
207
+ const noteObj = envelope.objectForPredicate(NOTE);
208
+ if (noteObj !== undefined) {
209
+ const noteStr = noteObj.asText();
210
+ if (noteStr !== undefined) {
211
+ note = noteStr;
212
+ }
213
+ }
214
+ } catch {
215
+ // Note is optional
216
+ }
217
+
218
+ // Extract optional creation date (CBOR Date tag 1)
219
+ let creationDate: Date | undefined;
220
+ try {
221
+ const dateObj = envelope.objectForPredicate(DATE);
222
+ if (dateObj !== undefined) {
223
+ // Try to extract as CborDate first (tag 1)
224
+ const leaf = dateObj.asLeaf();
225
+ if (leaf !== undefined) {
226
+ const cborDate = CborDate.fromTaggedCbor(leaf);
227
+ creationDate = cborDate.datetime();
228
+ }
229
+ }
230
+ } catch {
231
+ // Date is optional, or might be a different format - try ISO string fallback
232
+ try {
233
+ const dateObj = envelope.objectForPredicate(DATE);
234
+ if (dateObj !== undefined) {
235
+ const dateStr = dateObj.asText();
236
+ if (dateStr !== undefined) {
237
+ creationDate = new Date(dateStr);
238
+ }
239
+ }
240
+ } catch {
241
+ // Date is optional
242
+ }
243
+ }
244
+
245
+ return Seed.newOpt(data, name, note, creationDate);
246
+ }
247
+
248
+ // ============================================================================
249
+ // ComponentsSeed Conversion
250
+ // ============================================================================
251
+
252
+ /**
253
+ * Convert to @bcts/components Seed.
254
+ * Matches Rust impl TryFrom<&Seed> for ComponentsSeed.
255
+ */
256
+ toComponentsSeed(): ComponentsSeed {
257
+ return ComponentsSeed.newOpt(
258
+ this._data,
259
+ this._name.length > 0 ? this._name : undefined,
260
+ this._note.length > 0 ? this._note : undefined,
261
+ this._creationDate,
262
+ );
263
+ }
264
+
265
+ /**
266
+ * Create from @bcts/components Seed.
267
+ * Matches Rust impl From<ComponentsSeed> for Seed.
268
+ */
269
+ static fromComponentsSeed(seed: ComponentsSeed): Seed {
270
+ return Seed.newOpt(seed.asBytes(), seed.name(), seed.note(), seed.creationDate());
271
+ }
272
+
273
+ // ============================================================================
274
+ // String Representation
275
+ // ============================================================================
276
+
277
+ /**
278
+ * Get string representation.
279
+ */
280
+ toString(): string {
281
+ const hex = Array.from(this._data.slice(0, 8))
282
+ .map((b) => b.toString(16).padStart(2, "0"))
283
+ .join("");
284
+ return `Seed(${hex}..., ${this._data.length} bytes)`;
285
+ }
286
+
287
+ /**
288
+ * Check equality with another Seed.
289
+ */
290
+ equals(other: Seed): boolean {
291
+ if (this._data.length !== other._data.length) return false;
292
+ for (let i = 0; i < this._data.length; i++) {
293
+ if (this._data[i] !== other._data[i]) return false;
294
+ }
295
+ if (this._name !== other._name) return false;
296
+ if (this._note !== other._note) return false;
297
+ if (this._creationDate?.getTime() !== other._creationDate?.getTime()) return false;
298
+ return true;
299
+ }
300
+ }
package/src/styles.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CLI styling utilities
3
+ * Ported from seedtool-cli-rust/src/styles.rs
4
+ */
5
+
6
+ import chalk from "chalk";
7
+
8
+ /**
9
+ * Get CLI styles matching the Rust clap styling.
10
+ * Matches Rust get_styles function.
11
+ *
12
+ * These styles are used for CLI help text formatting:
13
+ * - usage: Bold, underlined, yellow - for usage section headers
14
+ * - header: Bold, underlined, yellow - for section headers
15
+ * - literal: Green - for command/argument literals
16
+ * - invalid: Bold, red - for invalid input indicators
17
+ * - error: Bold, red - for error messages
18
+ * - valid: Bold, underlined, green - for valid input indicators
19
+ * - placeholder: Bright cyan - for placeholder values
20
+ */
21
+ export const styles = {
22
+ /** Style for usage section headers */
23
+ usage: chalk.bold.underline.yellow,
24
+
25
+ /** Style for section headers */
26
+ header: chalk.bold.underline.yellow,
27
+
28
+ /** Style for command/argument literals */
29
+ literal: chalk.green,
30
+
31
+ /** Style for invalid input indicators */
32
+ invalid: chalk.bold.red,
33
+
34
+ /** Style for error messages */
35
+ error: chalk.bold.red,
36
+
37
+ /** Style for valid input indicators */
38
+ valid: chalk.bold.underline.green,
39
+
40
+ /** Style for placeholder values */
41
+ placeholder: chalk.cyanBright,
42
+ };
43
+
44
+ /**
45
+ * Get styles object for use with commander.js
46
+ * Note: Commander uses different styling API than clap
47
+ */
48
+ export function getStyles() {
49
+ return styles;
50
+ }
package/src/util.ts ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Utility functions for data conversion
3
+ * Ported from seedtool-cli-rust/src/util.rs
4
+ */
5
+
6
+ /**
7
+ * Convert bytes to hex string.
8
+ * Matches Rust data_to_hex function.
9
+ */
10
+ export function dataToHex(bytes: Uint8Array): string {
11
+ return Array.from(bytes)
12
+ .map((b) => b.toString(16).padStart(2, "0"))
13
+ .join("");
14
+ }
15
+
16
+ /**
17
+ * Convert hex string to bytes.
18
+ * Matches Rust hex_to_data function.
19
+ */
20
+ export function hexToData(hex: string): Uint8Array {
21
+ if (hex.length % 2 !== 0) {
22
+ throw new Error("Hex string must have even length");
23
+ }
24
+ const bytes = new Uint8Array(hex.length / 2);
25
+ for (let i = 0; i < hex.length; i += 2) {
26
+ const byte = parseInt(hex.substring(i, i + 2), 16);
27
+ if (isNaN(byte)) {
28
+ throw new Error(`Invalid hex character at position ${i}`);
29
+ }
30
+ bytes[i / 2] = byte;
31
+ }
32
+ return bytes;
33
+ }
34
+
35
+ /**
36
+ * Convert byte values to a different base range [0, base-1].
37
+ * Each byte (0-255) is scaled proportionally to the target base.
38
+ * Matches Rust data_to_base function.
39
+ *
40
+ * @param buf - Input bytes
41
+ * @param base - Target base (e.g., 6 for base-6)
42
+ * @returns Array of values in range [0, base-1]
43
+ */
44
+ export function dataToBase(buf: Uint8Array, base: number): Uint8Array {
45
+ const result = new Uint8Array(buf.length);
46
+ for (let i = 0; i < buf.length; i++) {
47
+ // Scale from [0,255] to [0,base-1] with rounding
48
+ result[i] = Math.round((buf[i] / 255) * (base - 1));
49
+ }
50
+ return result;
51
+ }
52
+
53
+ /**
54
+ * Convert bytes to an alphabet string using a base and alphabet function.
55
+ * Matches Rust data_to_alphabet function.
56
+ *
57
+ * @param buf - Input bytes
58
+ * @param base - Target base
59
+ * @param toAlphabet - Function to convert index to character
60
+ * @returns String of alphabet characters
61
+ */
62
+ export function dataToAlphabet(
63
+ buf: Uint8Array,
64
+ base: number,
65
+ toAlphabet: (n: number) => string,
66
+ ): string {
67
+ const data = dataToBase(buf, base);
68
+ return Array.from(data)
69
+ .map((b) => toAlphabet(b))
70
+ .join("");
71
+ }
72
+
73
+ /**
74
+ * Parse whitespace-separated integers.
75
+ * Matches Rust parse_ints function.
76
+ *
77
+ * @param input - Space-separated integer string
78
+ * @returns Array of bytes
79
+ * @throws Error if any integer is out of range [0, 255]
80
+ */
81
+ export function parseInts(input: string): Uint8Array {
82
+ const parts = input.trim().split(/\s+/);
83
+ const result: number[] = [];
84
+ for (const s of parts) {
85
+ if (s === "") continue;
86
+ const i = parseInt(s, 10);
87
+ if (isNaN(i)) {
88
+ throw new Error(`Invalid integer: ${s}`);
89
+ }
90
+ if (i < 0 || i > 255) {
91
+ throw new Error("Integer out of range. Allowed: [0-255]");
92
+ }
93
+ result.push(i);
94
+ }
95
+ return new Uint8Array(result);
96
+ }
97
+
98
+ /**
99
+ * Convert bytes to a string of integers in a given range.
100
+ * Matches Rust data_to_ints function.
101
+ *
102
+ * @param buf - Input bytes
103
+ * @param low - Lowest output value (0-254)
104
+ * @param high - Highest output value (1-255), low < high
105
+ * @param separator - String to separate values
106
+ * @returns String of integers
107
+ * @throws Error if range is invalid
108
+ */
109
+ export function dataToInts(buf: Uint8Array, low: number, high: number, separator: string): string {
110
+ if (!(low < high && high <= 255)) {
111
+ throw new Error("Int conversion range must be in 0 <= low < high <= 255.");
112
+ }
113
+ const base = high - low + 1;
114
+ const data = dataToBase(buf, base);
115
+ return Array.from(data)
116
+ .map((b) => (b + low).toString())
117
+ .join(separator);
118
+ }
119
+
120
+ /**
121
+ * Parse a string of digits in a given range to bytes.
122
+ * Matches Rust digits_to_data function.
123
+ *
124
+ * @param inStr - String of digits
125
+ * @param low - Lowest valid digit
126
+ * @param high - Highest valid digit
127
+ * @returns Array of digit values
128
+ * @throws Error if any digit is out of range
129
+ */
130
+ export function digitsToData(inStr: string, low: number, high: number): Uint8Array {
131
+ const result: number[] = [];
132
+ for (const c of inStr) {
133
+ const n = c.charCodeAt(0) - "0".charCodeAt(0);
134
+ if (n < low || n > high) {
135
+ throw new Error(`Invalid digit: ${c}. Expected range [${low}-${high}].`);
136
+ }
137
+ result.push(n);
138
+ }
139
+ return new Uint8Array(result);
140
+ }