veriskit 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-07-13
4
+
5
+ ### Added
6
+ - Publish to GitHub. `veris verify --github` posts and updates one sticky PR comment with the verdict and report, and creates a Check Run (verified passes, failed fails, partial is neutral). Token read from `GITHUB_TOKEN`; publishing never changes the verdict or exit code. `veris badge` writes a shields.io endpoint JSON. GitHub API over built-in fetch, no new dependency.
7
+ - Browser tests. A real Playwright runner, opt-in with `veris verify --browser` (or a `browser` entry in `.veris/config.json`). Detected Playwright now shows as an available capability.
8
+ - `veris log` lists past runs from the stored evidence records, and `veris log --flaky` flags checks that both passed and failed across recent runs. Local per-machine history.
9
+
10
+ ## 0.4.1 — 2026-07-10
11
+
12
+ ### Added
13
+ - Signed evidence. `veris evidence keygen` creates an Ed25519 keypair (via Node's built-in crypto, no new dependency); `veris evidence sign <evidence.json>` writes a detached signature; `veris evidence verify` checks a sibling signature automatically and can assert the signer with `--pubkey` or `--key-id`. Bundles carry the signature. Signing is opt-in; unsigned evidence still verifies for integrity. `VERISKIT_SIGNING_KEY` supplies the key in CI.
14
+
15
+ ### Changed
16
+ - `veris init` now gitignores `keys/`.
17
+
3
18
  ## 0.4.0 — 2026-07-10
4
19
 
5
20
  ### Added
package/README.md CHANGED
@@ -190,7 +190,7 @@ veris evidence verify .veris/runs/<run-id>/evidence.json
190
190
  This recomputes the digest and reports whether the record was edited or
191
191
  corrupted since it was written. An integrity digest is not forgery-proof on its
192
192
  own. To prove authorship, publish the digest separately (a CI log or PR) or sign
193
- it. Cryptographic signing is planned for a later release.
193
+ the record with an Ed25519 key (see Signing below).
194
194
 
195
195
  Package a run as a single portable file (the record, its report, and its logs,
196
196
  each with a digest, plus a bundle digest over everything):
@@ -202,6 +202,28 @@ veris evidence verify <bundle> # checks the record, every log, and the report
202
202
 
203
203
  `veris evidence show` prints the latest record's key facts.
204
204
 
205
+ ### Signing (optional)
206
+
207
+ An integrity digest proves a record was not edited. A signature proves a
208
+ specific key vouched for it. VerisKit signs with Ed25519 from Node's built-in
209
+ crypto, so signing adds no dependency and works offline.
210
+
211
+ ```bash
212
+ veris evidence keygen # writes .veris/keys/veriskit-signing.key(.pub)
213
+ veris evidence sign .veris/runs/<id>/evidence.json --key .veris/keys/veriskit-signing.key
214
+ veris evidence verify .veris/runs/<id>/evidence.json --pubkey .veris/keys/veriskit-signing.key.pub
215
+ ```
216
+
217
+ `sign` writes a detached `<evidence.json>.sig` next to the record. `verify`
218
+ picks it up automatically and checks it. In CI, pass the key through the
219
+ `VERISKIT_SIGNING_KEY` environment variable instead of a file.
220
+
221
+ A signature proves that whoever holds the private key signed the record. It
222
+ does not prove who that is. VerisKit prints the key fingerprint; to bind it to
223
+ a person or system, compare that fingerprint to one you already trust, or
224
+ assert it with `--pubkey` or `--key-id`. Keep the private key secret; `keygen`
225
+ writes it into `.veris/keys/`, which `veris init` gitignores.
226
+
205
227
  Commit `.veris/config.json` and `.veris/.gitignore`. `veris init` keeps `runs/`,
206
228
  `reports/`, `cache/`, `graph.json`, and `evidence/` out of your history.
207
229
 
@@ -213,7 +235,65 @@ VerisKit says what it cannot do as plainly as what it can:
213
235
  - **No test generation.** `plan` tells you what to test. Writing the tests is a later release.
214
236
  - **One project root.** A monorepo with several `tsconfig.json` files is not modeled yet. Resolution runs against the root project.
215
237
  - **Scanner fallback on plain-JS or TS 7.x-native projects.** The accurate resolver needs the classic TypeScript compiler API. Without it you get the labeled, relative-imports-only graph described above, and no dependency is added to paper over the gap.
216
- - **No cryptographic signing.** Evidence carries an integrity digest, not a signature. Keyless signing is planned.
238
+ - **No keyless or identity-bound signing.** Evidence can be signed with a local Ed25519 key (see Signing), but sigstore-style keyless signing that ties a signature to an identity is not built yet.
239
+
240
+ ## Publish to a pull request
241
+
242
+ In CI, surface the verdict where reviewers look. Opt in with `--github`; VerisKit
243
+ reads `GITHUB_TOKEN` from the environment and never stores it.
244
+
245
+ ```bash
246
+ veris verify --github # posts/updates a sticky PR comment + a Check Run
247
+ ```
248
+
249
+ It edits one comment on re-runs (no spam) and creates a Check Run whose
250
+ conclusion follows the verdict (verified passes, failed fails, partial is
251
+ neutral). Publishing is a side channel: if there is no token or PR, VerisKit
252
+ prints a notice and the exit code still reflects the verdict, and a GitHub API
253
+ error is reported but never changes the result.
254
+
255
+ The workflow needs permission to write them:
256
+
257
+ ```yaml
258
+ permissions:
259
+ contents: read
260
+ pull-requests: write
261
+ checks: write
262
+ ```
263
+
264
+ For a README badge, write a shields.io endpoint file:
265
+
266
+ ```bash
267
+ veris badge # writes .veris/badge.json
268
+ ```
269
+
270
+ ```markdown
271
+ ![VerisKit](https://img.shields.io/endpoint?url=<raw-url-to>/.veris/badge.json)
272
+ ```
273
+
274
+ ## Browser tests
275
+
276
+ VerisKit can run your Playwright suite as part of the verdict. It is opt-in, so
277
+ a normal `veris verify` stays fast:
278
+
279
+ ```bash
280
+ veris verify --browser # also runs `playwright test`, folded into the verdict
281
+ ```
282
+
283
+ When Playwright is detected, `veris doctor` lists `browser` as available. You can
284
+ also add `browser` to the `checks` array in `.veris/config.json` to run it every
285
+ time.
286
+
287
+ ## History
288
+
289
+ Every run leaves an evidence record, so VerisKit can show you a trend:
290
+
291
+ ```bash
292
+ veris log # past runs, newest first
293
+ veris log --flaky # checks that both passed and failed across recent runs
294
+ ```
295
+
296
+ History is local to your machine (the `.veris/runs` directory is gitignored).
217
297
 
218
298
  ## Part of Baseframe Labs
219
299
 
package/dist/index.js CHANGED
@@ -105,13 +105,9 @@ function detectLint(root, has) {
105
105
  return { id: "lint", available: false, reason: "no linter configured" };
106
106
  }
107
107
  function detectBrowser(root, has) {
108
- if (has("@playwright/test") || existsSync2(join(root, "playwright.config.ts")))
109
- return {
110
- id: "browser",
111
- available: false,
112
- runner: "playwright",
113
- reason: "detected; browser execution deferred to v0.5"
114
- };
108
+ if (has("@playwright/test") || existsSync2(join(root, "playwright.config.ts")) || existsSync2(join(root, "playwright.config.js"))) {
109
+ return { id: "browser", available: true, runner: "playwright" };
110
+ }
115
111
  return {
116
112
  id: "browser",
117
113
  available: false,
@@ -548,6 +544,76 @@ var init_node_test = __esm({
548
544
  }
549
545
  });
550
546
 
547
+ // src/runners/playwright.ts
548
+ function parsePlaywrightStats(stdout) {
549
+ try {
550
+ const json = JSON.parse(stdout);
551
+ return json.stats ?? null;
552
+ } catch {
553
+ return null;
554
+ }
555
+ }
556
+ var TAIL_LINES2, playwrightRunner;
557
+ var init_playwright = __esm({
558
+ "src/runners/playwright.ts"() {
559
+ "use strict";
560
+ init_store();
561
+ init_exec();
562
+ init_base();
563
+ TAIL_LINES2 = 20;
564
+ playwrightRunner = {
565
+ id: "playwright",
566
+ toCheck(project, _cap) {
567
+ return {
568
+ id: "browser",
569
+ title: "Browser tests",
570
+ runner: "playwright",
571
+ cmd: localBin(project.root, "playwright"),
572
+ args: ["test", "--reporter=json"]
573
+ };
574
+ },
575
+ async run(check, ctx) {
576
+ const r = await exec(check.cmd, check.args, {
577
+ cwd: ctx.root,
578
+ timeoutMs: 15 * 6e4
579
+ });
580
+ const output = [r.stdout, r.stderr].filter(Boolean).join("\n").trim();
581
+ const logRef = await writeLog(ctx.runDir, check.id, `${output}
582
+ `);
583
+ const stats = parsePlaywrightStats(r.stdout);
584
+ let status;
585
+ if (r.timedOut) {
586
+ status = "unknown";
587
+ } else if (stats) {
588
+ status = r.code === 0 && (stats.unexpected ?? 0) === 0 ? "passed" : "failed";
589
+ } else {
590
+ status = r.code === 0 ? "unknown" : "failed";
591
+ }
592
+ const result = {
593
+ checkId: "browser",
594
+ status,
595
+ durationMs: r.durationMs,
596
+ summary: status === "passed" ? "browser tests passed" : r.timedOut ? "timed out" : status === "unknown" ? "browser tests ran but the results could not be parsed" : "browser tests failed",
597
+ logRef
598
+ };
599
+ if (stats) {
600
+ const passed = stats.expected ?? 0;
601
+ const failed = stats.unexpected ?? 0;
602
+ result.counts = {
603
+ passed,
604
+ failed,
605
+ total: passed + failed + (stats.flaky ?? 0) + (stats.skipped ?? 0)
606
+ };
607
+ }
608
+ if (status !== "passed" && output) {
609
+ result.outputTail = output.split("\n").slice(-TAIL_LINES2).join("\n");
610
+ }
611
+ return result;
612
+ }
613
+ };
614
+ }
615
+ });
616
+
551
617
  // src/runners/tsc.ts
552
618
  var tscRunner;
553
619
  var init_tsc = __esm({
@@ -614,6 +680,7 @@ var init_runners = __esm({
614
680
  init_eslint();
615
681
  init_jest();
616
682
  init_node_test();
683
+ init_playwright();
617
684
  init_tsc();
618
685
  init_vitest();
619
686
  init_base();
@@ -623,7 +690,8 @@ var init_runners = __esm({
623
690
  jest: jestRunner,
624
691
  "node-test": nodeTestRunner,
625
692
  eslint: eslintRunner,
626
- biome: biomeRunner
693
+ biome: biomeRunner,
694
+ playwright: playwrightRunner
627
695
  };
628
696
  }
629
697
  });
@@ -849,6 +917,7 @@ var init_init = __esm({
849
917
  "cache/",
850
918
  "graph.json",
851
919
  "evidence/",
920
+ "keys/",
852
921
  ""
853
922
  ].join("\n");
854
923
  }
@@ -996,15 +1065,205 @@ var init_markdown = __esm({
996
1065
  }
997
1066
  });
998
1067
 
1068
+ // src/publish/comment.ts
1069
+ function renderComment(run, record) {
1070
+ const summary = run.results.filter((r) => r.status !== "skipped").map((r) => `${r.checkId} ${r.status}`).join(" \xB7 ");
1071
+ return [
1072
+ MARKER,
1073
+ `**VerisKit: ${STATE_LABEL2[run.verdict.state]}**`,
1074
+ "",
1075
+ summary,
1076
+ "",
1077
+ "<details><summary>Report</summary>",
1078
+ "",
1079
+ renderMarkdown(run, record),
1080
+ "",
1081
+ "</details>",
1082
+ ""
1083
+ ].join("\n");
1084
+ }
1085
+ var MARKER, STATE_LABEL2;
1086
+ var init_comment = __esm({
1087
+ "src/publish/comment.ts"() {
1088
+ "use strict";
1089
+ init_markdown();
1090
+ MARKER = "<!-- veriskit-report -->";
1091
+ STATE_LABEL2 = {
1092
+ verified: "verified",
1093
+ failed: "failed",
1094
+ partial: "partial"
1095
+ };
1096
+ }
1097
+ });
1098
+
1099
+ // src/publish/context.ts
1100
+ import { readFileSync as readFileSync2 } from "fs";
1101
+ function defaultReadEvent(path) {
1102
+ try {
1103
+ return JSON.parse(readFileSync2(path, "utf8"));
1104
+ } catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+ function resolvePublishContext(env = process.env, readEvent = defaultReadEvent) {
1109
+ const repository = env.GITHUB_REPOSITORY;
1110
+ const token = env.GITHUB_TOKEN;
1111
+ if (!repository || !token) return null;
1112
+ const [owner, repo] = repository.split("/");
1113
+ if (!owner || !repo) return null;
1114
+ let prNumber = null;
1115
+ let headSha = env.GITHUB_SHA ?? "";
1116
+ const refMatch = (env.GITHUB_REF ?? "").match(/^refs\/pull\/(\d+)\//);
1117
+ if (refMatch) prNumber = Number(refMatch[1]);
1118
+ const event = readEvent(env.GITHUB_EVENT_PATH ?? "");
1119
+ const pr = event?.pull_request;
1120
+ if (pr) {
1121
+ if (prNumber === null && typeof pr.number === "number") {
1122
+ prNumber = pr.number;
1123
+ }
1124
+ if (pr.head?.sha) headSha = pr.head.sha;
1125
+ }
1126
+ if (prNumber === null || !headSha) return null;
1127
+ return { owner, repo, token, prNumber, headSha };
1128
+ }
1129
+ var init_context = __esm({
1130
+ "src/publish/context.ts"() {
1131
+ "use strict";
1132
+ }
1133
+ });
1134
+
1135
+ // src/publish/github.ts
1136
+ function headers(token) {
1137
+ return {
1138
+ Authorization: `Bearer ${token}`,
1139
+ Accept: "application/vnd.github+json",
1140
+ "X-GitHub-Api-Version": "2022-11-28",
1141
+ "User-Agent": "veriskit",
1142
+ "Content-Type": "application/json"
1143
+ };
1144
+ }
1145
+ async function gh(method, url, token, body) {
1146
+ const res = await fetch(url, {
1147
+ method,
1148
+ headers: headers(token),
1149
+ body: body === void 0 ? void 0 : JSON.stringify(body)
1150
+ });
1151
+ if (!res.ok) {
1152
+ const path = url.replace("https://api.github.com", "");
1153
+ throw new GitHubApiError(
1154
+ res.status,
1155
+ `GitHub API ${method} ${path} -> ${res.status}`
1156
+ );
1157
+ }
1158
+ return res.json();
1159
+ }
1160
+ function repoBase(ctx) {
1161
+ return `https://api.github.com/repos/${ctx.owner}/${ctx.repo}`;
1162
+ }
1163
+ async function upsertComment(ctx, body) {
1164
+ const base = repoBase(ctx);
1165
+ const comments = await gh(
1166
+ "GET",
1167
+ `${base}/issues/${ctx.prNumber}/comments?per_page=100`,
1168
+ ctx.token
1169
+ );
1170
+ const existing = comments.find((c) => (c.body ?? "").includes(MARKER));
1171
+ if (existing) {
1172
+ await gh("PATCH", `${base}/issues/comments/${existing.id}`, ctx.token, {
1173
+ body
1174
+ });
1175
+ } else {
1176
+ await gh("POST", `${base}/issues/${ctx.prNumber}/comments`, ctx.token, {
1177
+ body
1178
+ });
1179
+ }
1180
+ }
1181
+ async function createCheckRun(ctx, opts) {
1182
+ await gh("POST", `${repoBase(ctx)}/check-runs`, ctx.token, {
1183
+ name: opts.name,
1184
+ head_sha: ctx.headSha,
1185
+ status: "completed",
1186
+ conclusion: opts.conclusion,
1187
+ output: { title: opts.title, summary: opts.summary }
1188
+ });
1189
+ }
1190
+ var GitHubApiError;
1191
+ var init_github = __esm({
1192
+ "src/publish/github.ts"() {
1193
+ "use strict";
1194
+ init_comment();
1195
+ GitHubApiError = class extends Error {
1196
+ status;
1197
+ constructor(status, message) {
1198
+ super(message);
1199
+ this.name = "GitHubApiError";
1200
+ this.status = status;
1201
+ }
1202
+ };
1203
+ }
1204
+ });
1205
+
1206
+ // src/publish/publish.ts
1207
+ async function publishToGitHub(run, record) {
1208
+ const ctx = resolvePublishContext();
1209
+ if (!ctx) {
1210
+ process.stdout.write(
1211
+ "veris: --github set but no GitHub PR context found (need GITHUB_TOKEN + a pull_request event); skipping publish.\n"
1212
+ );
1213
+ return;
1214
+ }
1215
+ try {
1216
+ await upsertComment(ctx, renderComment(run, record));
1217
+ await createCheckRun(ctx, {
1218
+ name: "VerisKit",
1219
+ conclusion: CONCLUSION[run.verdict.state],
1220
+ title: `VerisKit: ${run.verdict.state}`,
1221
+ summary: renderMarkdown(run, record)
1222
+ });
1223
+ process.stdout.write(
1224
+ `veris: published the verdict to PR #${ctx.prNumber}
1225
+ `
1226
+ );
1227
+ } catch (err) {
1228
+ const msg = err instanceof Error ? err.message : String(err);
1229
+ process.stderr.write(`veris: GitHub publish failed: ${msg}
1230
+ `);
1231
+ }
1232
+ }
1233
+ var CONCLUSION;
1234
+ var init_publish = __esm({
1235
+ "src/publish/publish.ts"() {
1236
+ "use strict";
1237
+ init_markdown();
1238
+ init_comment();
1239
+ init_context();
1240
+ init_github();
1241
+ CONCLUSION = {
1242
+ verified: "success",
1243
+ failed: "failure",
1244
+ partial: "neutral"
1245
+ };
1246
+ }
1247
+ });
1248
+
999
1249
  // src/cli/commands/verify.ts
1000
1250
  var verify_exports = {};
1001
1251
  __export(verify_exports, {
1252
+ resolveChecks: () => resolveChecks,
1002
1253
  runVerify: () => runVerify
1003
1254
  });
1255
+ function resolveChecks(configChecks, project, opts) {
1256
+ let checks = configChecks?.length ? configChecks : DEFAULT_CHECKS;
1257
+ if (opts.browser && !checks.includes("browser")) {
1258
+ const cap = project.capabilities.find((c) => c.id === "browser");
1259
+ if (cap?.available) checks = [...checks, "browser"];
1260
+ }
1261
+ return checks;
1262
+ }
1004
1263
  async function runVerify(root, opts = {}) {
1005
1264
  const project = await detectProject(root);
1006
1265
  const config = await loadConfig(root);
1007
- const checks = config?.checks?.length ? config.checks : DEFAULT_CHECKS;
1266
+ const checks = resolveChecks(config?.checks, project, opts);
1008
1267
  const run = await runChecks(project, checks, root);
1009
1268
  const git = await gitAnchor(root);
1010
1269
  const logDigests = await digestLogs(run);
@@ -1019,6 +1278,7 @@ async function runVerify(root, opts = {}) {
1019
1278
  await writeEvidence(runDir, record);
1020
1279
  process.stdout.write(`${renderRun(run, record)}
1021
1280
  `);
1281
+ if (opts.github) await publishToGitHub(run, record);
1022
1282
  return verdictExitCode(run.verdict, opts);
1023
1283
  }
1024
1284
  var DEFAULT_CHECKS;
@@ -1032,6 +1292,7 @@ var init_verify = __esm({
1032
1292
  init_record();
1033
1293
  init_store();
1034
1294
  init_changes();
1295
+ init_publish();
1035
1296
  init_markdown();
1036
1297
  init_terminal();
1037
1298
  init_version();
@@ -1044,7 +1305,7 @@ var report_exports = {};
1044
1305
  __export(report_exports, {
1045
1306
  runReport: () => runReport
1046
1307
  });
1047
- import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1308
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
1048
1309
  import { join as join6 } from "path";
1049
1310
  async function runReport(root) {
1050
1311
  const dir = join6(root, ".veris", "reports");
@@ -1060,7 +1321,7 @@ async function runReport(root) {
1060
1321
  process.stdout.write("No reports yet. Run `veris verify` first.\n");
1061
1322
  return 0;
1062
1323
  }
1063
- process.stdout.write(`${readFileSync2(join6(dir, latest), "utf8")}
1324
+ process.stdout.write(`${readFileSync3(join6(dir, latest), "utf8")}
1064
1325
  `);
1065
1326
  return 0;
1066
1327
  }
@@ -1175,7 +1436,7 @@ var init_discover = __esm({
1175
1436
  });
1176
1437
 
1177
1438
  // src/project-graph/scanner-resolver.ts
1178
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
1439
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
1179
1440
  import { dirname, join as join8, relative as relative3, resolve } from "path";
1180
1441
  function extractSpecifiers(text) {
1181
1442
  const specs = [];
@@ -1214,7 +1475,7 @@ function resolveRelative(fromDir, spec) {
1214
1475
  function scannerImports(root, file) {
1215
1476
  let text;
1216
1477
  try {
1217
- text = readFileSync3(join8(root, file), "utf8");
1478
+ text = readFileSync4(join8(root, file), "utf8");
1218
1479
  } catch {
1219
1480
  return [];
1220
1481
  }
@@ -1238,7 +1499,7 @@ var init_scanner_resolver = __esm({
1238
1499
  });
1239
1500
 
1240
1501
  // src/project-graph/ts-resolver.ts
1241
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1502
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
1242
1503
  import { createRequire } from "module";
1243
1504
  import { join as join9, relative as relative4, sep as sep2 } from "path";
1244
1505
  function hasClassicApi(mod) {
@@ -1265,7 +1526,7 @@ function loadCompilerOptions(tsmod, root) {
1265
1526
  function tsImports(tsmod, root, options, file) {
1266
1527
  let text;
1267
1528
  try {
1268
- text = readFileSync4(join9(root, file), "utf8");
1529
+ text = readFileSync5(join9(root, file), "utf8");
1269
1530
  } catch {
1270
1531
  return [];
1271
1532
  }
@@ -1517,6 +1778,7 @@ async function runAffected(root, opts = {}) {
1517
1778
  `);
1518
1779
  if (narrowedNote) process.stdout.write(`${narrowedNote}
1519
1780
  `);
1781
+ if (opts.github) await publishToGitHub(run, record);
1520
1782
  return verdictExitCode(run.verdict, opts);
1521
1783
  }
1522
1784
  var init_affected = __esm({
@@ -1529,6 +1791,7 @@ var init_affected = __esm({
1529
1791
  init_record();
1530
1792
  init_store();
1531
1793
  init_changes();
1794
+ init_publish();
1532
1795
  init_markdown();
1533
1796
  init_terminal();
1534
1797
  init_version();
@@ -1897,7 +2160,84 @@ var init_plan = __esm({
1897
2160
  }
1898
2161
  });
1899
2162
 
2163
+ // src/evidence/signing.ts
2164
+ import {
2165
+ createHash as createHash2,
2166
+ createPrivateKey,
2167
+ createPublicKey,
2168
+ sign as cryptoSign,
2169
+ verify as cryptoVerify,
2170
+ generateKeyPairSync
2171
+ } from "crypto";
2172
+ function derOf(publicKey) {
2173
+ return publicKey.export({ type: "spki", format: "der" });
2174
+ }
2175
+ function keyId(publicKey) {
2176
+ const key = typeof publicKey === "string" ? createPublicKey(publicKey) : publicKey;
2177
+ return createHash2("sha256").update(derOf(key)).digest("hex").slice(0, 8);
2178
+ }
2179
+ function signatureKeyId(sig) {
2180
+ const key = createPublicKey({
2181
+ key: Buffer.from(sig.publicKey, "base64"),
2182
+ format: "der",
2183
+ type: "spki"
2184
+ });
2185
+ return keyId(key);
2186
+ }
2187
+ function generateKeyPair() {
2188
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
2189
+ return {
2190
+ publicKeyPem: publicKey.export({ type: "spki", format: "pem" }),
2191
+ privateKeyPem: privateKey.export({
2192
+ type: "pkcs8",
2193
+ format: "pem"
2194
+ }),
2195
+ keyId: keyId(publicKey)
2196
+ };
2197
+ }
2198
+ function signDigest(digest, privateKeyPem) {
2199
+ const privateKey = createPrivateKey(privateKeyPem);
2200
+ const publicKey = createPublicKey(
2201
+ privateKey
2202
+ );
2203
+ const sig = cryptoSign(null, Buffer.from(digest, "utf8"), privateKey);
2204
+ return {
2205
+ schema: SIGNATURE_SCHEMA,
2206
+ alg: "ed25519",
2207
+ keyId: keyId(publicKey),
2208
+ publicKey: derOf(publicKey).toString("base64"),
2209
+ digest,
2210
+ signature: sig.toString("base64"),
2211
+ signedAt: (/* @__PURE__ */ new Date()).toISOString()
2212
+ };
2213
+ }
2214
+ function verifySignature(sig) {
2215
+ try {
2216
+ const publicKey = createPublicKey({
2217
+ key: Buffer.from(sig.publicKey, "base64"),
2218
+ format: "der",
2219
+ type: "spki"
2220
+ });
2221
+ return cryptoVerify(
2222
+ null,
2223
+ Buffer.from(sig.digest, "utf8"),
2224
+ publicKey,
2225
+ Buffer.from(sig.signature, "base64")
2226
+ );
2227
+ } catch {
2228
+ return false;
2229
+ }
2230
+ }
2231
+ var SIGNATURE_SCHEMA;
2232
+ var init_signing = __esm({
2233
+ "src/evidence/signing.ts"() {
2234
+ "use strict";
2235
+ SIGNATURE_SCHEMA = "veriskit/signature@1";
2236
+ }
2237
+ });
2238
+
1900
2239
  // src/evidence/verify-evidence.ts
2240
+ import { existsSync as existsSync5 } from "fs";
1901
2241
  import { readFile as readFile3 } from "fs/promises";
1902
2242
  function verifyRecord(record) {
1903
2243
  const recomputed = computeDigest(
@@ -1911,20 +2251,71 @@ function verifyRecord(record) {
1911
2251
  detail: ok ? "matches the canonical record" : `recomputed ${recomputed}, record claims ${record.digest}`
1912
2252
  }
1913
2253
  ];
1914
- return { ok, kind: "record", record, checks };
2254
+ return { ok, kind: "record", record, checks, signed: false };
2255
+ }
2256
+ function signatureChecks(recordDigest, sig, opts = {}) {
2257
+ const checks = [];
2258
+ const kid = signatureKeyId(sig);
2259
+ const validSig = verifySignature(sig) && sig.digest === recordDigest;
2260
+ checks.push({
2261
+ name: "signature",
2262
+ ok: validSig,
2263
+ detail: validSig ? `valid ed25519 signature by key ${kid}` : "signature does not match this record"
2264
+ });
2265
+ if (opts.expectedKeyId || opts.expectedPubKeyPem) {
2266
+ let matches = false;
2267
+ if (opts.expectedPubKeyPem) {
2268
+ try {
2269
+ matches = keyId(opts.expectedPubKeyPem) === kid;
2270
+ } catch {
2271
+ matches = false;
2272
+ }
2273
+ } else if (opts.expectedKeyId) {
2274
+ matches = opts.expectedKeyId.toLowerCase() === kid.toLowerCase();
2275
+ }
2276
+ checks.push({
2277
+ name: "signer",
2278
+ ok: matches,
2279
+ detail: matches ? `signer matches the expected key ${kid}` : `signer ${kid} does not match the expected key`
2280
+ });
2281
+ }
2282
+ return checks;
1915
2283
  }
1916
- async function verifyEvidenceFile(path) {
2284
+ async function verifyEvidenceFile(path, opts = {}) {
1917
2285
  const parsed = JSON.parse(await readFile3(path, "utf8"));
1918
2286
  if (parsed?.schema === "veriskit/bundle@1") {
1919
2287
  const { verifyBundle: verifyBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
1920
- return verifyBundle2(parsed);
2288
+ return verifyBundle2(parsed, opts);
1921
2289
  }
1922
- return verifyRecord(parsed);
2290
+ const result = verifyRecord(parsed);
2291
+ const assertedSigner = Boolean(
2292
+ opts.expectedKeyId || opts.expectedPubKeyPem || opts.sigPath
2293
+ );
2294
+ const sigPath = opts.sigPath ?? `${path}.sig`;
2295
+ if (existsSync5(sigPath)) {
2296
+ const sig = JSON.parse(await readFile3(sigPath, "utf8"));
2297
+ result.checks.push(
2298
+ ...signatureChecks(result.record.digest, sig, {
2299
+ expectedKeyId: opts.expectedKeyId,
2300
+ expectedPubKeyPem: opts.expectedPubKeyPem
2301
+ })
2302
+ );
2303
+ result.signed = true;
2304
+ } else if (assertedSigner) {
2305
+ result.checks.push({
2306
+ name: "signature",
2307
+ ok: false,
2308
+ detail: "a signature was required (--pubkey/--key-id/--sig) but none was found"
2309
+ });
2310
+ }
2311
+ result.ok = result.checks.every((c) => c.ok);
2312
+ return result;
1923
2313
  }
1924
2314
  var init_verify_evidence = __esm({
1925
2315
  "src/evidence/verify-evidence.ts"() {
1926
2316
  "use strict";
1927
2317
  init_record();
2318
+ init_signing();
1928
2319
  }
1929
2320
  });
1930
2321
 
@@ -1935,7 +2326,7 @@ __export(bundle_exports, {
1935
2326
  buildBundle: () => buildBundle,
1936
2327
  verifyBundle: () => verifyBundle
1937
2328
  });
1938
- function buildBundle(record, report, logs) {
2329
+ function buildBundle(record, report, logs, signature) {
1939
2330
  const manifest = {
1940
2331
  record: record.digest,
1941
2332
  report: sha256(report),
@@ -1943,10 +2334,10 @@ function buildBundle(record, report, logs) {
1943
2334
  Object.entries(logs).map(([k, v]) => [k, sha256(v)])
1944
2335
  )
1945
2336
  };
1946
- const base = { schema: BUNDLE_SCHEMA, record, report, logs, manifest };
2337
+ const base = signature ? { schema: BUNDLE_SCHEMA, record, report, logs, manifest, signature } : { schema: BUNDLE_SCHEMA, record, report, logs, manifest };
1947
2338
  return { ...base, bundleDigest: sha256(canonicalize(base)) };
1948
2339
  }
1949
- function verifyBundle(bundle) {
2340
+ function verifyBundle(bundle, opts = {}) {
1950
2341
  const checks = [];
1951
2342
  const recordResult = verifyRecord(bundle.record);
1952
2343
  checks.push(...recordResult.checks);
@@ -1971,11 +2362,28 @@ function verifyBundle(bundle) {
1971
2362
  ok: recomputed === bundle.bundleDigest,
1972
2363
  detail: recomputed === bundle.bundleDigest ? "matches" : "mismatch"
1973
2364
  });
2365
+ let signed = false;
2366
+ if (bundle.signature) {
2367
+ signed = true;
2368
+ checks.push(
2369
+ ...signatureChecks(bundle.record.digest, bundle.signature, {
2370
+ expectedKeyId: opts.expectedKeyId,
2371
+ expectedPubKeyPem: opts.expectedPubKeyPem
2372
+ })
2373
+ );
2374
+ } else if (opts.expectedKeyId || opts.expectedPubKeyPem) {
2375
+ checks.push({
2376
+ name: "signature",
2377
+ ok: false,
2378
+ detail: "a signature was required (--pubkey/--key-id) but the bundle is unsigned"
2379
+ });
2380
+ }
1974
2381
  return {
1975
2382
  ok: checks.every((c) => c.ok),
1976
2383
  kind: "bundle",
1977
2384
  record: bundle.record,
1978
- checks
2385
+ checks,
2386
+ signed
1979
2387
  };
1980
2388
  }
1981
2389
  var BUNDLE_SCHEMA;
@@ -1992,17 +2400,33 @@ var init_bundle = __esm({
1992
2400
  var evidence_exports = {};
1993
2401
  __export(evidence_exports, {
1994
2402
  runEvidenceBundle: () => runEvidenceBundle,
2403
+ runEvidenceKeygen: () => runEvidenceKeygen,
1995
2404
  runEvidenceShow: () => runEvidenceShow,
2405
+ runEvidenceSign: () => runEvidenceSign,
1996
2406
  runEvidenceVerify: () => runEvidenceVerify
1997
2407
  });
1998
- import { readFileSync as readFileSync5 } from "fs";
2408
+ import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync } from "fs";
1999
2409
  import { writeFile as writeFile4 } from "fs/promises";
2000
- import { basename as basename3, join as join12 } from "path";
2410
+ import { basename as basename3, dirname as dirname2, join as join12 } from "path";
2001
2411
  import pc5 from "picocolors";
2002
- async function runEvidenceVerify(path) {
2412
+ async function runEvidenceVerify(path, opts = {}) {
2413
+ let expectedPubKeyPem;
2414
+ if (opts.pubkey) {
2415
+ try {
2416
+ expectedPubKeyPem = readFileSync6(opts.pubkey, "utf8");
2417
+ } catch {
2418
+ process.stderr.write(`veris: cannot read public key at ${opts.pubkey}
2419
+ `);
2420
+ return 1;
2421
+ }
2422
+ }
2003
2423
  let result;
2004
2424
  try {
2005
- result = await verifyEvidenceFile(path);
2425
+ result = await verifyEvidenceFile(path, {
2426
+ sigPath: opts.sig,
2427
+ expectedKeyId: opts.keyId,
2428
+ expectedPubKeyPem
2429
+ });
2006
2430
  } catch (err) {
2007
2431
  const msg = err instanceof Error ? err.message : String(err);
2008
2432
  process.stderr.write(`veris: cannot read evidence at ${path}: ${msg}
@@ -2027,8 +2451,93 @@ ${result.ok ? "digest OK" : "TAMPERED"} \xB7 verdict ${result.record.verdict.sta
2027
2451
  process.stdout.write(`
2028
2452
  ${HONESTY}
2029
2453
  `);
2454
+ if (result.signed && !opts.pubkey && !opts.keyId) {
2455
+ process.stdout.write(
2456
+ "\nThis record is signed. Confirm you trust the signing key by comparing its\nkey id above to one you already trust, or re-run with --pubkey / --key-id.\n"
2457
+ );
2458
+ }
2030
2459
  return result.ok ? 0 : 1;
2031
2460
  }
2461
+ async function runEvidenceKeygen(root, opts = {}) {
2462
+ const out = opts.out ?? join12(root, ".veris", "keys", "veriskit-signing.key");
2463
+ const pub = `${out}.pub`;
2464
+ if (existsSync6(out) || existsSync6(pub)) {
2465
+ process.stderr.write(
2466
+ `veris: a key already exists at ${out}. Refusing to overwrite it.
2467
+ `
2468
+ );
2469
+ return 1;
2470
+ }
2471
+ const { mkdir: mkdir3 } = await import("fs/promises");
2472
+ await mkdir3(dirname2(out), { recursive: true });
2473
+ const kp = generateKeyPair();
2474
+ writeFileSync(out, kp.privateKeyPem, { mode: 384 });
2475
+ chmodSync(out, 384);
2476
+ writeFileSync(pub, kp.publicKeyPem, "utf8");
2477
+ process.stdout.write(
2478
+ [
2479
+ `Wrote signing key ${out} (keep this secret; do not commit it)`,
2480
+ `Wrote public key ${pub}`,
2481
+ `Key id ${kp.keyId}`,
2482
+ ""
2483
+ ].join("\n")
2484
+ );
2485
+ return 0;
2486
+ }
2487
+ async function runEvidenceSign(evidencePath, opts = {}) {
2488
+ let privateKeyPem;
2489
+ if (process.env.VERISKIT_SIGNING_KEY) {
2490
+ privateKeyPem = process.env.VERISKIT_SIGNING_KEY;
2491
+ } else if (opts.key) {
2492
+ try {
2493
+ privateKeyPem = readFileSync6(opts.key, "utf8");
2494
+ } catch {
2495
+ process.stderr.write(`veris: cannot read signing key at ${opts.key}
2496
+ `);
2497
+ return 1;
2498
+ }
2499
+ } else {
2500
+ process.stderr.write(
2501
+ "veris: no signing key. Pass --key <path> or set VERISKIT_SIGNING_KEY.\n"
2502
+ );
2503
+ return 1;
2504
+ }
2505
+ let record;
2506
+ try {
2507
+ record = JSON.parse(readFileSync6(evidencePath, "utf8"));
2508
+ } catch {
2509
+ process.stderr.write(`veris: cannot read evidence at ${evidencePath}
2510
+ `);
2511
+ return 1;
2512
+ }
2513
+ const recomputed = computeDigest(
2514
+ record
2515
+ );
2516
+ if (recomputed !== record.digest) {
2517
+ process.stderr.write(
2518
+ "veris: refusing to sign, the record digest does not match its contents.\n"
2519
+ );
2520
+ return 1;
2521
+ }
2522
+ let sig;
2523
+ try {
2524
+ sig = signDigest(record.digest, privateKeyPem);
2525
+ } catch (err) {
2526
+ const msg = err instanceof Error ? err.message : String(err);
2527
+ process.stderr.write(`veris: signing failed: ${msg}
2528
+ `);
2529
+ return 1;
2530
+ }
2531
+ const out = opts.out ?? `${evidencePath}.sig`;
2532
+ writeFileSync(out, `${JSON.stringify(sig, null, 2)}
2533
+ `, "utf8");
2534
+ process.stdout.write(
2535
+ `Signed ${evidencePath}
2536
+ Wrote ${out} (key ${sig.keyId})
2537
+ `
2538
+ );
2539
+ return 0;
2540
+ }
2032
2541
  async function runEvidenceBundle(root, opts = {}) {
2033
2542
  const runDir = latestRunDir(root);
2034
2543
  if (!runDir) {
@@ -2038,7 +2547,7 @@ async function runEvidenceBundle(root, opts = {}) {
2038
2547
  let record;
2039
2548
  try {
2040
2549
  record = JSON.parse(
2041
- readFileSync5(join12(runDir, "evidence.json"), "utf8")
2550
+ readFileSync6(join12(runDir, "evidence.json"), "utf8")
2042
2551
  );
2043
2552
  } catch {
2044
2553
  process.stderr.write(`veris: no evidence.json in ${runDir}
@@ -2047,7 +2556,7 @@ async function runEvidenceBundle(root, opts = {}) {
2047
2556
  }
2048
2557
  let report = "";
2049
2558
  try {
2050
- report = readFileSync5(
2559
+ report = readFileSync6(
2051
2560
  join12(root, ".veris", "reports", `verify-${record.id}.md`),
2052
2561
  "utf8"
2053
2562
  );
@@ -2055,7 +2564,15 @@ async function runEvidenceBundle(root, opts = {}) {
2055
2564
  }
2056
2565
  const logIds = record.checks.filter((c) => c.logDigest).map((c) => c.id);
2057
2566
  const logs = await readRunLogs(runDir, logIds);
2058
- const bundle = buildBundle(record, report, logs);
2567
+ let signature;
2568
+ const sigPath = join12(runDir, "evidence.json.sig");
2569
+ if (existsSync6(sigPath)) {
2570
+ try {
2571
+ signature = JSON.parse(readFileSync6(sigPath, "utf8"));
2572
+ } catch {
2573
+ }
2574
+ }
2575
+ const bundle = buildBundle(record, report, logs, signature);
2059
2576
  const outDir = await ensureEvidenceDir(root);
2060
2577
  const out = opts.out ?? join12(outDir, `${record.id}.bundle.json`);
2061
2578
  await writeFile4(out, `${JSON.stringify(bundle, null, 2)}
@@ -2075,7 +2592,7 @@ async function runEvidenceShow(root, path) {
2075
2592
  }
2076
2593
  let record;
2077
2594
  try {
2078
- record = JSON.parse(readFileSync5(target, "utf8"));
2595
+ record = JSON.parse(readFileSync6(target, "utf8"));
2079
2596
  } catch {
2080
2597
  process.stderr.write(`veris: cannot read evidence at ${target}
2081
2598
  `);
@@ -2101,10 +2618,187 @@ var init_evidence = __esm({
2101
2618
  "src/cli/commands/evidence.ts"() {
2102
2619
  "use strict";
2103
2620
  init_bundle();
2621
+ init_record();
2622
+ init_signing();
2104
2623
  init_store();
2105
2624
  init_verify_evidence();
2106
2625
  init_tty();
2107
- HONESTY = "An integrity digest confirms the record was not edited or corrupted since it was written.\nIt is not forgery-proof on its own: publish the digest separately (CI log, PR) or sign it (planned) to prove authorship.";
2626
+ HONESTY = "An integrity digest confirms the record was not edited or corrupted since it was written.\nIt is not forgery-proof on its own: publish the digest separately (CI log, PR) or sign it (veris evidence sign) to prove authorship.";
2627
+ }
2628
+ });
2629
+
2630
+ // src/publish/badge.ts
2631
+ function buildBadge(verdict) {
2632
+ const s = STYLE[verdict.state];
2633
+ return {
2634
+ schemaVersion: 1,
2635
+ label: "veriskit",
2636
+ message: s.message,
2637
+ color: s.color
2638
+ };
2639
+ }
2640
+ var STYLE;
2641
+ var init_badge = __esm({
2642
+ "src/publish/badge.ts"() {
2643
+ "use strict";
2644
+ STYLE = {
2645
+ verified: { message: "verified", color: "14b8a6" },
2646
+ failed: { message: "failed", color: "e5484d" },
2647
+ partial: { message: "partial", color: "f5a623" }
2648
+ };
2649
+ }
2650
+ });
2651
+
2652
+ // src/cli/commands/badge.ts
2653
+ var badge_exports = {};
2654
+ __export(badge_exports, {
2655
+ runBadge: () => runBadge
2656
+ });
2657
+ import { readFileSync as readFileSync7 } from "fs";
2658
+ import { mkdir as mkdir2, writeFile as writeFile5 } from "fs/promises";
2659
+ import { dirname as dirname3, join as join13 } from "path";
2660
+ async function runBadge(root, opts = {}) {
2661
+ const dir = latestRunDir(root);
2662
+ if (!dir) {
2663
+ process.stdout.write("No runs yet. Run `veris verify` first.\n");
2664
+ return 1;
2665
+ }
2666
+ let record;
2667
+ try {
2668
+ record = JSON.parse(
2669
+ readFileSync7(join13(dir, "evidence.json"), "utf8")
2670
+ );
2671
+ } catch {
2672
+ process.stderr.write(`veris: no evidence.json in ${dir}
2673
+ `);
2674
+ return 1;
2675
+ }
2676
+ const out = opts.out ?? join13(root, ".veris", "badge.json");
2677
+ await mkdir2(dirname3(out), { recursive: true });
2678
+ await writeFile5(
2679
+ out,
2680
+ `${JSON.stringify(buildBadge(record.verdict), null, 2)}
2681
+ `,
2682
+ "utf8"
2683
+ );
2684
+ process.stdout.write(`Wrote badge ${out}
2685
+ `);
2686
+ return 0;
2687
+ }
2688
+ var init_badge2 = __esm({
2689
+ "src/cli/commands/badge.ts"() {
2690
+ "use strict";
2691
+ init_store();
2692
+ init_badge();
2693
+ }
2694
+ });
2695
+
2696
+ // src/history/flaky.ts
2697
+ function detectFlaky(records) {
2698
+ const byId = /* @__PURE__ */ new Map();
2699
+ for (const rec of records) {
2700
+ for (const c of rec.checks ?? []) {
2701
+ const arr = byId.get(c.id) ?? [];
2702
+ arr.push(c.status);
2703
+ byId.set(c.id, arr);
2704
+ }
2705
+ }
2706
+ const flaky = [];
2707
+ for (const [id, statuses] of byId) {
2708
+ if (statuses.includes("passed") && statuses.includes("failed")) {
2709
+ flaky.push({ id, statuses });
2710
+ }
2711
+ }
2712
+ return flaky;
2713
+ }
2714
+ var init_flaky = __esm({
2715
+ "src/history/flaky.ts"() {
2716
+ "use strict";
2717
+ }
2718
+ });
2719
+
2720
+ // src/history/read.ts
2721
+ import { readdirSync as readdirSync5, readFileSync as readFileSync8 } from "fs";
2722
+ import { join as join14 } from "path";
2723
+ function loadRuns(root, limit = 20) {
2724
+ const runs = join14(root, ".veris", "runs");
2725
+ let names;
2726
+ try {
2727
+ names = readdirSync5(runs, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "watch").map((d) => d.name);
2728
+ } catch {
2729
+ return [];
2730
+ }
2731
+ const records = [];
2732
+ for (const name of names) {
2733
+ try {
2734
+ const rec = JSON.parse(
2735
+ readFileSync8(join14(runs, name, "evidence.json"), "utf8")
2736
+ );
2737
+ if (rec?.schema === "veriskit/evidence@1") records.push(rec);
2738
+ } catch {
2739
+ }
2740
+ }
2741
+ records.sort(
2742
+ (a, b) => a.startedAt < b.startedAt ? 1 : a.startedAt > b.startedAt ? -1 : 0
2743
+ );
2744
+ return records.slice(0, limit);
2745
+ }
2746
+ var init_read = __esm({
2747
+ "src/history/read.ts"() {
2748
+ "use strict";
2749
+ }
2750
+ });
2751
+
2752
+ // src/cli/commands/log.ts
2753
+ var log_exports = {};
2754
+ __export(log_exports, {
2755
+ runLog: () => runLog
2756
+ });
2757
+ async function runLog(root, opts = {}) {
2758
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? opts.limit : 20;
2759
+ const records = loadRuns(root, limit);
2760
+ if (records.length === 0) {
2761
+ process.stdout.write("No runs yet. Run `veris verify` first.\n");
2762
+ return 0;
2763
+ }
2764
+ if (opts.flaky) {
2765
+ const flaky = detectFlaky(records);
2766
+ if (flaky.length === 0) {
2767
+ process.stdout.write(
2768
+ `No flaky checks in the last ${records.length} run(s).
2769
+ `
2770
+ );
2771
+ return 0;
2772
+ }
2773
+ process.stdout.write(
2774
+ `Flaky checks (both passed and failed in the last ${records.length} run(s), newest first):
2775
+ `
2776
+ );
2777
+ for (const f of flaky) {
2778
+ process.stdout.write(` ${f.id.padEnd(8)} ${f.statuses.join(" ")}
2779
+ `);
2780
+ }
2781
+ return 0;
2782
+ }
2783
+ for (const rec of records) {
2784
+ const date = rec.startedAt.slice(0, 19).replace("T", " ");
2785
+ const checks = (rec.checks ?? []).map((c) => `${c.id}:${c.status}`).join(" ");
2786
+ const commit = rec.git ? rec.git.commit.slice(0, 7) : "-";
2787
+ process.stdout.write(
2788
+ `${date} ${rec.verdict.state.padEnd(9)} ${checks} ${commit}
2789
+ `
2790
+ );
2791
+ }
2792
+ process.stdout.write(
2793
+ "\nHistory is local to this machine (.veris/runs is gitignored).\n"
2794
+ );
2795
+ return 0;
2796
+ }
2797
+ var init_log = __esm({
2798
+ "src/cli/commands/log.ts"() {
2799
+ "use strict";
2800
+ init_flaky();
2801
+ init_read();
2108
2802
  }
2109
2803
  });
2110
2804
 
@@ -2130,23 +2824,36 @@ function buildCli() {
2130
2824
  const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2131
2825
  process.exitCode = await runInit2(process.cwd());
2132
2826
  });
2133
- cli.command("verify", "Run the full check set and produce a verdict + report").option("--partial-ok", "Exit 0 even when the verdict is partial").action(async (opts) => {
2134
- const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
2135
- process.exitCode = await runVerify2(process.cwd(), {
2136
- partialOk: opts.partialOk
2137
- });
2138
- });
2827
+ cli.command("verify", "Run the full check set and produce a verdict + report").option("--partial-ok", "Exit 0 even when the verdict is partial").option(
2828
+ "--github",
2829
+ "Publish the verdict to the GitHub PR (comment + check run)"
2830
+ ).option("--browser", "Also run browser tests (Playwright), when available").action(
2831
+ async (opts) => {
2832
+ const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
2833
+ process.exitCode = await runVerify2(process.cwd(), {
2834
+ partialOk: opts.partialOk,
2835
+ github: opts.github,
2836
+ browser: opts.browser
2837
+ });
2838
+ }
2839
+ );
2139
2840
  cli.command("report", "Print the latest verification report").action(async () => {
2140
2841
  const { runReport: runReport2 } = await Promise.resolve().then(() => (init_report(), report_exports));
2141
2842
  process.exitCode = await runReport2(process.cwd());
2142
2843
  });
2143
- cli.command("affected", "Run only the checks affected by changed files").option("--base <ref>", "Compare against a git ref instead of HEAD").option("--partial-ok", "Exit 0 even when the verdict is partial").action(async (opts) => {
2144
- const { runAffected: runAffected2 } = await Promise.resolve().then(() => (init_affected(), affected_exports));
2145
- process.exitCode = await runAffected2(process.cwd(), {
2146
- base: opts.base,
2147
- partialOk: opts.partialOk
2148
- });
2149
- });
2844
+ cli.command("affected", "Run only the checks affected by changed files").option("--base <ref>", "Compare against a git ref instead of HEAD").option("--partial-ok", "Exit 0 even when the verdict is partial").option(
2845
+ "--github",
2846
+ "Publish the verdict to the GitHub PR (comment + check run)"
2847
+ ).action(
2848
+ async (opts) => {
2849
+ const { runAffected: runAffected2 } = await Promise.resolve().then(() => (init_affected(), affected_exports));
2850
+ process.exitCode = await runAffected2(process.cwd(), {
2851
+ base: opts.base,
2852
+ partialOk: opts.partialOk,
2853
+ github: opts.github
2854
+ });
2855
+ }
2856
+ );
2150
2857
  cli.command("watch", "Re-run affected checks as files change (Ctrl-C to stop)").option("--poll", "Use mtime polling instead of native fs.watch").action(async (opts) => {
2151
2858
  const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
2152
2859
  process.exitCode = await runWatch2(process.cwd(), { poll: opts.poll });
@@ -2164,8 +2871,8 @@ function buildCli() {
2164
2871
  });
2165
2872
  cli.command(
2166
2873
  "evidence <action> [file]",
2167
- "Evidence tools: verify <file> | bundle | show [file]"
2168
- ).option("--out <file>", "For bundle: write to a specific path").action(
2874
+ "Evidence: verify <file> | bundle | show [file] | keygen | sign <file>"
2875
+ ).option("--out <file>", "For bundle/keygen/sign: output path").option("--key <file>", "For sign: the private signing key (PEM)").option("--pubkey <file>", "For verify: assert the signer's public key").option("--key-id <id>", "For verify: assert the signer's key id").option("--sig <file>", "For verify: signature path (default <file>.sig)").action(
2169
2876
  async (action, file, opts) => {
2170
2877
  const mod = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
2171
2878
  if (action === "verify") {
@@ -2174,22 +2881,54 @@ function buildCli() {
2174
2881
  process.exitCode = 1;
2175
2882
  return;
2176
2883
  }
2177
- process.exitCode = await mod.runEvidenceVerify(file);
2884
+ process.exitCode = await mod.runEvidenceVerify(file, {
2885
+ pubkey: opts.pubkey,
2886
+ keyId: opts.keyId,
2887
+ sig: opts.sig
2888
+ });
2178
2889
  } else if (action === "bundle") {
2179
2890
  process.exitCode = await mod.runEvidenceBundle(process.cwd(), {
2180
2891
  out: opts.out
2181
2892
  });
2182
2893
  } else if (action === "show") {
2183
2894
  process.exitCode = await mod.runEvidenceShow(process.cwd(), file);
2895
+ } else if (action === "keygen") {
2896
+ process.exitCode = await mod.runEvidenceKeygen(process.cwd(), {
2897
+ out: opts.out
2898
+ });
2899
+ } else if (action === "sign") {
2900
+ if (!file) {
2901
+ process.stderr.write("veris: evidence sign needs a <file>\n");
2902
+ process.exitCode = 1;
2903
+ return;
2904
+ }
2905
+ process.exitCode = await mod.runEvidenceSign(file, {
2906
+ key: opts.key,
2907
+ out: opts.out
2908
+ });
2184
2909
  } else {
2185
2910
  process.stderr.write(
2186
- `veris: unknown evidence action '${action}' (use verify | bundle | show)
2911
+ `veris: unknown evidence action '${action}' (use verify | bundle | show | keygen | sign)
2187
2912
  `
2188
2913
  );
2189
2914
  process.exitCode = 1;
2190
2915
  }
2191
2916
  }
2192
2917
  );
2918
+ cli.command(
2919
+ "badge",
2920
+ "Write a shields.io endpoint JSON from the latest verdict"
2921
+ ).option("--out <file>", "Output path (default .veris/badge.json)").action(async (opts) => {
2922
+ const { runBadge: runBadge2 } = await Promise.resolve().then(() => (init_badge2(), badge_exports));
2923
+ process.exitCode = await runBadge2(process.cwd(), { out: opts.out });
2924
+ });
2925
+ cli.command("log", "List past verification runs (local history)").option("--limit <n>", "How many runs to show (default 20)").option("--flaky", "Show only checks that flip-flop across runs").action(async (opts) => {
2926
+ const { runLog: runLog2 } = await Promise.resolve().then(() => (init_log(), log_exports));
2927
+ process.exitCode = await runLog2(process.cwd(), {
2928
+ limit: opts.limit ? Number(opts.limit) : void 0,
2929
+ flaky: opts.flaky
2930
+ });
2931
+ });
2193
2932
  return { raw: cli, version: VERSION };
2194
2933
  }
2195
2934
  async function main(argv2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veriskit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The fastest way to prove your software works — a zero-config verification CLI.",
5
5
  "scripts": {
6
6
  "test": "vitest run",