@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/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] WARNING: Local renderer NOT implemented yet — outputs placeholder image only`);
343
- pngBytes = createPlaceholderPng();
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] WARNING: Local renderer NOT implemented yet — outputs placeholder image only`);
419
- pngBytes = createPlaceholderPng();
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] WARNING: Local renderer NOT implemented — using placeholder hash`);
515
- actual = sha256(createPlaceholderPng());
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 (placeholder)',
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
- .options(apiKeyOption);
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}