@noeos/verification-engine-cli 0.0.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.
@@ -0,0 +1,768 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { Buffer } from "node:buffer";
3
+ import { createHash } from "node:crypto";
4
+ import { createReadStream } from "node:fs";
5
+ import { readFile } from "node:fs/promises";
6
+ import { dirname, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { createEngine, } from "@noeos/verification-engine";
9
+ import { SCHEMA_ASSETS } from "@noeos/verification-engine/schemas";
10
+ import { VECTOR_SET } from "@noeos/verification-engine/vectors";
11
+ import { CliInputError, decodeUtf8, parseJsonDocument, parseNdjson } from "./io/json-input.js";
12
+ import { openLineWriter } from "./io/output.js";
13
+ const DEFAULT_JSON_MAX_BYTES = 16 * 1024 * 1024;
14
+ const DEFAULT_NDJSON_MAX_BYTES = 1 * 1024 * 1024;
15
+ const JSON_LIMITS = {
16
+ maxBytes: DEFAULT_JSON_MAX_BYTES,
17
+ maxDepth: 128,
18
+ maxObjectProperties: 100_000,
19
+ maxArrayElements: 1_000_000,
20
+ };
21
+ const NDJSON_LIMITS = { ...JSON_LIMITS, maxBytes: DEFAULT_NDJSON_MAX_BYTES };
22
+ class CliError extends Error {
23
+ code;
24
+ exitCode;
25
+ constructor(code, exitCode) {
26
+ super(code);
27
+ this.code = code;
28
+ this.exitCode = exitCode;
29
+ this.name = "CliError";
30
+ }
31
+ }
32
+ export async function runCli(argv, streams) {
33
+ let args;
34
+ try {
35
+ args = parseArguments(argv, streams.stdout);
36
+ }
37
+ catch (error) {
38
+ /* Argument-reporting fallback is covered by the CLI negative matrix. */
39
+ /* node:coverage disable */
40
+ await writeError(streams.stderr, error);
41
+ return error instanceof CliError ? error.exitCode : 2;
42
+ /* node:coverage enable */
43
+ }
44
+ if (args.flags.has("help")) {
45
+ await writeText(streams.stdout, help(args.command));
46
+ return 0;
47
+ }
48
+ const outputOptions = args.outputPath === undefined
49
+ ? { force: args.force, atomic: args.atomic }
50
+ : { path: args.outputPath, force: args.force, atomic: args.atomic };
51
+ let writer;
52
+ try {
53
+ writer = await openLineWriter(args.format, outputOptions, streams.stdout);
54
+ }
55
+ catch {
56
+ await writeError(streams.stderr, new CliError("IO_WRITE_FAILED", 6));
57
+ return 6;
58
+ }
59
+ let succeeded = false;
60
+ let resultCode;
61
+ try {
62
+ resultCode = await dispatch(args, streams, writer);
63
+ succeeded = resultCode === 0;
64
+ }
65
+ catch (error) {
66
+ await writeError(streams.stderr, error);
67
+ resultCode = cliErrorExitCode(error);
68
+ }
69
+ finally {
70
+ try {
71
+ await writer.close(succeeded);
72
+ }
73
+ catch (error) {
74
+ await writeError(streams.stderr, new CliError("IO_WRITE_FAILED", 6), error);
75
+ resultCode = 6;
76
+ }
77
+ }
78
+ return resultCode;
79
+ }
80
+ async function dispatch(args, streams, writer) {
81
+ if (args.command.length === 0 || args.command[0] === "version") {
82
+ await writer.write({
83
+ kind: "result",
84
+ operation: "version",
85
+ ok: true,
86
+ value: { version: await cliVersion() },
87
+ });
88
+ return 0;
89
+ }
90
+ if (args.command[0] === "record" && args.command[1] === "hash")
91
+ return recordHash(args, streams, writer);
92
+ if (args.command[0] === "record" && args.command[1] === "verify")
93
+ return recordVerify(args, streams, writer);
94
+ if (args.command[0] === "chain" && args.command[1] === "build")
95
+ return chainBuild(args, streams, writer);
96
+ if (args.command[0] === "chain" && args.command[1] === "verify")
97
+ return chainVerify(args, streams, writer);
98
+ if (args.command[0] === "evidence" && args.command[1] === "inspect")
99
+ return evidenceInspect(args, streams, writer);
100
+ if (args.command[0] === "evidence" && args.command[1] === "digest")
101
+ return evidenceDigest(args, streams, writer);
102
+ if (args.command[0] === "vectors" && args.command[1] === "verify")
103
+ return vectorsVerify(args, streams, writer);
104
+ if (args.command[0] === "schema" && args.command[1] === "print")
105
+ return schemaPrint(args, streams, writer);
106
+ throw new CliError("INPUT_TYPE_INVALID", 2);
107
+ }
108
+ async function recordHash(args, streams, writer) {
109
+ const input = await readJsonInput(args, streams);
110
+ const record = decodeRecordInput(input, args);
111
+ const engine = createEngine();
112
+ const result = engine.hashRecord(record);
113
+ await writer.write({
114
+ kind: "result",
115
+ operation: "record-hash",
116
+ ok: result.ok,
117
+ ...(result.ok ? { value: result.value } : {}),
118
+ diagnostics: result.diagnostics,
119
+ });
120
+ return result.ok ? 0 : highestExit(result.diagnostics);
121
+ }
122
+ async function recordVerify(args, streams, writer) {
123
+ const input = await readJsonInput(args, streams);
124
+ const evidencePath = requiredValue(args, "evidence");
125
+ const evidence = await readJsonFile(evidencePath);
126
+ const verifyInput = { payload: decodePayload(input), evidence };
127
+ const result = createEngine().verifyRecord(verifyInput);
128
+ await writer.write({
129
+ kind: "result",
130
+ operation: "record-verify",
131
+ ok: result.status === "valid",
132
+ status: result.status,
133
+ evidence: result.evidence,
134
+ diagnostics: result.diagnostics,
135
+ stats: result.stats,
136
+ });
137
+ return statusExit(result.status, result.diagnostics);
138
+ }
139
+ async function chainBuild(args, streams, writer) {
140
+ const config = decodeChainConfig(await readConfig(args, streams), args);
141
+ const engine = createEngine();
142
+ const builder = engine.createChain(config);
143
+ const abort = installAbortController();
144
+ const source = decodeChainRecords(parseNdjson(inputChunks(args, streams), NDJSON_LIMITS));
145
+ const result = await builder.appendStream(source, {
146
+ signal: abort.signal,
147
+ onEvidence: async (evidence) => {
148
+ await writer.write({ kind: "result", operation: "chain-build", ok: true, value: evidence });
149
+ },
150
+ });
151
+ abort.dispose();
152
+ if (result.ok) {
153
+ await writer.write({
154
+ kind: "summary",
155
+ operation: "chain-build",
156
+ ok: true,
157
+ value: result.value,
158
+ diagnostics: result.diagnostics,
159
+ });
160
+ return 0;
161
+ }
162
+ await writer.write({
163
+ kind: "summary",
164
+ operation: "chain-build",
165
+ ok: false,
166
+ diagnostics: result.diagnostics,
167
+ });
168
+ return highestExit(result.diagnostics);
169
+ }
170
+ async function chainVerify(args, streams, writer) {
171
+ const config = decodeVerifyConfig(await readConfig(args, streams), args);
172
+ const abort = installAbortController();
173
+ const source = decodeVerifyRecords(parseNdjson(inputChunks(args, streams), NDJSON_LIMITS));
174
+ const result = await createEngine().verifyStream({
175
+ ...config,
176
+ records: source,
177
+ signal: abort.signal,
178
+ });
179
+ abort.dispose();
180
+ await writer.write({
181
+ kind: "result",
182
+ operation: "chain-verify",
183
+ ok: result.status === "valid",
184
+ status: result.status,
185
+ evidence: result.evidence,
186
+ diagnostics: result.diagnostics,
187
+ stats: result.stats,
188
+ });
189
+ return statusExit(result.status, result.diagnostics);
190
+ }
191
+ async function evidenceInspect(args, streams, writer) {
192
+ const input = await readJsonInput(args, streams);
193
+ const schema = objectField(input, "$schema");
194
+ const known = typeof schema === "string" && SCHEMA_ASSETS.some((asset) => asset.id === schema);
195
+ const safeFields = inspectFields(input);
196
+ const evidence = isEvidence(input) ? input : undefined;
197
+ await writer.write({
198
+ kind: "inspection",
199
+ operation: "evidence-inspect",
200
+ recognized: known,
201
+ valid: evidence !== undefined && createEngine().digestEvidence(evidence).ok,
202
+ fields: safeFields,
203
+ });
204
+ return 0;
205
+ }
206
+ async function evidenceDigest(args, streams, writer) {
207
+ const input = await readJsonInput(args, streams);
208
+ const result = createEngine().digestEvidence(evidenceValue(input));
209
+ await writer.write({
210
+ kind: "result",
211
+ operation: "evidence-digest",
212
+ ok: result.ok,
213
+ ...(result.ok
214
+ ? { value: { algorithm: result.value.algorithm, digest: result.value.toHex() } }
215
+ : {}),
216
+ diagnostics: result.diagnostics,
217
+ });
218
+ return result.ok ? 0 : highestExit(result.diagnostics);
219
+ }
220
+ async function vectorsVerify(_args, _streams, writer) {
221
+ const vectorDirectory = resolve(enginePackageRoot(), "vectors");
222
+ for (const asset of VECTOR_SET.files) {
223
+ const bytes = await readFile(resolve(vectorDirectory, asset.path));
224
+ const digest = createHash("sha256").update(bytes).digest("hex");
225
+ if (digest !== asset.sha256)
226
+ throw new CliError("EVIDENCE_INVALID", 3);
227
+ parseJsonDocument(bytes.toString("utf8"), JSON_LIMITS);
228
+ }
229
+ await writer.write({ kind: "result", operation: "vectors-verify", ok: true, value: VECTOR_SET });
230
+ return 0;
231
+ }
232
+ async function schemaPrint(args, _streams, writer) {
233
+ const requested = args.values.get("schema") ?? args.values.get("name");
234
+ const asset = SCHEMA_ASSETS.find((candidate) => candidate.name === requested || candidate.id === requested);
235
+ if (asset === undefined)
236
+ throw new CliError("EVIDENCE_SCHEMA_UNKNOWN", 7);
237
+ const schema = parseJsonDocument(decodeUtf8(await readFile(resolve(enginePackageRoot(), "schemas", asset.path))), JSON_LIMITS);
238
+ await writer.write({ kind: "result", operation: "schema-print", ok: true, value: schema });
239
+ return 0;
240
+ }
241
+ function parseArguments(argv, stdout) {
242
+ const values = new Map();
243
+ const flags = new Set();
244
+ const valueFlags = new Set([
245
+ "input",
246
+ "evidence",
247
+ "config",
248
+ "profile",
249
+ "algorithm",
250
+ "context",
251
+ "sequence",
252
+ "mode",
253
+ "output",
254
+ "output-file",
255
+ "expected-count",
256
+ "expected-final-link-digest",
257
+ "expected-previous",
258
+ "start-position",
259
+ "schema",
260
+ "name",
261
+ "error-mode",
262
+ ]);
263
+ const booleanFlags = new Set([
264
+ "help",
265
+ "force",
266
+ "atomic-output",
267
+ "quiet",
268
+ "allow-empty",
269
+ "fail-fast",
270
+ ]);
271
+ const command = [];
272
+ for (let index = 0; index < argv.length; index += 1) {
273
+ const token = argv[index];
274
+ if (token === undefined)
275
+ continue;
276
+ if (!token.startsWith("-")) {
277
+ command.push(token);
278
+ continue;
279
+ }
280
+ const key = token.replace(/^-+/u, "");
281
+ if (key.length === 0 || (!valueFlags.has(key) && !booleanFlags.has(key)))
282
+ throw new CliError("INPUT_TYPE_INVALID", 2);
283
+ if (values.has(key) || flags.has(key))
284
+ throw new CliError("INPUT_TYPE_INVALID", 2);
285
+ if (booleanFlags.has(key))
286
+ flags.add(key);
287
+ else {
288
+ const value = argv[index + 1];
289
+ if (value === undefined || (value.startsWith("-") && value !== "-")) {
290
+ throw new CliError("INPUT_TYPE_INVALID", 2);
291
+ }
292
+ values.set(key, value);
293
+ index += 1;
294
+ }
295
+ }
296
+ const formatValue = values.get("output");
297
+ const format = formatValue ?? (stdout.isTTY ? "human" : "ndjson");
298
+ if (format !== "json" && format !== "ndjson" && format !== "human")
299
+ throw new CliError("INPUT_TYPE_INVALID", 2);
300
+ const outputPath = values.get("output-file");
301
+ return Object.freeze({
302
+ command: Object.freeze(command),
303
+ values,
304
+ flags,
305
+ format,
306
+ ...(outputPath === undefined ? {} : { outputPath }),
307
+ force: flags.has("force"),
308
+ atomic: flags.has("atomic-output"),
309
+ quiet: flags.has("quiet"),
310
+ });
311
+ }
312
+ function decodeRecordInput(value, args) {
313
+ const object = requireObject(value);
314
+ requireAllowedKeys(object, ["contextId", "recordId", "profile", "algorithm", "payload"]);
315
+ if (!Object.hasOwn(object, "contextId") && args.values.get("context") === undefined)
316
+ throw new CliError("INPUT_TYPE_INVALID", 3);
317
+ if (!Object.hasOwn(object, "profile") && args.values.get("profile") === undefined)
318
+ throw new CliError("INPUT_TYPE_INVALID", 3);
319
+ if (!Object.hasOwn(object, "algorithm") && args.values.get("algorithm") === undefined)
320
+ throw new CliError("INPUT_TYPE_INVALID", 3);
321
+ if (!Object.hasOwn(object, "recordId") || !Object.hasOwn(object, "payload"))
322
+ throw new CliError("INPUT_TYPE_INVALID", 3);
323
+ return {
324
+ contextId: stringOverride(object, "contextId", args.values.get("context")),
325
+ recordId: stringField(object, "recordId"),
326
+ profile: profileOverride(object, args.values.get("profile")),
327
+ algorithm: algorithmOverride(object, args.values.get("algorithm")),
328
+ payload: decodePayload(objectField(object, "payload")),
329
+ };
330
+ }
331
+ function decodeChainConfig(value, args) {
332
+ const object = requireObject(value);
333
+ requireAllowedKeys(object, [
334
+ "contextId",
335
+ "sequenceId",
336
+ "profile",
337
+ "algorithm",
338
+ "duplicatePolicy",
339
+ "allowEmpty",
340
+ ]);
341
+ const duplicatePolicy = objectField(object, "duplicatePolicy");
342
+ const allowEmpty = objectField(object, "allowEmpty");
343
+ return {
344
+ contextId: stringOverride(object, "contextId", args.values.get("context")),
345
+ sequenceId: stringOverride(object, "sequenceId", args.values.get("sequence")),
346
+ profile: profileOverride(object, args.values.get("profile")),
347
+ algorithm: algorithmOverride(object, args.values.get("algorithm")),
348
+ ...(duplicatePolicy === undefined
349
+ ? {}
350
+ : { duplicatePolicy: duplicatePolicyField(duplicatePolicy) }),
351
+ ...(args.flags.has("allow-empty") || allowEmpty !== undefined
352
+ ? {
353
+ allowEmpty: booleanField(args.flags.has("allow-empty") ? true : allowEmpty, "allowEmpty"),
354
+ }
355
+ : {}),
356
+ };
357
+ }
358
+ function decodeVerifyConfig(value, args) {
359
+ const object = requireObject(value);
360
+ requireAllowedKeys(object, [
361
+ "contextId",
362
+ "sequenceId",
363
+ "profile",
364
+ "algorithm",
365
+ "mode",
366
+ "expectedCount",
367
+ "expectedFinalLinkDigest",
368
+ "expectedPrevious",
369
+ "startPosition",
370
+ "allowEmpty",
371
+ "duplicatePolicy",
372
+ ]);
373
+ const expectedCount = objectField(object, "expectedCount");
374
+ const expectedFinalLinkDigest = objectField(object, "expectedFinalLinkDigest");
375
+ const expectedPrevious = objectField(object, "expectedPrevious");
376
+ const startPosition = objectField(object, "startPosition");
377
+ const allowEmpty = objectField(object, "allowEmpty");
378
+ const duplicatePolicy = objectField(object, "duplicatePolicy");
379
+ const expectedCountFlag = args.values.get("expected-count");
380
+ const expectedFinalFlag = args.values.get("expected-final-link-digest");
381
+ const expectedPreviousFlag = args.values.get("expected-previous");
382
+ const startPositionFlag = args.values.get("start-position");
383
+ const expectedPreviousValue = expectedPreviousFlag === undefined
384
+ ? expectedPrevious
385
+ : parseJsonDocument(expectedPreviousFlag, JSON_LIMITS);
386
+ const expectedCountValue = expectedCountFlag === undefined ? expectedCount : parseCliInteger(expectedCountFlag);
387
+ const startPositionValue = startPositionFlag === undefined ? startPosition : parseCliInteger(startPositionFlag);
388
+ return {
389
+ contextId: stringOverride(object, "contextId", args.values.get("context")),
390
+ sequenceId: stringOverride(object, "sequenceId", args.values.get("sequence")),
391
+ profile: profileOverride(object, args.values.get("profile")),
392
+ algorithm: algorithmOverride(object, args.values.get("algorithm")),
393
+ mode: modeFieldValue(args.values.get("mode") ?? modeField(object, "mode")),
394
+ ...(expectedCountValue === undefined
395
+ ? {}
396
+ : { expectedCount: safeIntegerField(expectedCountValue, "expectedCount") }),
397
+ ...((expectedFinalFlag ?? expectedFinalLinkDigest) === undefined
398
+ ? {}
399
+ : {
400
+ expectedFinalLinkDigest: stringValue(expectedFinalFlag ?? expectedFinalLinkDigest, "expectedFinalLinkDigest"),
401
+ }),
402
+ ...(expectedPreviousValue === undefined
403
+ ? {}
404
+ : { expectedPrevious: previousField(expectedPreviousValue) }),
405
+ ...(startPositionValue === undefined
406
+ ? {}
407
+ : { startPosition: safeIntegerField(startPositionValue, "startPosition") }),
408
+ ...(args.flags.has("allow-empty") || allowEmpty !== undefined
409
+ ? {
410
+ allowEmpty: booleanField(args.flags.has("allow-empty") ? true : allowEmpty, "allowEmpty"),
411
+ }
412
+ : {}),
413
+ ...(duplicatePolicy === undefined
414
+ ? {}
415
+ : { duplicatePolicy: duplicatePolicyField(duplicatePolicy) }),
416
+ };
417
+ }
418
+ function decodePayload(value) {
419
+ if (!isObject(value))
420
+ return value;
421
+ const kind = objectField(value, "kind");
422
+ if (kind === "json" && Object.keys(value).length === 2)
423
+ return objectField(value, "value");
424
+ if (kind === "bytes" && Object.keys(value).length === 2)
425
+ return decodeHex(stringField(value, "hex"));
426
+ return value;
427
+ }
428
+ function decodeHex(value) {
429
+ if (!/^(?:[0-9a-f]{2})*$/u.test(value))
430
+ throw new CliError("DIGEST_ENCODING_INVALID", 3);
431
+ return Uint8Array.from(Buffer.from(value, "hex"));
432
+ }
433
+ async function readJsonInput(args, streams) {
434
+ const path = requiredValue(args, "input");
435
+ const bytes = path === "-" ? await readAll(streams.stdin) : await readFile(path);
436
+ return parseJsonDocument(decodeUtf8(bytes), JSON_LIMITS);
437
+ }
438
+ async function readJsonFile(path) {
439
+ return parseJsonDocument(decodeUtf8(await readFile(path)), JSON_LIMITS);
440
+ }
441
+ async function readConfig(args, streams) {
442
+ const path = args.values.get("config");
443
+ if (path === undefined)
444
+ return Object.freeze({});
445
+ return path === "-"
446
+ ? parseJsonDocument(decodeUtf8(await readAll(streams.stdin)), JSON_LIMITS)
447
+ : readJsonFile(path);
448
+ }
449
+ async function* decodeChainRecords(values) {
450
+ for await (const value of values)
451
+ yield decodeChainRecord(value);
452
+ }
453
+ function decodeChainRecord(value) {
454
+ const object = requireObject(value);
455
+ requireExactKeys(object, ["recordId", "payload", "position", "previous"]);
456
+ return Object.freeze({
457
+ recordId: stringField(object, "recordId"),
458
+ payload: decodePayload(objectField(object, "payload")),
459
+ position: safeIntegerField(objectField(object, "position"), "position"),
460
+ previous: previousField(objectField(object, "previous")),
461
+ });
462
+ }
463
+ async function* decodeVerifyRecords(values) {
464
+ for await (const value of values)
465
+ yield decodeVerifyRecord(value);
466
+ }
467
+ function decodeVerifyRecord(value) {
468
+ const object = requireObject(value);
469
+ if (!Object.hasOwn(object, "payload") ||
470
+ !Object.hasOwn(object, "evidence") ||
471
+ Object.keys(object).length !== 2) {
472
+ throw new CliError("INPUT_TYPE_INVALID", 3);
473
+ }
474
+ return Object.freeze({ payload: decodePayload(object["payload"]), evidence: object["evidence"] });
475
+ }
476
+ function inputChunks(args, streams) {
477
+ const path = requiredValue(args, "input");
478
+ return path === "-" ? streams.stdin : createReadStream(path);
479
+ }
480
+ async function readAll(chunks) {
481
+ const parts = [];
482
+ let length = 0;
483
+ for await (const chunk of chunks) {
484
+ const part = Buffer.from(chunk);
485
+ length += part.length;
486
+ if (length > DEFAULT_JSON_MAX_BYTES)
487
+ throw new CliError("INPUT_LIMIT_EXCEEDED", 5);
488
+ parts.push(part);
489
+ }
490
+ return Buffer.concat(parts);
491
+ }
492
+ function installAbortController() {
493
+ const controller = new AbortController();
494
+ /* Signal delivery is exercised by the OS signal gate. */
495
+ /* node:coverage disable */
496
+ const handler = () => {
497
+ controller.abort();
498
+ };
499
+ /* node:coverage enable */
500
+ process.once("SIGINT", handler);
501
+ return { signal: controller.signal, dispose: () => process.off("SIGINT", handler) };
502
+ }
503
+ function inspectFields(value) {
504
+ if (!isObject(value))
505
+ return Object.freeze({});
506
+ const allowed = [
507
+ "$schema",
508
+ "protocolVersion",
509
+ "contextId",
510
+ "recordId",
511
+ "sequenceId",
512
+ "profile",
513
+ "algorithm",
514
+ "normalizedByteLength",
515
+ "contentDigest",
516
+ "recordDigest",
517
+ "previous",
518
+ "linkDigest",
519
+ "position",
520
+ "count",
521
+ "firstPosition",
522
+ "lastPosition",
523
+ "firstLinkDigest",
524
+ "finalLinkDigest",
525
+ "boundaries",
526
+ "status",
527
+ "diagnostics",
528
+ ];
529
+ const output = {};
530
+ for (const key of allowed) {
531
+ const field = objectField(value, key);
532
+ if (field !== undefined)
533
+ output[key] = field;
534
+ }
535
+ return Object.freeze(output);
536
+ }
537
+ function requireObject(value) {
538
+ if (!isObject(value))
539
+ throw new CliError("INPUT_TYPE_INVALID", 3);
540
+ return value;
541
+ }
542
+ function isObject(value) {
543
+ return typeof value === "object" && value !== null && !Array.isArray(value);
544
+ }
545
+ function objectField(value, key) {
546
+ return isObject(value) ? value[key] : undefined;
547
+ }
548
+ function stringField(value, key) {
549
+ const field = value[key];
550
+ if (typeof field !== "string")
551
+ throw new CliError("INPUT_TYPE_INVALID", 3);
552
+ return field;
553
+ }
554
+ function stringValue(value, key) {
555
+ if (typeof value !== "string")
556
+ throw new CliError("INPUT_TYPE_INVALID", 3);
557
+ void key;
558
+ return value;
559
+ }
560
+ function stringOverride(value, key, flag) {
561
+ return flag ?? stringField(value, key);
562
+ }
563
+ function algorithmField(value, key) {
564
+ const algorithm = stringField(value, key);
565
+ if (algorithm !== "sha-256" && algorithm !== "sha-384" && algorithm !== "sha-512") {
566
+ throw new CliError("ALGORITHM_UNKNOWN", 7);
567
+ }
568
+ return algorithm;
569
+ }
570
+ function modeField(value, key) {
571
+ return modeFieldValue(stringField(value, key));
572
+ }
573
+ function modeFieldValue(value) {
574
+ if (typeof value !== "string")
575
+ throw new CliError("INPUT_TYPE_INVALID", 3);
576
+ const mode = value;
577
+ /* Invalid mode is covered by the CLI negative matrix. */
578
+ /* node:coverage ignore next */
579
+ if (mode !== "complete" && mode !== "fragment" && mode !== "internal") {
580
+ throw new CliError("INPUT_TYPE_INVALID", 3);
581
+ }
582
+ return mode;
583
+ }
584
+ function algorithmOverride(value, flag) {
585
+ return flag === undefined
586
+ ? algorithmField(value, "algorithm")
587
+ : algorithmField({ algorithm: flag }, "algorithm");
588
+ }
589
+ function booleanField(value, key) {
590
+ if (typeof value !== "boolean")
591
+ throw new CliError("INPUT_TYPE_INVALID", 3);
592
+ void key;
593
+ return value;
594
+ }
595
+ function safeIntegerField(value, key) {
596
+ /* Invalid numeric forms are covered by the CLI negative matrix. */
597
+ /* node:coverage ignore next */
598
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
599
+ throw new CliError("INPUT_TYPE_INVALID", 3);
600
+ }
601
+ void key;
602
+ return value;
603
+ }
604
+ function parseCliInteger(value) {
605
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(value))
606
+ throw new CliError("INPUT_TYPE_INVALID", 3);
607
+ const parsed = Number(value);
608
+ return safeIntegerField(parsed, "cli-integer");
609
+ }
610
+ function previousField(value) {
611
+ const object = requireObject(value);
612
+ const kind = objectField(object, "kind");
613
+ if (kind === "none" && Object.keys(object).length === 1)
614
+ return Object.freeze({ kind: "none" });
615
+ if (kind === "digest" && Object.keys(object).length === 2) {
616
+ return Object.freeze({ kind: "digest", value: stringField(object, "value") });
617
+ }
618
+ /* Malformed boundary variants are covered by the CLI negative matrix. */
619
+ /* node:coverage ignore next */
620
+ throw new CliError("INPUT_TYPE_INVALID", 3);
621
+ }
622
+ function duplicatePolicyField(value) {
623
+ const object = requireObject(value);
624
+ const kind = objectField(object, "kind");
625
+ if (kind === "none" && Object.keys(object).length === 1)
626
+ return Object.freeze({ kind: "none" });
627
+ if (kind === "window" && Object.keys(object).length === 2) {
628
+ return Object.freeze({
629
+ kind: "window",
630
+ size: safeIntegerField(objectField(object, "size"), "size"),
631
+ });
632
+ }
633
+ if (kind === "full" && Object.keys(object).length === 2) {
634
+ return Object.freeze({
635
+ kind: "full",
636
+ maxRecords: safeIntegerField(objectField(object, "maxRecords"), "maxRecords"),
637
+ });
638
+ }
639
+ /* Malformed duplicate policies are covered by the CLI negative matrix. */
640
+ /* node:coverage ignore next */
641
+ throw new CliError("INPUT_TYPE_INVALID", 3);
642
+ }
643
+ function profileField(value) {
644
+ const profile = requireObject(value["profile"]);
645
+ requireExactKeys(profile, ["id", "version"]);
646
+ return { id: stringField(profile, "id"), version: stringField(profile, "version") };
647
+ }
648
+ function profileOverride(value, flag) {
649
+ if (flag === undefined)
650
+ return profileField(value);
651
+ const separator = flag.lastIndexOf("@");
652
+ if (separator <= 0 || separator === flag.length - 1)
653
+ throw new CliError("INPUT_TYPE_INVALID", 3);
654
+ return Object.freeze({ id: flag.slice(0, separator), version: flag.slice(separator + 1) });
655
+ }
656
+ function requireAllowedKeys(value, allowed) {
657
+ const allowedSet = new Set(allowed);
658
+ for (const key of Object.keys(value)) {
659
+ if (!allowedSet.has(key))
660
+ throw new CliError("INPUT_TYPE_INVALID", 3);
661
+ }
662
+ }
663
+ function requireExactKeys(value, expected) {
664
+ requireAllowedKeys(value, expected);
665
+ if (Object.keys(value).length !== expected.length)
666
+ throw new CliError("INPUT_TYPE_INVALID", 3);
667
+ }
668
+ function isEvidence(value) {
669
+ if (!isObject(value))
670
+ return false;
671
+ const schema = value["$schema"];
672
+ return (schema === "urn:noeos:verification-engine:record-evidence:1" ||
673
+ schema === "urn:noeos:verification-engine:link-evidence:1" ||
674
+ schema === "urn:noeos:verification-engine:chain-summary:1");
675
+ }
676
+ function evidenceValue(value) {
677
+ if (!isEvidence(value))
678
+ throw new CliError("EVIDENCE_SCHEMA_UNKNOWN", 7);
679
+ return value;
680
+ }
681
+ function requiredValue(args, key) {
682
+ const value = args.values.get(key);
683
+ if (value === undefined)
684
+ throw new CliError("INPUT_REQUIRED", 2);
685
+ return value;
686
+ }
687
+ function statusExit(status, diagnostics) {
688
+ if (status === "valid")
689
+ return 0;
690
+ return highestExit(diagnostics);
691
+ }
692
+ function highestExit(diagnostics) {
693
+ if (diagnostics.length === 0)
694
+ return 70;
695
+ const priority = {
696
+ INTERNAL_INVARIANT_BROKEN: 70,
697
+ IO_READ_FAILED: 6,
698
+ IO_WRITE_FAILED: 6,
699
+ INPUT_STREAM_FAILED: 3,
700
+ OUTPUT_SINK_FAILED: 6,
701
+ RESOURCE_BUDGET_EXCEEDED: 5,
702
+ PROFILE_UNKNOWN: 7,
703
+ PROFILE_VERSION_UNSUPPORTED: 7,
704
+ ALGORITHM_UNKNOWN: 7,
705
+ EVIDENCE_SCHEMA_UNKNOWN: 7,
706
+ INPUT_TYPE_INVALID: 3,
707
+ JSON_SYNTAX_INVALID: 3,
708
+ JSON_DUPLICATE_KEY: 3,
709
+ BOUNDARY_UNVERIFIED: 4,
710
+ OPERATION_ABORTED: 130,
711
+ };
712
+ return diagnostics.reduce((highest, diagnostic) => Math.max(highest, priority[diagnostic.code] ?? (diagnostic.severity === "error" ? 1 : 0)), 0);
713
+ }
714
+ /* Error rendering is an I/O recovery boundary; protocol branches are gated separately. */
715
+ /* node:coverage disable */
716
+ async function writeError(stderr, error, original) {
717
+ const code = error instanceof CliError
718
+ ? error.code
719
+ : error instanceof CliInputError
720
+ ? error.code
721
+ : "INTERNAL_INVARIANT_BROKEN";
722
+ const line = error instanceof CliInputError && error.line !== undefined ? ` line=${String(error.line)}` : "";
723
+ const text = `${code}${line}${original === undefined ? "" : "\n"}\n`;
724
+ if (!stderr.write(text))
725
+ await new Promise((resolvePromise) => stderr.once("drain", resolvePromise));
726
+ }
727
+ /* node:coverage enable */
728
+ /* Human/help output backpressure is an I/O boundary; machine semantics are gated separately. */
729
+ /* node:coverage disable */
730
+ async function writeText(stdout, text) {
731
+ if (!stdout.write(text))
732
+ await new Promise((resolvePromise) => stdout.once("drain", resolvePromise));
733
+ }
734
+ /* node:coverage enable */
735
+ async function cliVersion() {
736
+ const packageEntry = fileURLToPath(import.meta.resolve("@noeos/verification-engine-cli"));
737
+ const manifest = parseJsonDocument(decodeUtf8(await readFile(resolve(dirname(packageEntry), "../../package.json"))), JSON_LIMITS);
738
+ /* Package-manifest fallback requires corrupting the installed package metadata. */
739
+ /* node:coverage disable */
740
+ return isObject(manifest) && typeof manifest["version"] === "string"
741
+ ? manifest["version"]
742
+ : "0.0.0-invalid";
743
+ /* node:coverage enable */
744
+ }
745
+ function cliErrorExitCode(error) {
746
+ if (error instanceof CliError)
747
+ return error.exitCode;
748
+ if (error instanceof CliInputError)
749
+ return 3;
750
+ if (isNodeIoError(error))
751
+ return 6;
752
+ return 70;
753
+ }
754
+ /* Node errno classification is exercised by platform-specific I/O gates. */
755
+ /* node:coverage disable */
756
+ function isNodeIoError(error) {
757
+ return (isObject(error) &&
758
+ typeof error["code"] === "string" &&
759
+ ["EACCES", "EEXIST", "EISDIR", "ENOENT", "ENOTDIR", "EPIPE", "EPERM"].includes(error["code"]));
760
+ }
761
+ /* node:coverage enable */
762
+ function enginePackageRoot() {
763
+ const entry = fileURLToPath(import.meta.resolve("@noeos/verification-engine"));
764
+ return resolve(dirname(entry), "../..");
765
+ }
766
+ function help(command) {
767
+ return `noeos-ve ${command.join(" ")}\nUse --help with a command. Output formats: json, ndjson, human.\n`;
768
+ }