@certivu/cli 2.1.0 → 2.3.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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +165 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -72,6 +72,13 @@ certivu verify ./output.jpg
72
72
  | Flag | Description |
73
73
  |------|-------------|
74
74
  | `--token <ctv_token>` | Provide token explicitly (extracted automatically if omitted) |
75
+ | `--fail-on-missing` | Exit non-zero if any file lacks valid provenance (for CI) |
76
+
77
+ `verify` also accepts multiple files and glob patterns (quote them — the CLI expands them itself):
78
+
79
+ ```bash
80
+ certivu verify "assets/**/*.{png,jpg,mp4}" --fail-on-missing
81
+ ```
75
82
 
76
83
  ### `certivu status <ctv_token>`
77
84
 
package/dist/index.js CHANGED
@@ -382,59 +382,183 @@ async function statusCommand(token, flags) {
382
382
 
383
383
  // src/commands/verify.ts
384
384
  var import_node_fs3 = require("fs");
385
- async function verifyCommand(filePath, flags) {
386
- if (!filePath) die("Usage: certivu verify <file> [--token <ctv_token>]");
387
- const config = await loadConfig();
388
- let content;
385
+ var import_node_path3 = require("path");
386
+ var GLOB_CHARS = /[*?{[]/;
387
+ function expandBraces(pattern) {
388
+ const match = pattern.match(/\{([^{}]*)\}/);
389
+ if (!match) return [pattern];
390
+ const whole = match[0];
391
+ const body = match[1] ?? "";
392
+ const out = [];
393
+ for (const choice of body.split(",")) {
394
+ out.push(...expandBraces(pattern.replace(whole, choice)));
395
+ }
396
+ return out;
397
+ }
398
+ function globToRegExp(glob) {
399
+ let re = "";
400
+ for (let i = 0; i < glob.length; i++) {
401
+ const c2 = glob[i];
402
+ if (c2 === "*") {
403
+ if (glob[i + 1] === "*") {
404
+ i++;
405
+ if (glob[i + 1] === "/") i++;
406
+ re += "(?:.*/)?";
407
+ } else {
408
+ re += "[^/]*";
409
+ }
410
+ } else if (c2 === "?") {
411
+ re += "[^/]";
412
+ } else if ("\\^$+.()|[]{}".includes(c2)) {
413
+ re += `\\${c2}`;
414
+ } else {
415
+ re += c2;
416
+ }
417
+ }
418
+ return new RegExp(`^${re}$`);
419
+ }
420
+ function staticBase(glob) {
421
+ const parts = glob.split("/");
422
+ const fixed = [];
423
+ for (const p of parts) {
424
+ if (GLOB_CHARS.test(p)) break;
425
+ fixed.push(p);
426
+ }
427
+ return fixed.join("/") || ".";
428
+ }
429
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".svn", ".hg"]);
430
+ function walk(dir, acc) {
431
+ let entries;
389
432
  try {
390
- content = new Uint8Array((0, import_node_fs3.readFileSync)(filePath));
433
+ entries = (0, import_node_fs3.readdirSync)(dir, { encoding: "utf8" });
391
434
  } catch {
392
- die(`Cannot read file: ${filePath}`);
435
+ return;
436
+ }
437
+ for (const name of entries) {
438
+ if (SKIP_DIRS.has(name)) continue;
439
+ const full = (0, import_node_path3.join)(dir, name);
440
+ try {
441
+ if ((0, import_node_fs3.statSync)(full).isDirectory()) walk(full, acc);
442
+ else acc.push(full);
443
+ } catch {
444
+ }
445
+ }
446
+ }
447
+ function resolveTargets(targets) {
448
+ const found = /* @__PURE__ */ new Set();
449
+ for (const target of targets) {
450
+ if (!GLOB_CHARS.test(target)) {
451
+ try {
452
+ if ((0, import_node_fs3.statSync)(target).isFile()) found.add(target);
453
+ } catch {
454
+ die(`Cannot read file: ${target}`);
455
+ }
456
+ continue;
457
+ }
458
+ for (const pattern of expandBraces(target)) {
459
+ const base = staticBase(pattern);
460
+ const files = [];
461
+ try {
462
+ if ((0, import_node_fs3.statSync)(base).isFile()) {
463
+ found.add(base);
464
+ continue;
465
+ }
466
+ } catch {
467
+ }
468
+ walk(base, files);
469
+ const rx = globToRegExp(pattern);
470
+ for (const f of files) {
471
+ const rel = (0, import_node_path3.relative)(".", f).split(import_node_path3.sep).join("/");
472
+ if (rx.test(rel) || rx.test(f.split(import_node_path3.sep).join("/"))) found.add(f);
473
+ }
474
+ }
393
475
  }
476
+ return [...found].sort();
477
+ }
478
+ async function verifyOne(client, filePath, token) {
479
+ const content = new Uint8Array((0, import_node_fs3.readFileSync)(filePath));
480
+ return client.verify({ content, ...token ? { token } : {} });
481
+ }
482
+ async function verifyCommand(targets, flags) {
483
+ if (!targets || targets.length === 0) {
484
+ die("Usage: certivu verify <file|glob...> [--token <ctv_token>] [--fail-on-missing]");
485
+ }
486
+ const failOnMissing = flags.failOnMissing === true || flags["fail-on-missing"] === true;
487
+ const config = await loadConfig();
394
488
  const client = new CertivuClient({
395
489
  apiKey: flags.apiKey ?? config.apiKey ?? "public",
396
490
  ...flags.baseUrl || config.baseUrl ? { baseUrl: flags.baseUrl ?? config.baseUrl } : {}
397
491
  });
398
- let result;
399
- try {
400
- result = await client.verify({ content, ...flags.token ? { token: flags.token } : {} });
401
- } catch (e) {
402
- const msg = e instanceof Error ? e.message : String(e);
403
- die(`Verify failed: ${msg}`);
404
- }
405
- if (result.authentic) {
406
- console.log(ok(`Authentic \u2014 ${confidenceBadge(result.confidence)} confidence`));
407
- if (result.provenance) {
408
- row("Org", result.provenance.org);
409
- row("Model", result.provenance.model);
410
- row("Signed", result.provenance.signed_at);
492
+ const files = resolveTargets(targets);
493
+ if (files.length === 0) {
494
+ die(`No files matched: ${targets.join(", ")}`);
495
+ }
496
+ if (files.length === 1 && !failOnMissing) {
497
+ const filePath = files[0];
498
+ let result;
499
+ try {
500
+ result = await verifyOne(client, filePath, flags.token);
501
+ } catch (e) {
502
+ die(`Verify failed: ${e instanceof Error ? e.message : String(e)}`);
411
503
  }
412
- if (result.token_source) row("Source", result.token_source);
413
- } else if (result.tampered) {
414
- console.log(err("Tampered \u2014 content has been modified since signing"));
415
- row("Reason", result.reason ?? "hash mismatch");
416
- } else {
417
- console.log(err(`Not verified \u2014 ${result.reason ?? "no provenance found"}`));
418
- console.log(" Absence of provenance does not imply human origin.");
419
- }
420
- const { signals } = result;
421
- const signalLine = [
422
- signals.watermark_found ? "watermark \u2713" : "watermark \u2717",
423
- signals.record_found ? "record \u2713" : "record \u2717",
424
- signals.signature_valid ? "signature \u2713" : "signature \u2717"
425
- ].join(" ");
504
+ if (result.authentic) {
505
+ console.log(ok(`Authentic \u2014 ${confidenceBadge(result.confidence)} confidence`));
506
+ if (result.provenance) {
507
+ row("Org", result.provenance.org);
508
+ row("Model", result.provenance.model);
509
+ row("Signed", result.provenance.signed_at);
510
+ }
511
+ if (result.token_source) row("Source", result.token_source);
512
+ } else if (result.tampered) {
513
+ console.log(err("Tampered \u2014 content has been modified since signing"));
514
+ row("Reason", result.reason ?? "hash mismatch");
515
+ } else {
516
+ console.log(err(`Not verified \u2014 ${result.reason ?? "no provenance found"}`));
517
+ console.log(" Absence of provenance does not imply human origin.");
518
+ }
519
+ const { signals } = result;
520
+ console.log(
521
+ `
522
+ Signals: ${[
523
+ signals.watermark_found ? "watermark \u2713" : "watermark \u2717",
524
+ signals.record_found ? "record \u2713" : "record \u2717",
525
+ signals.signature_valid ? "signature \u2713" : "signature \u2717"
526
+ ].join(" ")}`
527
+ );
528
+ return;
529
+ }
530
+ let verified = 0;
531
+ let failedCount = 0;
532
+ for (const filePath of files) {
533
+ try {
534
+ const result = await verifyOne(client, filePath);
535
+ if (result.authentic) {
536
+ verified++;
537
+ console.log(ok(`${filePath} \u2014 authentic (${result.confidence})`));
538
+ } else {
539
+ failedCount++;
540
+ console.log(err(`${filePath} \u2014 ${result.tampered ? "tampered" : result.reason ?? "no provenance"}`));
541
+ }
542
+ } catch (e) {
543
+ failedCount++;
544
+ console.log(err(`${filePath} \u2014 error: ${e instanceof Error ? e.message : String(e)}`));
545
+ }
546
+ }
426
547
  console.log(`
427
- Signals: ${signalLine}`);
548
+ ${files.length} checked \xB7 ${verified} verified \xB7 ${failedCount} without valid provenance`);
549
+ if (failOnMissing && failedCount > 0) {
550
+ process.exit(1);
551
+ }
428
552
  }
429
553
 
430
554
  // src/index.ts
431
- var VERSION = "1.1.0";
555
+ var VERSION = "2.3.0";
432
556
  var HELP = `
433
557
  ${amber("certivu")} \u2014 quantum-resistant AI content trust
434
558
 
435
559
  ${bold("Usage:")}
436
560
  certivu sign <file> --model <model> [flags]
437
- certivu verify <file> [--token <ctv_token>]
561
+ certivu verify <file|glob...> [--token <ctv_token>] [--fail-on-missing]
438
562
  certivu status <ctv_token>
439
563
  certivu config [get | set <key> <value>]
440
564
 
@@ -453,6 +577,9 @@ ${bold("Sign flags:")}
453
577
 
454
578
  ${bold("Verify flags:")}
455
579
  --token <ctv_token> Provide token explicitly (extracted automatically if omitted)
580
+ --fail-on-missing Exit non-zero if any file lacks valid provenance ${dim("(for CI)")}
581
+
582
+ Accepts multiple files and glob patterns, e.g. ${dim('certivu verify "assets/**/*.{png,jpg}" --fail-on-missing')}
456
583
 
457
584
  ${bold("Global flags:")}
458
585
  --base-url <url> Override API base URL ${dim("(default: https://api.certivu.ai)")}
@@ -469,6 +596,7 @@ ${bold("Examples:")}
469
596
  certivu config set api-key ctv_key_abc123
470
597
  certivu sign ./output.jpg --model stable-diffusion-xl
471
598
  certivu verify ./output.jpg
599
+ certivu verify "assets/**/*.{png,jpg,mp4}" --fail-on-missing
472
600
  certivu status ctv_7f3kx9mq2...
473
601
  `.trim();
474
602
  function parseArgs(argv) {
@@ -510,7 +638,7 @@ async function main() {
510
638
  await signCommand(positional[0] ?? "", flags);
511
639
  break;
512
640
  case "verify":
513
- await verifyCommand(positional[0] ?? "", flags);
641
+ await verifyCommand(positional, flags);
514
642
  break;
515
643
  case "status":
516
644
  await statusCommand(positional[0] ?? "", flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@certivu/cli",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Certivu CLI — sign and verify AI-generated content",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://certivu.ai",