@bcts/provenance-mark-cli 1.0.0-alpha.13
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/LICENSE +48 -0
- package/README.md +177 -0
- package/dist/cli.cjs +129 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +130 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +27 -0
- package/dist/index.d.cts +332 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +332 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/src-Bu1tG89l.mjs +705 -0
- package/dist/src-Bu1tG89l.mjs.map +1 -0
- package/dist/src-XNNUeTzh.cjs +889 -0
- package/dist/src-XNNUeTzh.cjs.map +1 -0
- package/package.json +88 -0
- package/src/cli.ts +243 -0
- package/src/cmd/index.ts +12 -0
- package/src/cmd/info.ts +147 -0
- package/src/cmd/new.ts +235 -0
- package/src/cmd/next.ts +119 -0
- package/src/cmd/print.ts +108 -0
- package/src/cmd/seed.ts +114 -0
- package/src/cmd/validate.ts +288 -0
- package/src/exec.ts +23 -0
- package/src/index.ts +55 -0
- package/src/utils.ts +130 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate command - 1:1 port of validate.rs
|
|
3
|
+
*
|
|
4
|
+
* Validate one or more provenance marks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { UR } from "@bcts/uniform-resources";
|
|
10
|
+
import { Envelope } from "@bcts/envelope";
|
|
11
|
+
import { PROVENANCE } from "@bcts/known-values";
|
|
12
|
+
import {
|
|
13
|
+
ProvenanceMark,
|
|
14
|
+
ProvenanceMarkInfo,
|
|
15
|
+
ValidationReportFormat,
|
|
16
|
+
validate,
|
|
17
|
+
hasIssues,
|
|
18
|
+
formatReport,
|
|
19
|
+
} from "@bcts/provenance-mark";
|
|
20
|
+
|
|
21
|
+
import type { Exec } from "../exec.js";
|
|
22
|
+
import { readExistingDirectoryPath } from "../utils.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Output format for the validation report.
|
|
26
|
+
*
|
|
27
|
+
* Corresponds to Rust `Format`
|
|
28
|
+
*/
|
|
29
|
+
export enum ValidateFormat {
|
|
30
|
+
Text = "text",
|
|
31
|
+
JsonCompact = "json-compact",
|
|
32
|
+
JsonPretty = "json-pretty",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert ValidateFormat to ValidationReportFormat.
|
|
37
|
+
*/
|
|
38
|
+
function formatToValidationReportFormat(format: ValidateFormat): ValidationReportFormat {
|
|
39
|
+
switch (format) {
|
|
40
|
+
case ValidateFormat.Text:
|
|
41
|
+
return ValidationReportFormat.Text;
|
|
42
|
+
case ValidateFormat.JsonCompact:
|
|
43
|
+
return ValidationReportFormat.JsonCompact;
|
|
44
|
+
case ValidateFormat.JsonPretty:
|
|
45
|
+
return ValidationReportFormat.JsonPretty;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a format string.
|
|
51
|
+
*/
|
|
52
|
+
export function parseValidateFormat(value: string): ValidateFormat {
|
|
53
|
+
switch (value.toLowerCase()) {
|
|
54
|
+
case "text":
|
|
55
|
+
return ValidateFormat.Text;
|
|
56
|
+
case "json-compact":
|
|
57
|
+
return ValidateFormat.JsonCompact;
|
|
58
|
+
case "json-pretty":
|
|
59
|
+
return ValidateFormat.JsonPretty;
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`Invalid format: ${value}. Must be one of: text, json-compact, json-pretty`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Arguments for the validate command.
|
|
67
|
+
*
|
|
68
|
+
* Corresponds to Rust `CommandArgs`
|
|
69
|
+
*/
|
|
70
|
+
export interface ValidateCommandArgs {
|
|
71
|
+
/** One or more provenance mark URs to validate. */
|
|
72
|
+
marks: string[];
|
|
73
|
+
/** Path to a chain directory containing marks to validate. */
|
|
74
|
+
dir?: string;
|
|
75
|
+
/** Report issues as warnings without failing. */
|
|
76
|
+
warn: boolean;
|
|
77
|
+
/** Output format for the validation report. */
|
|
78
|
+
format: ValidateFormat;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create default args for the validate command.
|
|
83
|
+
*/
|
|
84
|
+
export function defaultValidateCommandArgs(): ValidateCommandArgs {
|
|
85
|
+
return {
|
|
86
|
+
marks: [],
|
|
87
|
+
warn: false,
|
|
88
|
+
format: ValidateFormat.Text,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate command implementation.
|
|
94
|
+
*
|
|
95
|
+
* Corresponds to Rust `impl Exec for CommandArgs`
|
|
96
|
+
*/
|
|
97
|
+
export class ValidateCommand implements Exec {
|
|
98
|
+
private readonly args: ValidateCommandArgs;
|
|
99
|
+
|
|
100
|
+
constructor(args: ValidateCommandArgs) {
|
|
101
|
+
this.args = args;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
exec(): string {
|
|
105
|
+
// Collect marks from either URs or directory
|
|
106
|
+
let marks: ProvenanceMark[];
|
|
107
|
+
if (this.args.dir !== undefined) {
|
|
108
|
+
marks = this.loadMarksFromDir(this.args.dir);
|
|
109
|
+
} else {
|
|
110
|
+
marks = this.parseMarksFromUrs(this.args.marks);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Validate the marks
|
|
114
|
+
const report = validate(marks);
|
|
115
|
+
|
|
116
|
+
// Format the output
|
|
117
|
+
const output = formatReport(report, formatToValidationReportFormat(this.args.format));
|
|
118
|
+
|
|
119
|
+
// Determine if we should fail
|
|
120
|
+
if (hasIssues(report) && !this.args.warn) {
|
|
121
|
+
throw new Error(`Validation failed with issues:\n${output}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return output;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse marks from UR strings.
|
|
129
|
+
*
|
|
130
|
+
* Corresponds to Rust `parse_marks_from_urs()`
|
|
131
|
+
*/
|
|
132
|
+
private parseMarksFromUrs(urStrings: string[]): ProvenanceMark[] {
|
|
133
|
+
const marks: ProvenanceMark[] = [];
|
|
134
|
+
for (const urString of urStrings) {
|
|
135
|
+
const mark = this.extractProvenanceMark(urString.trim());
|
|
136
|
+
marks.push(mark);
|
|
137
|
+
}
|
|
138
|
+
return marks;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract a ProvenanceMark from a UR string.
|
|
143
|
+
*
|
|
144
|
+
* Supports three types of URs:
|
|
145
|
+
* 1. `ur:provenance` - Direct provenance mark
|
|
146
|
+
* 2. `ur:envelope` - Envelope with a 'provenance' assertion
|
|
147
|
+
* 3. Any other UR type - Attempts to decode CBOR as an envelope
|
|
148
|
+
*
|
|
149
|
+
* Corresponds to Rust `extract_provenance_mark()`
|
|
150
|
+
*/
|
|
151
|
+
private extractProvenanceMark(urString: string): ProvenanceMark {
|
|
152
|
+
// Parse the UR to get its type and CBOR
|
|
153
|
+
let ur: UR;
|
|
154
|
+
try {
|
|
155
|
+
ur = UR.fromURString(urString);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
158
|
+
throw new Error(`Failed to parse UR '${urString}': ${message}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const urType = ur.urTypeStr();
|
|
162
|
+
const cborValue = ur.cbor();
|
|
163
|
+
|
|
164
|
+
// Case 1: Direct provenance mark
|
|
165
|
+
// URs don't include the CBOR tag in their encoded format, so we use fromUntaggedCbor
|
|
166
|
+
if (urType === "provenance") {
|
|
167
|
+
try {
|
|
168
|
+
return ProvenanceMark.fromUntaggedCbor(cborValue);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
171
|
+
throw new Error(`Failed to decode provenance mark from '${urString}': ${message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Case 2 & 3: Try to decode CBOR as an envelope
|
|
176
|
+
let envelope: Envelope;
|
|
177
|
+
try {
|
|
178
|
+
envelope = Envelope.fromUntaggedCbor(cborValue);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
181
|
+
throw new Error(
|
|
182
|
+
`UR type '${urType}' is not 'provenance', and CBOR is not decodable as an envelope: ${message}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Extract the provenance mark from the envelope
|
|
187
|
+
return this.extractProvenanceMarkFromEnvelope(envelope, urString);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract a ProvenanceMark from an Envelope.
|
|
192
|
+
*
|
|
193
|
+
* The envelope must contain exactly one 'provenance' assertion,
|
|
194
|
+
* and the object subject of that assertion must be a ProvenanceMark.
|
|
195
|
+
*
|
|
196
|
+
* Corresponds to Rust `extract_provenance_mark_from_envelope()`
|
|
197
|
+
*/
|
|
198
|
+
private extractProvenanceMarkFromEnvelope(envelope: Envelope, urString: string): ProvenanceMark {
|
|
199
|
+
// If the envelope is wrapped, unwrap it to get to the actual content
|
|
200
|
+
let workingEnvelope = envelope;
|
|
201
|
+
if (envelope.isWrapped()) {
|
|
202
|
+
const innerEnvelope = envelope.unwrap();
|
|
203
|
+
if (innerEnvelope !== undefined) {
|
|
204
|
+
workingEnvelope = innerEnvelope;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Find all assertions with the 'provenance' predicate
|
|
209
|
+
const provenancePredicate = Envelope.newWithKnownValue(PROVENANCE);
|
|
210
|
+
const provenanceAssertions = workingEnvelope.assertionsWithPredicate(provenancePredicate);
|
|
211
|
+
|
|
212
|
+
// Verify exactly one provenance assertion exists
|
|
213
|
+
if (provenanceAssertions.length === 0) {
|
|
214
|
+
throw new Error(`Envelope in '${urString}' does not contain a 'provenance' assertion`);
|
|
215
|
+
}
|
|
216
|
+
if (provenanceAssertions.length > 1) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Envelope in '${urString}' contains ${provenanceAssertions.length} 'provenance' assertions, expected exactly one`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Get the object of the provenance assertion
|
|
223
|
+
const provenanceAssertion = provenanceAssertions[0];
|
|
224
|
+
const objectEnvelope = provenanceAssertion.asObject();
|
|
225
|
+
if (objectEnvelope === undefined) {
|
|
226
|
+
throw new Error(`Failed to extract object from provenance assertion in '${urString}'`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The object should be decodable as a ProvenanceMark.
|
|
230
|
+
// Extract the CBOR from the leaf envelope and parse it.
|
|
231
|
+
try {
|
|
232
|
+
const cborValue = objectEnvelope.asLeaf();
|
|
233
|
+
if (cborValue === undefined) {
|
|
234
|
+
throw new Error("Object envelope is not a leaf");
|
|
235
|
+
}
|
|
236
|
+
return ProvenanceMark.fromTaggedCbor(cborValue);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Failed to decode ProvenanceMark from provenance assertion in '${urString}': ${message}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Load marks from a directory.
|
|
247
|
+
*
|
|
248
|
+
* Corresponds to Rust `load_marks_from_dir()`
|
|
249
|
+
*/
|
|
250
|
+
private loadMarksFromDir(dirPath: string): ProvenanceMark[] {
|
|
251
|
+
// Get the chain's directory path
|
|
252
|
+
const resolvedPath = readExistingDirectoryPath(dirPath);
|
|
253
|
+
|
|
254
|
+
// Get the marks subdirectory
|
|
255
|
+
const marksPath = path.join(resolvedPath, "marks");
|
|
256
|
+
if (!fs.existsSync(marksPath) || !fs.statSync(marksPath).isDirectory()) {
|
|
257
|
+
throw new Error(`Marks subdirectory not found: ${marksPath}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Read all JSON files from the marks directory
|
|
261
|
+
const entries = fs.readdirSync(marksPath);
|
|
262
|
+
const markFiles = entries
|
|
263
|
+
.map((entry) => path.join(marksPath, entry))
|
|
264
|
+
.filter((p) => p.endsWith(".json"))
|
|
265
|
+
.sort();
|
|
266
|
+
|
|
267
|
+
if (markFiles.length === 0) {
|
|
268
|
+
throw new Error(`No mark JSON files found in: ${marksPath}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Parse each JSON file and extract the mark
|
|
272
|
+
const marks: ProvenanceMark[] = [];
|
|
273
|
+
for (const markFile of markFiles) {
|
|
274
|
+
try {
|
|
275
|
+
const jsonContent = fs.readFileSync(markFile, "utf-8");
|
|
276
|
+
const markInfo = ProvenanceMarkInfo.fromJSON(
|
|
277
|
+
JSON.parse(jsonContent) as Record<string, unknown>,
|
|
278
|
+
);
|
|
279
|
+
marks.push(markInfo.mark());
|
|
280
|
+
} catch (e) {
|
|
281
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
282
|
+
throw new Error(`Failed to parse JSON from ${markFile}: ${message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return marks;
|
|
287
|
+
}
|
|
288
|
+
}
|
package/src/exec.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exec interface - 1:1 port of exec.rs
|
|
3
|
+
*
|
|
4
|
+
* All CLI commands implement this interface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Result type for command execution.
|
|
9
|
+
* Commands return either a string output or throw an error.
|
|
10
|
+
*/
|
|
11
|
+
export type ExecResult = string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Interface that all CLI commands implement.
|
|
15
|
+
* Each command's exec() method returns a string output.
|
|
16
|
+
*/
|
|
17
|
+
export interface Exec {
|
|
18
|
+
/**
|
|
19
|
+
* Execute the command and return the output string.
|
|
20
|
+
* @throws Error if the command fails
|
|
21
|
+
*/
|
|
22
|
+
exec(): ExecResult;
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bcts/provenance-mark-cli - Command line tool for creating and managing Provenance Marks
|
|
3
|
+
*
|
|
4
|
+
* This is a 1:1 TypeScript port of provenance-mark-cli-rust.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const VERSION = "1.0.0-alpha.13";
|
|
10
|
+
|
|
11
|
+
// Export exec interface
|
|
12
|
+
export type { Exec, ExecResult } from "./exec.js";
|
|
13
|
+
|
|
14
|
+
// Export utilities
|
|
15
|
+
export {
|
|
16
|
+
readNewPath,
|
|
17
|
+
readExistingDirectoryPath,
|
|
18
|
+
readArgument,
|
|
19
|
+
readStdinSync,
|
|
20
|
+
bytesToHex,
|
|
21
|
+
hexToBytes,
|
|
22
|
+
toBase64,
|
|
23
|
+
fromBase64,
|
|
24
|
+
} from "./utils.js";
|
|
25
|
+
|
|
26
|
+
// Export command types and classes
|
|
27
|
+
export {
|
|
28
|
+
// Info args
|
|
29
|
+
type InfoArgs,
|
|
30
|
+
parseInfoArgs,
|
|
31
|
+
// Seed parsing
|
|
32
|
+
parseSeed,
|
|
33
|
+
// New command
|
|
34
|
+
OutputFormat,
|
|
35
|
+
Resolution,
|
|
36
|
+
parseResolution,
|
|
37
|
+
parseOutputFormat,
|
|
38
|
+
type NewCommandArgs,
|
|
39
|
+
defaultNewCommandArgs,
|
|
40
|
+
NewCommand,
|
|
41
|
+
// Next command
|
|
42
|
+
type NextCommandArgs,
|
|
43
|
+
defaultNextCommandArgs,
|
|
44
|
+
NextCommand,
|
|
45
|
+
// Print command
|
|
46
|
+
type PrintCommandArgs,
|
|
47
|
+
defaultPrintCommandArgs,
|
|
48
|
+
PrintCommand,
|
|
49
|
+
// Validate command
|
|
50
|
+
ValidateFormat,
|
|
51
|
+
parseValidateFormat,
|
|
52
|
+
type ValidateCommandArgs,
|
|
53
|
+
defaultValidateCommandArgs,
|
|
54
|
+
ValidateCommand,
|
|
55
|
+
} from "./cmd/index.js";
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities module - 1:1 port of utils.rs
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for CLI operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* eslint-disable no-restricted-globals, no-undef */
|
|
8
|
+
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read a new path, supporting globbing, and resolving relative paths.
|
|
14
|
+
*
|
|
15
|
+
* Corresponds to Rust `read_new_path()`
|
|
16
|
+
*/
|
|
17
|
+
export function readNewPath(pathStr: string): string {
|
|
18
|
+
// For TypeScript, we simplify glob handling - just resolve the path
|
|
19
|
+
let effectivePath: string;
|
|
20
|
+
|
|
21
|
+
if (path.isAbsolute(pathStr)) {
|
|
22
|
+
effectivePath = pathStr;
|
|
23
|
+
} else {
|
|
24
|
+
const currentDir = process.cwd();
|
|
25
|
+
effectivePath = path.join(currentDir, pathStr);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Normalize the path (resolve . and ..)
|
|
29
|
+
return path.normalize(effectivePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read an existing directory path, supporting globbing, and resolving relative paths.
|
|
34
|
+
*
|
|
35
|
+
* Corresponds to Rust `read_existing_directory_path()`
|
|
36
|
+
*/
|
|
37
|
+
export function readExistingDirectoryPath(pathStr: string): string {
|
|
38
|
+
const effectivePath = readNewPath(pathStr);
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(effectivePath)) {
|
|
41
|
+
throw new Error(`Path does not exist: ${effectivePath}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!fs.statSync(effectivePath).isDirectory()) {
|
|
45
|
+
throw new Error(`Path is not a directory: ${effectivePath}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return effectivePath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read an argument from command line or stdin.
|
|
53
|
+
*
|
|
54
|
+
* Corresponds to Rust `read_argument()`
|
|
55
|
+
*/
|
|
56
|
+
export function readArgument(argument?: string): string {
|
|
57
|
+
if (argument !== undefined && argument !== "") {
|
|
58
|
+
return argument;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read from stdin
|
|
62
|
+
const input = readStdinSync();
|
|
63
|
+
if (input.trim() === "") {
|
|
64
|
+
throw new Error("No argument provided");
|
|
65
|
+
}
|
|
66
|
+
return input.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read all stdin synchronously.
|
|
71
|
+
*/
|
|
72
|
+
export function readStdinSync(): string {
|
|
73
|
+
let input = "";
|
|
74
|
+
const BUFSIZE = 256;
|
|
75
|
+
const buf = Buffer.alloc(BUFSIZE);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Use process.stdin.fd (file descriptor 0) directly for reading
|
|
79
|
+
const fd = process.stdin.fd;
|
|
80
|
+
|
|
81
|
+
while (true) {
|
|
82
|
+
try {
|
|
83
|
+
const bytesRead = fs.readSync(fd, buf, 0, BUFSIZE, null);
|
|
84
|
+
if (bytesRead === 0) break;
|
|
85
|
+
input += buf.toString("utf8", 0, bytesRead);
|
|
86
|
+
} catch {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Fallback: stdin might not be readable
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return input;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Convert bytes to hex string.
|
|
99
|
+
*/
|
|
100
|
+
export function bytesToHex(bytes: Uint8Array): string {
|
|
101
|
+
return Array.from(bytes)
|
|
102
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
103
|
+
.join("");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert hex string to bytes.
|
|
108
|
+
*/
|
|
109
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
110
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
111
|
+
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
112
|
+
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
113
|
+
bytes[i / 2] = parseInt(cleanHex.slice(i, i + 2), 16);
|
|
114
|
+
}
|
|
115
|
+
return bytes;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convert bytes to base64 string.
|
|
120
|
+
*/
|
|
121
|
+
export function toBase64(bytes: Uint8Array): string {
|
|
122
|
+
return Buffer.from(bytes).toString("base64");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert base64 string to bytes.
|
|
127
|
+
*/
|
|
128
|
+
export function fromBase64(base64: string): Uint8Array {
|
|
129
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
130
|
+
}
|