@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.
- package/README.md +7 -0
- package/dist/index.js +168 -39
- 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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
433
|
+
entries = (0, import_node_fs3.readdirSync)(dir, { encoding: "utf8" });
|
|
390
434
|
} catch {
|
|
391
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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 = "
|
|
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
|
|
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
|
|
641
|
+
await verifyCommand(positional, flags);
|
|
513
642
|
break;
|
|
514
643
|
case "status":
|
|
515
644
|
await statusCommand(positional[0] ?? "", flags);
|