@nexart/cli 0.8.1 → 0.10.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/CHANGELOG.md +298 -0
- package/README.md +138 -7
- package/dist/index.d.ts +36 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +419 -15
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/dist/index.js
CHANGED
|
@@ -64,7 +64,12 @@ import * as https from 'https';
|
|
|
64
64
|
import { fileURLToPath } from 'url';
|
|
65
65
|
import { realpathSync } from 'fs';
|
|
66
66
|
import { createRequire } from 'module';
|
|
67
|
-
import { verifyProjectBundle, verifyAiCerBundleDetailed, hasAttestation as sdkHasAttestation, isCerPackage as sdkIsCerPackage, getCerFromPackage, createSnapshot as sdkCreateSnapshot, sealCer as sdkSealCer, } from '@nexart/ai-execution';
|
|
67
|
+
import { verifyProjectBundle, verifyAiCerBundleDetailed, hasAttestation as sdkHasAttestation, isCerPackage as sdkIsCerPackage, getCerFromPackage, createSnapshot as sdkCreateSnapshot, sealCer as sdkSealCer, attachAnchor as sdkAttachAnchor, verifyAnchors as sdkVerifyAnchors, verifyTrustedTimestamps as sdkVerifyTrustedTimestamps, exportVerificationPackage as sdkExportVerificationPackage, } from '@nexart/ai-execution';
|
|
68
|
+
// Deterministic local renderer — reuses the canonical CodeMode SDK render core.
|
|
69
|
+
// Importing the module does NOT load the native `canvas` package; `canvas` is
|
|
70
|
+
// required lazily only when a local render is actually performed, so this import
|
|
71
|
+
// is safe even when the optional dependency is absent.
|
|
72
|
+
import { renderStaticPng } from '@nexart/codemode-sdk/node';
|
|
68
73
|
// Dynamic version source: read from this CLI package's own package.json at
|
|
69
74
|
// load time so `--version` output, CER metadata, and verify-report verifier
|
|
70
75
|
// strings can never drift from the published npm version. createRequire
|
|
@@ -304,6 +309,83 @@ function createPlaceholderPng() {
|
|
|
304
309
|
0x44, 0xAE, 0x42, 0x60, 0x82
|
|
305
310
|
]);
|
|
306
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Resolve the installed @nexart/codemode-sdk version (descriptive only).
|
|
314
|
+
*/
|
|
315
|
+
function getSdkCodemodeVersion() {
|
|
316
|
+
try {
|
|
317
|
+
const req = createRequire(import.meta.url);
|
|
318
|
+
const pkg = req('@nexart/codemode-sdk/package.json');
|
|
319
|
+
return pkg.version ?? 'unknown';
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return 'unknown';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Resolve the installed native `canvas` version if present (descriptive only).
|
|
327
|
+
* Returns undefined when the optional dependency is not installed.
|
|
328
|
+
*/
|
|
329
|
+
function getCanvasVersion() {
|
|
330
|
+
try {
|
|
331
|
+
const req = createRequire(import.meta.url);
|
|
332
|
+
const pkg = req('canvas/package.json');
|
|
333
|
+
return pkg.version;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Build the environment fingerprint for the current process. Descriptive
|
|
341
|
+
* metadata only — never hashed, never affects pass/fail.
|
|
342
|
+
*/
|
|
343
|
+
function buildEnvironmentFingerprint() {
|
|
344
|
+
return {
|
|
345
|
+
cliVersion: CLI_VERSION,
|
|
346
|
+
sdkVersion: getSdkCodemodeVersion(),
|
|
347
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
348
|
+
node: process.version,
|
|
349
|
+
platform: process.platform,
|
|
350
|
+
arch: process.arch,
|
|
351
|
+
canvas: getCanvasVersion(),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Compare two environment fingerprints on the fields that can affect
|
|
356
|
+
* byte-level render reproducibility (runtime + render libs). Returns the list
|
|
357
|
+
* of differing fields (empty array === environments are equivalent).
|
|
358
|
+
*/
|
|
359
|
+
function diffEnvironmentFingerprints(a, b) {
|
|
360
|
+
const fields = [
|
|
361
|
+
'sdkVersion', 'protocolVersion', 'node', 'platform', 'arch', 'canvas',
|
|
362
|
+
];
|
|
363
|
+
return fields.filter((f) => a[f] !== b[f]);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Render a controlled CodeMode sketch locally using the canonical SDK render
|
|
367
|
+
* core. Hashing basis is sha256 of the encoded PNG bytes — identical to the
|
|
368
|
+
* remote renderer's definition. Exits with a clear, actionable error (never a
|
|
369
|
+
* silent placeholder fallback) when the optional `canvas` dependency or its
|
|
370
|
+
* native libraries are missing.
|
|
371
|
+
*/
|
|
372
|
+
async function renderLocal(code, seed, VAR, width, height) {
|
|
373
|
+
try {
|
|
374
|
+
const result = await renderStaticPng({ mode: 'static', width, height }, { code, seed, vars: VAR });
|
|
375
|
+
return { pngBytes: result.png, pixelHash: result.pixelHash };
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
379
|
+
console.error(`[nexart] Local render failed: ${msg}`);
|
|
380
|
+
if (/canvas/i.test(msg)) {
|
|
381
|
+
console.error('[nexart] The local renderer requires the optional `canvas` package and its');
|
|
382
|
+
console.error('[nexart] native libraries (cairo, pango, libjpeg, giflib, librsvg, pixman).');
|
|
383
|
+
console.error('[nexart] Install with: npm install canvas');
|
|
384
|
+
console.error('[nexart] Or render remotely with: --renderer remote');
|
|
385
|
+
}
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
307
389
|
async function runCommand(file, options) {
|
|
308
390
|
console.log(`[nexart] Running: ${file}`);
|
|
309
391
|
console.log(`[nexart] Renderer: ${options.renderer}`);
|
|
@@ -339,9 +421,11 @@ async function runCommand(file, options) {
|
|
|
339
421
|
}
|
|
340
422
|
}
|
|
341
423
|
else {
|
|
342
|
-
console.log(`[nexart]
|
|
343
|
-
|
|
424
|
+
console.log(`[nexart] Rendering locally (deterministic CodeMode SDK renderer)`);
|
|
425
|
+
const result = await renderLocal(code, seed, VAR, width, height);
|
|
426
|
+
pngBytes = result.pngBytes;
|
|
344
427
|
runtimeHash = options.runtimeHash || sha256(CODE_MODE_RUNTIME_TAG);
|
|
428
|
+
console.log(`[nexart] Rendered ${pngBytes.length} bytes locally`);
|
|
345
429
|
}
|
|
346
430
|
const outputHash = sha256(pngBytes);
|
|
347
431
|
const snapshot = {
|
|
@@ -359,6 +443,12 @@ async function runCommand(file, options) {
|
|
|
359
443
|
if (options.includeCode) {
|
|
360
444
|
snapshot.code = code;
|
|
361
445
|
}
|
|
446
|
+
// Additive: capture the render environment for local renders so replay can
|
|
447
|
+
// warn on environment drift. Remote renders are produced server-side, so a
|
|
448
|
+
// local fingerprint would be misleading — omit it there.
|
|
449
|
+
if (options.renderer === 'local') {
|
|
450
|
+
snapshot.environment = buildEnvironmentFingerprint();
|
|
451
|
+
}
|
|
362
452
|
const pngPath = resolveOutputPath(options.out);
|
|
363
453
|
const snapshotPath = deriveSnapshotPath(pngPath);
|
|
364
454
|
const pngAbsPath = writeFileAndVerify(pngPath, pngBytes);
|
|
@@ -397,6 +487,32 @@ async function replayCommand(snapshotFile, options) {
|
|
|
397
487
|
console.log(`[nexart] Seed: ${snapshot.seed}`);
|
|
398
488
|
console.log(`[nexart] VAR: [${snapshot.VAR.join(', ')}]`);
|
|
399
489
|
console.log(`[nexart] Canvas: ${snapshot.canvas.width}x${snapshot.canvas.height}`);
|
|
490
|
+
// Code integrity check — independent of the render. A mismatch here means the
|
|
491
|
+
// code being replayed is not the code the snapshot was produced from.
|
|
492
|
+
const computedCodeHash = hashCode(code);
|
|
493
|
+
const codeMatch = computedCodeHash === snapshot.codeHash;
|
|
494
|
+
if (!codeMatch) {
|
|
495
|
+
console.warn(`[nexart] WARNING: code hash mismatch — replaying DIFFERENT code than the snapshot.`);
|
|
496
|
+
console.warn(`[nexart] expected: ${snapshot.codeHash}`);
|
|
497
|
+
console.warn(`[nexart] actual: ${computedCodeHash}`);
|
|
498
|
+
}
|
|
499
|
+
// Environment drift check (additive). Only meaningful when the snapshot
|
|
500
|
+
// carries a fingerprint (local renders). Legacy snapshots omit it.
|
|
501
|
+
const currentEnv = buildEnvironmentFingerprint();
|
|
502
|
+
let envDiff = null;
|
|
503
|
+
if (snapshot.environment) {
|
|
504
|
+
envDiff = diffEnvironmentFingerprints(snapshot.environment, currentEnv);
|
|
505
|
+
if (envDiff.length > 0) {
|
|
506
|
+
console.warn(`[nexart] WARNING: replay environment differs from capture environment: ${envDiff.join(', ')}`);
|
|
507
|
+
console.warn(`[nexart] byte-level reproducibility is only guaranteed within the same runtime/render libs.`);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
console.log(`[nexart] Environment matches capture fingerprint.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
console.warn(`[nexart] NOTE: snapshot has no environment fingerprint (legacy snapshot) — cannot verify environment parity.`);
|
|
515
|
+
}
|
|
400
516
|
let pngBytes;
|
|
401
517
|
if (options.renderer === 'remote') {
|
|
402
518
|
validateApiKey(options.apiKey, options.endpoint);
|
|
@@ -415,14 +531,54 @@ async function replayCommand(snapshotFile, options) {
|
|
|
415
531
|
}
|
|
416
532
|
}
|
|
417
533
|
else {
|
|
418
|
-
console.log(`[nexart]
|
|
419
|
-
|
|
534
|
+
console.log(`[nexart] Rendering locally (deterministic CodeMode SDK renderer)`);
|
|
535
|
+
const result = await renderLocal(code, snapshot.seed, snapshot.VAR, snapshot.canvas.width, snapshot.canvas.height);
|
|
536
|
+
pngBytes = result.pngBytes;
|
|
537
|
+
console.log(`[nexart] Rendered ${pngBytes.length} bytes locally`);
|
|
420
538
|
}
|
|
421
539
|
const outputHash = sha256(pngBytes);
|
|
540
|
+
const outputMatch = outputHash === snapshot.outputHash;
|
|
541
|
+
// Overall PASS requires both the output AND the code to match the snapshot.
|
|
542
|
+
const pass = outputMatch && codeMatch;
|
|
422
543
|
const pngPath = resolveOutputPath(options.out);
|
|
423
544
|
const pngAbsPath = writeFileAndVerify(pngPath, pngBytes);
|
|
424
545
|
console.log(`[nexart] Wrote PNG: ${pngAbsPath} (${pngBytes.length} bytes)`);
|
|
425
|
-
console.log(`[nexart] outputHash: ${outputHash}`);
|
|
546
|
+
console.log(`[nexart] Expected outputHash: ${snapshot.outputHash}`);
|
|
547
|
+
console.log(`[nexart] Actual outputHash: ${outputHash}`);
|
|
548
|
+
console.log(`[nexart] Replay result: ${pass ? 'PASS' : 'FAIL'}`);
|
|
549
|
+
if (options.evidence) {
|
|
550
|
+
const evidence = {
|
|
551
|
+
protocol: 'nexart',
|
|
552
|
+
kind: 'codemode.replay.evidence.v1',
|
|
553
|
+
result: pass ? 'PASS' : 'FAIL',
|
|
554
|
+
snapshotFile,
|
|
555
|
+
renderer: options.renderer,
|
|
556
|
+
protocolVersion: snapshot.protocolVersion,
|
|
557
|
+
seed: snapshot.seed,
|
|
558
|
+
VAR: snapshot.VAR,
|
|
559
|
+
canvas: snapshot.canvas,
|
|
560
|
+
codeHash: {
|
|
561
|
+
expected: snapshot.codeHash,
|
|
562
|
+
actual: computedCodeHash,
|
|
563
|
+
match: codeMatch,
|
|
564
|
+
},
|
|
565
|
+
outputHash: {
|
|
566
|
+
expected: snapshot.outputHash,
|
|
567
|
+
actual: outputHash,
|
|
568
|
+
match: outputMatch,
|
|
569
|
+
},
|
|
570
|
+
environment: {
|
|
571
|
+
snapshot: snapshot.environment ?? null,
|
|
572
|
+
current: currentEnv,
|
|
573
|
+
diff: envDiff,
|
|
574
|
+
},
|
|
575
|
+
replayedAt: new Date().toISOString(),
|
|
576
|
+
cliVersion: CLI_VERSION,
|
|
577
|
+
};
|
|
578
|
+
const evidenceAbsPath = writeFileAndVerify(options.evidence, JSON.stringify(evidence, null, 2));
|
|
579
|
+
console.log(`[nexart] Wrote replay evidence: ${evidenceAbsPath}`);
|
|
580
|
+
}
|
|
581
|
+
process.exit(pass ? 0 : 1);
|
|
426
582
|
}
|
|
427
583
|
async function verifyCommand(snapshotFile, options) {
|
|
428
584
|
const verifiedAt = new Date().toISOString();
|
|
@@ -511,8 +667,28 @@ async function verifyCommand(snapshotFile, options) {
|
|
|
511
667
|
}
|
|
512
668
|
else {
|
|
513
669
|
if (!options.json)
|
|
514
|
-
console.log(`[nexart]
|
|
515
|
-
|
|
670
|
+
console.log(`[nexart] Rendering locally (deterministic CodeMode SDK renderer)`);
|
|
671
|
+
// Environment drift check (additive). Mirrors `replay`. Only meaningful for
|
|
672
|
+
// local renders; emitted to stderr so it never pollutes machine-readable
|
|
673
|
+
// output and never affects the verification result. Legacy snapshots (no
|
|
674
|
+
// fingerprint) still verify, with an explanatory NOTE.
|
|
675
|
+
if (!options.json) {
|
|
676
|
+
if (snapshot.environment) {
|
|
677
|
+
const envDiff = diffEnvironmentFingerprints(snapshot.environment, buildEnvironmentFingerprint());
|
|
678
|
+
if (envDiff.length > 0) {
|
|
679
|
+
console.warn(`[nexart] WARNING: verify environment differs from capture environment: ${envDiff.join(', ')}`);
|
|
680
|
+
console.warn(`[nexart] byte-level reproducibility is only guaranteed within the same runtime/render libs.`);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
console.log(`[nexart] Environment matches capture fingerprint.`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
console.warn(`[nexart] NOTE: snapshot has no environment fingerprint (legacy snapshot) — cannot verify environment parity.`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const result = await renderLocal(code, snapshot.seed, snapshot.VAR, snapshot.canvas.width, snapshot.canvas.height);
|
|
691
|
+
actual = sha256(result.pngBytes);
|
|
516
692
|
}
|
|
517
693
|
const outputMatch = expected === actual;
|
|
518
694
|
if (options.json) {
|
|
@@ -901,6 +1077,17 @@ export async function aiCertifyCommand(file, opts, fetchFn = globalThis.fetch, s
|
|
|
901
1077
|
* `cli` envelope describing input handling. The SDK fields are never rewritten
|
|
902
1078
|
* or reinterpreted.
|
|
903
1079
|
*/
|
|
1080
|
+
/**
|
|
1081
|
+
* Map an AnchorVerifyResult to the user-facing label.
|
|
1082
|
+
* checked=false → NOT PRESENT
|
|
1083
|
+
* checked=true, valid → VALID
|
|
1084
|
+
* checked=true, !valid → INVALID
|
|
1085
|
+
*/
|
|
1086
|
+
function anchorLabel(status) {
|
|
1087
|
+
if (!status.checked)
|
|
1088
|
+
return 'NOT PRESENT';
|
|
1089
|
+
return status.valid ? 'VALID' : 'INVALID';
|
|
1090
|
+
}
|
|
904
1091
|
export async function aiVerifyCommand(file, opts, stdinReader = readStdinText) {
|
|
905
1092
|
const raw = await readInputJson(file, stdinReader);
|
|
906
1093
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
@@ -921,6 +1108,18 @@ export async function aiVerifyCommand(file, opts, stdinReader = readStdinText) {
|
|
|
921
1108
|
// Delegate ALL verification to the SDK. The CLI does not interpret, fall
|
|
922
1109
|
// back, or aggregate. The result object is the protocol's source of truth.
|
|
923
1110
|
const result = verifyAiCerBundleDetailed(bundleForVerification);
|
|
1111
|
+
// Anchoring Layer (opt-in via --anchors). Independently verified and reported
|
|
1112
|
+
// SEPARATELY from the three protocol layers. Anchor validity NEVER affects the
|
|
1113
|
+
// exit code or base verification — anchors are a secondary proof layer.
|
|
1114
|
+
const anchorStatus = opts.anchors
|
|
1115
|
+
? sdkVerifyAnchors(bundleForVerification)
|
|
1116
|
+
: null;
|
|
1117
|
+
// Trusted Timestamp Layer (opt-in via --timestamps). Same secondary-proof
|
|
1118
|
+
// semantics as anchors: verified independently (OFFLINE) and NEVER affecting
|
|
1119
|
+
// the exit code or base verification outcome.
|
|
1120
|
+
const timestampStatus = opts.timestamps
|
|
1121
|
+
? sdkVerifyTrustedTimestamps(bundleForVerification)
|
|
1122
|
+
: null;
|
|
924
1123
|
if (opts.json) {
|
|
925
1124
|
// Pass the SDK result through verbatim. Add only a small `cli` envelope
|
|
926
1125
|
// so callers can tell whether the input was a bundle or a package and
|
|
@@ -931,6 +1130,7 @@ export async function aiVerifyCommand(file, opts, stdinReader = readStdinText) {
|
|
|
931
1130
|
version: CLI_VERSION,
|
|
932
1131
|
sdkVersion: getSdkVersion(),
|
|
933
1132
|
inputType,
|
|
1133
|
+
protocolVersion: bundleForVerification?.snapshot?.protocolVersion ?? null,
|
|
934
1134
|
...(isPackage
|
|
935
1135
|
? {
|
|
936
1136
|
verifiedInnerCer: true,
|
|
@@ -938,6 +1138,8 @@ export async function aiVerifyCommand(file, opts, stdinReader = readStdinText) {
|
|
|
938
1138
|
}
|
|
939
1139
|
: {}),
|
|
940
1140
|
},
|
|
1141
|
+
...(anchorStatus ? { anchorStatus } : {}),
|
|
1142
|
+
...(timestampStatus ? { timestampStatus } : {}),
|
|
941
1143
|
};
|
|
942
1144
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
943
1145
|
}
|
|
@@ -950,12 +1152,47 @@ export async function aiVerifyCommand(file, opts, stdinReader = readStdinText) {
|
|
|
950
1152
|
}
|
|
951
1153
|
console.log(`Verification result: ${result.status}`);
|
|
952
1154
|
console.log(`bundleType: ${result.bundleType || '(missing)'}`);
|
|
1155
|
+
console.log(`protocolVersion: ${bundleForVerification?.snapshot?.protocolVersion || '(missing)'}`);
|
|
953
1156
|
console.log(`certificateHash: ${result.certificateHash || '(missing)'}`);
|
|
954
1157
|
console.log('');
|
|
955
1158
|
console.log('Layers:');
|
|
956
1159
|
console.log(` bundleIntegrity (Integrity): ${result.checks.bundleIntegrity}`);
|
|
957
1160
|
console.log(` nodeSignature (Receipt): ${result.checks.nodeSignature}`);
|
|
958
1161
|
console.log(` receiptConsistency (Envelope): ${result.checks.receiptConsistency}`);
|
|
1162
|
+
if (anchorStatus) {
|
|
1163
|
+
// Anchors are reported as an INDEPENDENT proof layer, separate from the
|
|
1164
|
+
// three protocol layers above. They never affect the exit code.
|
|
1165
|
+
console.log(` anchors (Anchoring): ${anchorLabel(anchorStatus)}`);
|
|
1166
|
+
if (anchorStatus.errors && anchorStatus.errors.length > 0) {
|
|
1167
|
+
for (const err of anchorStatus.errors) {
|
|
1168
|
+
console.log(` - ${err}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (timestampStatus) {
|
|
1173
|
+
// Trusted timestamps are an INDEPENDENT proof layer, like anchors. Never
|
|
1174
|
+
// affect the exit code.
|
|
1175
|
+
console.log(` trustedTimestamps (Timestamp): ${anchorLabel(timestampStatus)}`);
|
|
1176
|
+
if (timestampStatus.errors && timestampStatus.errors.length > 0) {
|
|
1177
|
+
for (const err of timestampStatus.errors) {
|
|
1178
|
+
console.log(` - ${err}`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
// Enterprise Trust summary — concise, layer-oriented overview. Signature is
|
|
1183
|
+
// derived from the node-signature protocol check; anchors/timestamps reflect
|
|
1184
|
+
// the opt-in secondary proof layers (NOT CHECKED when their flag is absent).
|
|
1185
|
+
const signatureLabel = result.checks.nodeSignature === 'PASS'
|
|
1186
|
+
? 'VALID'
|
|
1187
|
+
: result.checks.nodeSignature === 'FAIL'
|
|
1188
|
+
? 'INVALID'
|
|
1189
|
+
: 'NOT PRESENT';
|
|
1190
|
+
console.log('');
|
|
1191
|
+
console.log('Trust summary:');
|
|
1192
|
+
console.log(` Signature: ${signatureLabel}`);
|
|
1193
|
+
console.log(` Anchors: ${anchorStatus ? anchorLabel(anchorStatus) : 'NOT CHECKED'}`);
|
|
1194
|
+
console.log(` Timestamp: ${timestampStatus ? anchorLabel(timestampStatus) : 'NOT CHECKED'}`);
|
|
1195
|
+
console.log(` Protocol Version: ${bundleForVerification?.snapshot?.protocolVersion || '(missing)'}`);
|
|
959
1196
|
if (result.reasonCodes.length > 0) {
|
|
960
1197
|
console.log('');
|
|
961
1198
|
console.log('Reason codes:');
|
|
@@ -973,6 +1210,105 @@ export async function aiVerifyCommand(file, opts, stdinReader = readStdinText) {
|
|
|
973
1210
|
}
|
|
974
1211
|
process.exit(result.status === 'VERIFIED' ? 0 : 1);
|
|
975
1212
|
}
|
|
1213
|
+
/**
|
|
1214
|
+
* nexart ai attach-anchor — Attach an external anchor (proof of existence) to a
|
|
1215
|
+
* CER bundle and emit the updated bundle.
|
|
1216
|
+
*
|
|
1217
|
+
* Reads a CER bundle JSON and an anchor JSON, calls the SDK's pure
|
|
1218
|
+
* `attachAnchor()` (new object, no mutation), and writes the result to stdout
|
|
1219
|
+
* (or --out). The certificateHash is GUARANTEED unchanged: anchors are
|
|
1220
|
+
* non-evidentiary and never participate in hashing or signing.
|
|
1221
|
+
*
|
|
1222
|
+
* The SDK does NOT generate anchors — they come from external systems (e.g. the
|
|
1223
|
+
* attestation node's transparency log). This command only attaches a supplied
|
|
1224
|
+
* anchor.
|
|
1225
|
+
*/
|
|
1226
|
+
export async function aiAttachAnchorCommand(bundleFile, anchorFile, opts) {
|
|
1227
|
+
let rawBundle;
|
|
1228
|
+
let rawAnchor;
|
|
1229
|
+
try {
|
|
1230
|
+
rawBundle = JSON.parse(fs.readFileSync(bundleFile, 'utf-8'));
|
|
1231
|
+
}
|
|
1232
|
+
catch (err) {
|
|
1233
|
+
console.error(`[nexart] Error: could not read bundle "${bundleFile}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1234
|
+
process.exit(1);
|
|
1235
|
+
}
|
|
1236
|
+
try {
|
|
1237
|
+
rawAnchor = JSON.parse(fs.readFileSync(anchorFile, 'utf-8'));
|
|
1238
|
+
}
|
|
1239
|
+
catch (err) {
|
|
1240
|
+
console.error(`[nexart] Error: could not read anchor "${anchorFile}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
if (!rawBundle || typeof rawBundle !== 'object' || Array.isArray(rawBundle)) {
|
|
1244
|
+
console.error('[nexart] Error: bundle input is not a valid CER bundle object');
|
|
1245
|
+
process.exit(1);
|
|
1246
|
+
}
|
|
1247
|
+
if (!rawAnchor || typeof rawAnchor !== 'object' || Array.isArray(rawAnchor)) {
|
|
1248
|
+
console.error('[nexart] Error: anchor input is not a valid anchor object');
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
}
|
|
1251
|
+
const bundle = rawBundle;
|
|
1252
|
+
const anchor = rawAnchor;
|
|
1253
|
+
const updated = sdkAttachAnchor(bundle, anchor);
|
|
1254
|
+
const json = JSON.stringify(updated, null, 2);
|
|
1255
|
+
if (opts.out) {
|
|
1256
|
+
fs.writeFileSync(opts.out, json + '\n');
|
|
1257
|
+
console.error(`[nexart] Anchored bundle written to ${opts.out}`);
|
|
1258
|
+
console.error(`[nexart] certificateHash unchanged: ${updated.certificateHash}`);
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
process.stdout.write(json + '\n');
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* nexart export-verification — Produce a self-contained verification package for
|
|
1266
|
+
* a CER bundle so a third party can verify it INDEPENDENTLY (no NexArt node, no
|
|
1267
|
+
* this SDK required).
|
|
1268
|
+
*
|
|
1269
|
+
* Delegates entirely to the SDK's `exportVerificationPackage()`. The CLI only
|
|
1270
|
+
* handles file/stdin I/O and (optionally) supplies key material:
|
|
1271
|
+
* --public-key <b64url> raw Ed25519 public key (highest priority)
|
|
1272
|
+
* --node-keys <file> a NodeKeysDocument JSON to select the receipt's key from
|
|
1273
|
+
* --out <file> write the package to a file (default: stdout)
|
|
1274
|
+
*
|
|
1275
|
+
* Read-only and additive: never re-hashes, re-signs, or mutates the bundle.
|
|
1276
|
+
*/
|
|
1277
|
+
export async function exportVerificationCommand(file, opts, stdinReader = readStdinText) {
|
|
1278
|
+
const raw = await readInputJson(file, stdinReader);
|
|
1279
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1280
|
+
console.error('[nexart] Error: Input is not a valid CER bundle or CER package object');
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
1283
|
+
// Accept either a raw bundle or a CER package (unwrap to the inner bundle).
|
|
1284
|
+
const bundle = sdkIsCerPackage(raw)
|
|
1285
|
+
? getCerFromPackage(raw)
|
|
1286
|
+
: raw;
|
|
1287
|
+
let nodeKeys;
|
|
1288
|
+
if (opts.nodeKeys) {
|
|
1289
|
+
try {
|
|
1290
|
+
nodeKeys = JSON.parse(fs.readFileSync(opts.nodeKeys, 'utf-8'));
|
|
1291
|
+
}
|
|
1292
|
+
catch (err) {
|
|
1293
|
+
console.error(`[nexart] Error: could not read node keys "${opts.nodeKeys}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1294
|
+
process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
const pkg = sdkExportVerificationPackage(bundle, {
|
|
1298
|
+
publicKey: opts.publicKey,
|
|
1299
|
+
nodeKeys,
|
|
1300
|
+
});
|
|
1301
|
+
const json = JSON.stringify(pkg, null, 2);
|
|
1302
|
+
if (opts.out) {
|
|
1303
|
+
fs.writeFileSync(opts.out, json + '\n');
|
|
1304
|
+
console.error(`[nexart] Verification package written to ${opts.out}`);
|
|
1305
|
+
console.error(`[nexart] certificateHash: ${pkg.bundle.certificateHash}`);
|
|
1306
|
+
console.error(`[nexart] publicKey: ${pkg.publicKey ?? '(not resolved — supply --public-key or --node-keys)'}`);
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
process.stdout.write(json + '\n');
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
976
1312
|
/**
|
|
977
1313
|
* nexart ai project-verify — Read a cer.project.bundle.v1 JSON from file or stdin,
|
|
978
1314
|
* delegate all verification to verifyProjectBundle() from @nexart/ai-execution,
|
|
@@ -1142,7 +1478,7 @@ if (_isMainModule) {
|
|
|
1142
1478
|
default: 2400,
|
|
1143
1479
|
})
|
|
1144
1480
|
.option('renderer', {
|
|
1145
|
-
describe: 'Renderer mode: remote (default) or local (
|
|
1481
|
+
describe: 'Renderer mode: remote (default) or local (deterministic CodeMode SDK renderer)',
|
|
1146
1482
|
choices: ['local', 'remote'],
|
|
1147
1483
|
default: 'remote',
|
|
1148
1484
|
})
|
|
@@ -1196,7 +1532,7 @@ if (_isMainModule) {
|
|
|
1196
1532
|
type: 'string',
|
|
1197
1533
|
})
|
|
1198
1534
|
.option('renderer', {
|
|
1199
|
-
describe: 'Renderer mode',
|
|
1535
|
+
describe: 'Renderer mode: remote (default) or local (deterministic CodeMode SDK renderer)',
|
|
1200
1536
|
choices: ['local', 'remote'],
|
|
1201
1537
|
default: 'remote',
|
|
1202
1538
|
})
|
|
@@ -1205,7 +1541,12 @@ if (_isMainModule) {
|
|
|
1205
1541
|
type: 'string',
|
|
1206
1542
|
default: DEFAULT_ENDPOINT,
|
|
1207
1543
|
})
|
|
1208
|
-
.
|
|
1544
|
+
.option('evidence', {
|
|
1545
|
+
describe: 'Write a JSON replay-evidence file (PASS/FAIL + hash comparison) to this path',
|
|
1546
|
+
type: 'string',
|
|
1547
|
+
})
|
|
1548
|
+
.options(apiKeyOption)
|
|
1549
|
+
.example('$0 replay sketch.snapshot.json --renderer local --evidence replay.evidence.json', 'Deterministically replay locally and emit PASS/FAIL evidence');
|
|
1209
1550
|
}, async (argv) => {
|
|
1210
1551
|
await replayCommand(argv.snapshot, {
|
|
1211
1552
|
out: argv.out,
|
|
@@ -1213,6 +1554,7 @@ if (_isMainModule) {
|
|
|
1213
1554
|
renderer: argv.renderer,
|
|
1214
1555
|
endpoint: argv.endpoint,
|
|
1215
1556
|
apiKey: argv['api-key'],
|
|
1557
|
+
evidence: argv.evidence,
|
|
1216
1558
|
});
|
|
1217
1559
|
})
|
|
1218
1560
|
.command('verify <snapshot>', 'Verify a snapshot produces the expected output', (yargs) => {
|
|
@@ -1228,7 +1570,7 @@ if (_isMainModule) {
|
|
|
1228
1570
|
type: 'string',
|
|
1229
1571
|
})
|
|
1230
1572
|
.option('renderer', {
|
|
1231
|
-
describe: 'Renderer mode',
|
|
1573
|
+
describe: 'Renderer mode: remote (default) or local (deterministic CodeMode SDK renderer)',
|
|
1232
1574
|
choices: ['local', 'remote'],
|
|
1233
1575
|
default: 'remote',
|
|
1234
1576
|
})
|
|
@@ -1350,13 +1692,46 @@ if (_isMainModule) {
|
|
|
1350
1692
|
describe: 'Print machine-readable JSON result',
|
|
1351
1693
|
type: 'boolean',
|
|
1352
1694
|
default: false,
|
|
1695
|
+
})
|
|
1696
|
+
.option('anchors', {
|
|
1697
|
+
describe: 'Also verify the bundle\'s anchors (secondary proof layer; never affects exit code)',
|
|
1698
|
+
type: 'boolean',
|
|
1699
|
+
default: false,
|
|
1700
|
+
})
|
|
1701
|
+
.option('timestamps', {
|
|
1702
|
+
describe: 'Also verify the bundle\'s trusted timestamps (secondary proof layer; never affects exit code)',
|
|
1703
|
+
type: 'boolean',
|
|
1704
|
+
default: false,
|
|
1353
1705
|
})
|
|
1354
1706
|
.example('$0 ai verify cer.json', 'Verify a raw CER bundle')
|
|
1355
1707
|
.example('$0 ai verify cer-package.json', 'Verify a CER package (inner bundle verified)')
|
|
1356
1708
|
.example('cat cer.json | $0 ai verify', 'Verify from stdin')
|
|
1357
|
-
.example('$0 ai verify cer.json --json', 'Machine-readable output')
|
|
1709
|
+
.example('$0 ai verify cer.json --json', 'Machine-readable output')
|
|
1710
|
+
.example('$0 ai verify cer.json --anchors', 'Verify and report anchor status (VALID/INVALID/NOT PRESENT)')
|
|
1711
|
+
.example('$0 ai verify cer.json --anchors --timestamps', 'Report anchor + trusted timestamp status with a trust summary');
|
|
1358
1712
|
}, async (argv) => {
|
|
1359
|
-
await aiVerifyCommand(argv.file, { json: argv.json });
|
|
1713
|
+
await aiVerifyCommand(argv.file, { json: argv.json, anchors: argv.anchors, timestamps: argv.timestamps });
|
|
1714
|
+
})
|
|
1715
|
+
.command('attach-anchor <bundle> <anchor>', 'Attach an external anchor (proof of existence) to a CER bundle — certificateHash is unchanged', (yargs) => {
|
|
1716
|
+
return yargs
|
|
1717
|
+
.positional('bundle', {
|
|
1718
|
+
describe: 'Path to the CER bundle JSON to anchor',
|
|
1719
|
+
type: 'string',
|
|
1720
|
+
})
|
|
1721
|
+
.positional('anchor', {
|
|
1722
|
+
describe: 'Path to the anchor JSON to attach (supplied by an external system, e.g. the attestation node)',
|
|
1723
|
+
type: 'string',
|
|
1724
|
+
})
|
|
1725
|
+
.option('out', {
|
|
1726
|
+
alias: 'o',
|
|
1727
|
+
describe: 'Save the anchored bundle to this file (default: stdout)',
|
|
1728
|
+
type: 'string',
|
|
1729
|
+
})
|
|
1730
|
+
.example('$0 ai attach-anchor cer.json anchor.json', 'Attach an anchor and print the updated bundle')
|
|
1731
|
+
.example('$0 ai attach-anchor cer.json anchor.json --out anchored.json', 'Save the anchored bundle')
|
|
1732
|
+
.example('$0 ai attach-anchor cer.json anchor.json | $0 ai verify --anchors', 'Attach then verify anchors in a pipe');
|
|
1733
|
+
}, async (argv) => {
|
|
1734
|
+
await aiAttachAnchorCommand(argv.bundle, argv.anchor, { out: argv.out });
|
|
1360
1735
|
})
|
|
1361
1736
|
.command('project-verify [file]', 'Verify a project bundle (cer.project.bundle.v1) locally (no network required)', (yargs) => {
|
|
1362
1737
|
return yargs
|
|
@@ -1376,9 +1751,38 @@ if (_isMainModule) {
|
|
|
1376
1751
|
}, async (argv) => {
|
|
1377
1752
|
await aiProjectVerifyCommand(argv.file, { json: argv.json });
|
|
1378
1753
|
})
|
|
1379
|
-
.demandCommand(1, 'You must provide a subcommand: create, certify, verify, or project-verify')
|
|
1754
|
+
.demandCommand(1, 'You must provide a subcommand: create, certify, seal, verify, attach-anchor, or project-verify')
|
|
1380
1755
|
.help();
|
|
1381
1756
|
}, () => { })
|
|
1757
|
+
.command('export-verification [file]', 'Produce a self-contained verification package so a third party can verify a CER bundle independently', (yargs) => {
|
|
1758
|
+
return yargs
|
|
1759
|
+
.positional('file', {
|
|
1760
|
+
describe: 'Path to CER bundle or CER package JSON (omit to read from stdin)',
|
|
1761
|
+
type: 'string',
|
|
1762
|
+
})
|
|
1763
|
+
.option('node-keys', {
|
|
1764
|
+
describe: 'Path to a NodeKeysDocument JSON; the receipt\'s public key is selected from it',
|
|
1765
|
+
type: 'string',
|
|
1766
|
+
})
|
|
1767
|
+
.option('public-key', {
|
|
1768
|
+
describe: 'Raw Ed25519 public key (base64url) to embed (highest priority)',
|
|
1769
|
+
type: 'string',
|
|
1770
|
+
})
|
|
1771
|
+
.option('out', {
|
|
1772
|
+
alias: 'o',
|
|
1773
|
+
describe: 'Save the verification package to this file (default: stdout)',
|
|
1774
|
+
type: 'string',
|
|
1775
|
+
})
|
|
1776
|
+
.example('$0 export-verification cer.json', 'Print a verification package to stdout')
|
|
1777
|
+
.example('$0 export-verification cer.json --node-keys keys.json --out pkg.json', 'Resolve the public key and save the package')
|
|
1778
|
+
.example('cat cer.json | $0 export-verification', 'Build a package from stdin');
|
|
1779
|
+
}, async (argv) => {
|
|
1780
|
+
await exportVerificationCommand(argv.file, {
|
|
1781
|
+
out: argv.out,
|
|
1782
|
+
nodeKeys: argv['node-keys'],
|
|
1783
|
+
publicKey: argv['public-key'],
|
|
1784
|
+
});
|
|
1785
|
+
})
|
|
1382
1786
|
.demandCommand(1, 'You must provide a command')
|
|
1383
1787
|
.help()
|
|
1384
1788
|
.version(`@nexart/cli ${CLI_VERSION}
|