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