@certivu/cli 2.0.1 → 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 +168 -39
  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
@@ -304,10 +304,11 @@ function inferFormat(filePath) {
304
304
  if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image";
305
305
  if ([".mp3", ".flac", ".wav", ".ogg", ".aac", ".m4a", ".aiff"].includes(ext)) return "audio";
306
306
  if ([".pdf", ".docx", ".html", ".htm", ".txt", ".md"].includes(ext)) return "text";
307
+ if ([".mp4", ".mov", ".mkv", ".webm"].includes(ext)) return "video";
307
308
  return void 0;
308
309
  }
309
310
  async function signCommand(filePath, flags) {
310
- if (!filePath) die("Usage: certivu sign <file> --model <model> [--format image|audio|text]");
311
+ if (!filePath) die("Usage: certivu sign <file> --model <model> [--format image|audio|text|video]");
311
312
  const config = await loadConfig();
312
313
  const apiKey = flags.apiKey ?? config.apiKey;
313
314
  const generatorId = flags.generatorId ?? config.generatorId;
@@ -381,59 +382,183 @@ async function statusCommand(token, flags) {
381
382
 
382
383
  // src/commands/verify.ts
383
384
  var import_node_fs3 = require("fs");
384
- async function verifyCommand(filePath, flags) {
385
- if (!filePath) die("Usage: certivu verify <file> [--token <ctv_token>]");
386
- const config = await loadConfig();
387
- 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;
388
432
  try {
389
- content = new Uint8Array((0, import_node_fs3.readFileSync)(filePath));
433
+ entries = (0, import_node_fs3.readdirSync)(dir, { encoding: "utf8" });
390
434
  } catch {
391
- 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
+ }
392
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();
393
488
  const client = new CertivuClient({
394
489
  apiKey: flags.apiKey ?? config.apiKey ?? "public",
395
490
  ...flags.baseUrl || config.baseUrl ? { baseUrl: flags.baseUrl ?? config.baseUrl } : {}
396
491
  });
397
- let result;
398
- try {
399
- result = await client.verify({ content, ...flags.token ? { token: flags.token } : {} });
400
- } catch (e) {
401
- const msg = e instanceof Error ? e.message : String(e);
402
- die(`Verify failed: ${msg}`);
403
- }
404
- if (result.authentic) {
405
- console.log(ok(`Authentic \u2014 ${confidenceBadge(result.confidence)} confidence`));
406
- if (result.provenance) {
407
- row("Org", result.provenance.org);
408
- row("Model", result.provenance.model);
409
- 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)}`);
410
503
  }
411
- if (result.token_source) row("Source", result.token_source);
412
- } else if (result.tampered) {
413
- console.log(err("Tampered \u2014 content has been modified since signing"));
414
- row("Reason", result.reason ?? "hash mismatch");
415
- } else {
416
- console.log(err(`Not verified \u2014 ${result.reason ?? "no provenance found"}`));
417
- console.log(" Absence of provenance does not imply human origin.");
418
- }
419
- const { signals } = result;
420
- const signalLine = [
421
- signals.watermark_found ? "watermark \u2713" : "watermark \u2717",
422
- signals.record_found ? "record \u2713" : "record \u2717",
423
- signals.signature_valid ? "signature \u2713" : "signature \u2717"
424
- ].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
+ }
425
547
  console.log(`
426
- Signals: ${signalLine}`);
548
+ ${files.length} checked \xB7 ${verified} verified \xB7 ${failedCount} without valid provenance`);
549
+ if (failOnMissing && failedCount > 0) {
550
+ process.exit(1);
551
+ }
427
552
  }
428
553
 
429
554
  // src/index.ts
430
- var VERSION = "1.1.0";
555
+ var VERSION = "2.3.0";
431
556
  var HELP = `
432
557
  ${amber("certivu")} \u2014 quantum-resistant AI content trust
433
558
 
434
559
  ${bold("Usage:")}
435
560
  certivu sign <file> --model <model> [flags]
436
- certivu verify <file> [--token <ctv_token>]
561
+ certivu verify <file|glob...> [--token <ctv_token>] [--fail-on-missing]
437
562
  certivu status <ctv_token>
438
563
  certivu config [get | set <key> <value>]
439
564
 
@@ -445,13 +570,16 @@ ${bold("Commands:")}
445
570
 
446
571
  ${bold("Sign flags:")}
447
572
  --model <name> AI model name, e.g. stable-diffusion-xl ${dim("(required)")}
448
- --format <fmt> Content format: image | audio | text ${dim("(auto-detected if omitted)")}
573
+ --format <fmt> Content format: image | audio | text | video ${dim("(auto-detected if omitted)")}
449
574
  --generator-id <id> Override generator ID
450
575
  --private-key <key> Override ML-DSA private key (base64)
451
576
  --api-key <key> Override API key
452
577
 
453
578
  ${bold("Verify flags:")}
454
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')}
455
583
 
456
584
  ${bold("Global flags:")}
457
585
  --base-url <url> Override API base URL ${dim("(default: https://api.certivu.ai)")}
@@ -468,6 +596,7 @@ ${bold("Examples:")}
468
596
  certivu config set api-key ctv_key_abc123
469
597
  certivu sign ./output.jpg --model stable-diffusion-xl
470
598
  certivu verify ./output.jpg
599
+ certivu verify "assets/**/*.{png,jpg,mp4}" --fail-on-missing
471
600
  certivu status ctv_7f3kx9mq2...
472
601
  `.trim();
473
602
  function parseArgs(argv) {
@@ -509,7 +638,7 @@ async function main() {
509
638
  await signCommand(positional[0] ?? "", flags);
510
639
  break;
511
640
  case "verify":
512
- await verifyCommand(positional[0] ?? "", flags);
641
+ await verifyCommand(positional, flags);
513
642
  break;
514
643
  case "status":
515
644
  await statusCommand(positional[0] ?? "", flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@certivu/cli",
3
- "version": "2.0.1",
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",