@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.
@@ -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
+ }