@cejel/cejel 0.1.5 → 0.1.7

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 CHANGED
@@ -31,9 +31,15 @@ No account, no key, no signup.
31
31
  **Single-file binary.** One file. No Node, no npm, no `node_modules`, nothing installed.
32
32
 
33
33
  ```bash
34
+ set -eu
34
35
  asset="cejel-$(uname -s)-$(uname -m)"
35
36
  curl -fsSL "https://github.com/BargLabs/cejel/releases/latest/download/$asset" -o "$asset"
36
37
  curl -fsSL https://github.com/BargLabs/cejel/releases/latest/download/SHA256SUMS -o SHA256SUMS
38
+ checksum_entry_count="$(awk -v asset="$asset" '$2 == asset { count++ } END { print count + 0 }' SHA256SUMS)"
39
+ if [ "$checksum_entry_count" -ne 1 ]; then
40
+ echo "Expected exactly one SHA256SUMS entry for $asset; found $checksum_entry_count" >&2
41
+ exit 1
42
+ fi
37
43
  if command -v sha256sum >/dev/null; then
38
44
  grep " $asset$" SHA256SUMS | sha256sum -c -
39
45
  else
@@ -64,7 +70,7 @@ The npm package is scoped as `@cejel/cejel`; its executable remains the short co
64
70
  **GitHub Action** — score every PR and publish the badge:
65
71
 
66
72
  ```yaml
67
- - uses: BargLabs/cejel/action@v1
73
+ - uses: BargLabs/cejel/action@v0.1.6
68
74
  with:
69
75
  min-score: "2.5" # optional: fail the build below this
70
76
  ```
@@ -79,27 +85,62 @@ node dist/index.js .
79
85
 
80
86
  Released binaries: `cejel-Darwin-arm64`, `cejel-Darwin-x86_64`, `cejel-Linux-aarch64`, and
81
87
  `cejel-Linux-x86_64`. The release also carries `SHA256SUMS`; each binary is executed against
82
- the source build and with networking denied before attachment.
88
+ the source build and with networking denied before attachment. Releases from v0.1.6 also
89
+ carry a Sigstore bundle containing GitHub's signed build-provenance attestation for all four
90
+ binaries. Verify a downloaded binary with:
91
+
92
+ ```bash
93
+ gh attestation verify ./cejel-Darwin-arm64 -R BargLabs/cejel
94
+ ```
95
+
96
+ This is cryptographically signed provenance. It is distinct from Apple Developer ID or
97
+ Microsoft Authenticode code-signing.
98
+
99
+ **Docker / OCI.** The same MCP server is published as a non-root, multi-platform image:
100
+
101
+ ```bash
102
+ docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.1.6
103
+ ```
104
+
105
+ The image defaults to `cejel-mcp` over stdio. To use the CLI instead:
106
+
107
+ ```bash
108
+ docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.1.6 .
109
+ ```
110
+
111
+ Beginning with v0.1.7, the standard CLI identity flags are also available directly, while an
112
+ invocation without arguments continues to start the MCP server:
113
+
114
+ ```bash
115
+ docker run --rm ghcr.io/barglabs/cejel:0.1.7 --version
116
+ docker run --rm ghcr.io/barglabs/cejel:0.1.7 --help
117
+ ```
118
+
119
+ The OCI image carries an SBOM, maximum-mode build provenance, and a signed registry
120
+ attestation. It is also the package referenced by [`server.json`](./server.json) for the
121
+ Official MCP Registry.
83
122
 
84
123
  ## Leaderboard
85
124
 
86
- This repo ships the [Cejel OSS trust leaderboard](./leaderboard/leaderboard.md): the whole
87
- corpus scored and published in the open — elite OSS projects, Cejel itself, and the private
88
- studio monorepo Cejel was built inside — with a per-repository evidence report for every
89
- row under [`leaderboard/reports/`](./leaderboard/reports/). The board is also hosted at
90
- [cejel.dev](https://cejel.dev). Every score on this board is produced by the same sealed
91
- public scorer used by `npx @cejel/cejel .` — check out the source commit printed in the
92
- report, run the tool, and you will get this number. A required guard does exactly that for
93
- every corpus row and compares score, verdict, measured coverage, and evidence. No private
94
- domain collector contributes to any published score, ours included; nobody is exempt.
125
+ This repo ships the [Cejel OSS trust leaderboard](./leaderboard/leaderboard.md): elite OSS
126
+ projects, the public Cejel repository itself, and one explicitly labeled transparency
127
+ snapshot from the private studio monorepo where Cejel was built, with a per-repository
128
+ evidence report for every row under [`leaderboard/reports/`](./leaderboard/reports/). The
129
+ board is also hosted at [cejel.dev](https://cejel.dev). Every score is produced by the same
130
+ sealed public scorer used by `npx @cejel/cejel .`; no private domain collector contributes.
131
+ For each public-repository row, check out the immutable source commit printed in its report
132
+ and run the public scorer to reproduce the score, verdict, measured coverage, and evidence.
133
+ The private Alfred snapshot discloses its limitation instead: its source commit and withheld
134
+ locations are not publicly available, so that row is not independently reproducible and is
135
+ not presented as a public-repository self-score.
95
136
 
96
137
  ### Redaction policy
97
138
 
98
139
  A path is published exactly when the reader can check it. Every public repository on the
99
140
  board cites full evidence paths and line numbers, everywhere, in every artifact — a
100
- certificate whose evidence you cannot open is not evidence. The two private-repository
101
- entries (this monorepo's own transparency entry, and Cejel's own source sub-package inside
102
- it) never cite a source path, anywhere, in any field or format — but the finding itself, its
141
+ certificate whose evidence you cannot open is not evidence. The private-repository entry
142
+ (the studio monorepo's transparency snapshot) never cites a source path, anywhere, in any
143
+ field or format — but the finding itself, its
103
144
  dimension, status, score, and content hash always survive; only the location is withheld,
104
145
  marked uniformly as "path withheld — private repository". Redaction removes a location, never
105
146
  a fact: a private repository failing its own check still shows up on its certificate, by
@@ -127,10 +168,11 @@ better-evidenced rows.
127
168
  ## Usage
128
169
 
129
170
  ```bash
130
- ./cejel .
171
+ ./cejel scan .
131
172
  ```
132
173
 
133
- Scores the current directory with sensible defaults: no flags, no signup, fully offline.
174
+ `scan` scores the current directory with sensible defaults: no signup, fully offline. The
175
+ original `./cejel .` form remains supported as a compatibility shorthand.
134
176
  Prints a concise terminal certificate and writes to `.cejel/`:
135
177
 
136
178
  - `report.json` — the full structured report; scored runs carry numeric headline scores and a
@@ -147,6 +189,16 @@ statement, but it does not pretend to be an independent signer. Until a customer
147
189
  or provenance system signs it, `assurance.status` is `unsigned` and `issuer` is
148
190
  `self-generated`. An abstained scan carries only the refusal reason, never a numeric score.
149
191
 
192
+ Verify that an emitted report still matches its attestation:
193
+
194
+ ```bash
195
+ ./cejel verify .cejel/report.json .cejel/attestation.json
196
+ ```
197
+
198
+ This verifies the report schema and the report-to-attestation digest, repository, rubric,
199
+ timestamp, and outcome binding. It does not verify a signature or signer identity; the command
200
+ prints that boundary on every successful verification.
201
+
150
202
  ### Flags
151
203
 
152
204
  - `<path>` — repo to score (default: current directory)
@@ -272,7 +324,7 @@ a configurable `min-score` threshold. The scoring step makes no network calls an
272
324
  secrets.
273
325
 
274
326
  ```yaml
275
- - uses: BargLabs/cejel/action@v1
327
+ - uses: BargLabs/cejel/action@v0.1.6
276
328
  with:
277
329
  min-score: '2.5' # optional; omit to never fail the check
278
330
  ```
@@ -284,6 +336,10 @@ server over stdio, so any MCP client (Claude Code, Cowork, Cursor, Codex) can re
284
336
  trust certificate as a tool call. It wraps the exact same scan the CLI runs — same scores,
285
337
  same verdict — and is listed on Smithery via the repo's `smithery.yaml`.
286
338
 
339
+ The canonical discovery manifest is [`server.json`](./server.json), published under
340
+ `io.github.BargLabs/cejel` in the Official MCP Registry. Its npm ownership metadata,
341
+ OCI ownership label, image version, and registry version are checked together before release.
342
+
287
343
  Add it to an MCP client config:
288
344
 
289
345
  ```json
@@ -311,7 +367,7 @@ and the server writes no files.
311
367
 
312
368
  ## What "offline" means here
313
369
 
314
- Scoring a repo — `cejel .` itself, and the Action's scoring step — makes zero network
370
+ Scoring a repo — `cejel scan .` itself, and the Action's scoring step — makes zero network
315
371
  calls: no telemetry, no signup, no model call. Fetching the `@cejel/cejel` package the
316
372
  first time (like any npm-distributed CLI, including this Action's own dependency install)
317
373
  does need network; that's a one-time install cost, not part of the scoring guarantee.
@@ -347,7 +403,7 @@ enforced structurally — the public artifacts are built from filtered data rath
347
403
  rendered and then scrubbed — and a build that would emit a private path fails rather than
348
404
  publishes.
349
405
 
350
- **What we exclude from ranking.** The v3 repository scanner does not evaluate B1 (dispatch
406
+ **What we exclude from ranking.** The v5 repository scanner does not evaluate B1 (dispatch
351
407
  trace completeness) or B5 (verified learning trace) for repository inputs, including ours:
352
408
  both are always *not applicable* in a repository certificate. They remain defined in the
353
409
  rubric for structured substrate evidence, but that evidence is not accepted by this scanner.
@@ -392,6 +448,18 @@ scoring tool that has not been checked:
392
448
  and a required guard re-scores every row and compares score, verdict, coverage, and
393
449
  evidence. The honest correction moved Alfred's Code score from 2.5 to 2.4 and A5 from 2.9
394
450
  to 2.4. It is less flattering and more trustworthy.
451
+ - **A tracked symlink could make evidence depend on files outside the checkout.** Repository
452
+ discovery used a regular-file check that followed symlinks, so a tracked link could escape
453
+ the immutable source snapshot. Rubric v4 accepts only tracked regular files. On the pinned
454
+ corpus this removes a duplicate claim source from Zod: A5 moves from 2.2 to 2.0 while its
455
+ rounded Code score, Overall, verdict, and rank stay unchanged. The full 24-row delta is in
456
+ the rubric changelog.
457
+ - **A pinned commit could score differently depending on unrelated Git refs in the clone.**
458
+ History evidence traversed every local and remote-tracking ref, including branches that were
459
+ not ancestors of the revision being certified. The required clean-clone reproduction job
460
+ caught the mismatch. Rubric v5 scans only `HEAD` ancestry, so ambient branch state can no
461
+ longer add evidence or change coverage. The complete v4-to-v5 delta is published in the
462
+ rubric changelog.
395
463
  - **Version 0.1.1 rejected `--help` and `--version` as unknown flags, and read `-h` as a
396
464
  directory path.** Version 0.1.2 handles both aliases before positional-path parsing and
397
465
  derives its printed version from the package manifest.
@@ -4074,7 +4074,9 @@ var NEVER = INVALID;
4074
4074
  var WITAN_RUBRIC_VERSION_V1 = "witan-rubric-v1-2026-06-24";
4075
4075
  var WITAN_RUBRIC_VERSION_V2 = "witan-rubric-v2-2026-07-12";
4076
4076
  var WITAN_RUBRIC_VERSION_V3 = "witan-rubric-v3-2026-07-13";
4077
- var WITAN_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V3;
4077
+ var WITAN_RUBRIC_VERSION_V4 = "witan-rubric-v4-2026-07-18";
4078
+ var WITAN_RUBRIC_VERSION_V5 = "witan-rubric-v5-2026-07-18";
4079
+ var WITAN_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V5;
4078
4080
  var WITAN_TRADING_RUBRIC_VERSION_V0 = "witan-trading-rubric-v0-2026-07-01";
4079
4081
  var WITAN_CRITERION_IDS = [
4080
4082
  "A1",
@@ -4509,12 +4511,13 @@ function criteriaByCategory(report, category) {
4509
4511
  function renderCriterionColumn(title, criteria) {
4510
4512
  const applicable = criteria.filter((criterion) => criterion.status !== "not_applicable");
4511
4513
  const notApplicable = criteria.filter((criterion) => criterion.status === "not_applicable");
4514
+ const notApplicableGroup = notApplicable.length > 0 ? `
4515
+ ${renderNotApplicableGroup(notApplicable)}` : "";
4512
4516
  return `<section class="criteria-column">
4513
4517
  <h2>${escapeHtml(title)}</h2>
4514
4518
  <div class="criteria-list">
4515
4519
  ${applicable.map(renderCriterionCard).join("")}
4516
- </div>
4517
- ${notApplicable.length > 0 ? renderNotApplicableGroup(notApplicable) : ""}
4520
+ </div>${notApplicableGroup}
4518
4521
  </section>`;
4519
4522
  }
4520
4523
  function renderNotApplicableGroup(criteria) {
@@ -4863,11 +4866,55 @@ function createWitanAttestation(report, options) {
4863
4866
  }
4864
4867
  });
4865
4868
  }
4869
+ function verifyWitanAttestationBinding(statement, report, options = {}) {
4870
+ const parsed = WitanAttestationStatementSchema.safeParse(statement);
4871
+ if (!parsed.success) {
4872
+ return {
4873
+ valid: false,
4874
+ errors: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
4875
+ };
4876
+ }
4877
+ const errors = [];
4878
+ const expectedReportHash = options.reportSha256 ?? hashWitanReport(report);
4879
+ const subject = parsed.data.subject[0];
4880
+ const subjectHash = subject?.digest.sha256;
4881
+ if (subjectHash !== expectedReportHash) errors.push("subject digest does not match report.json");
4882
+ if (subject?.name !== `${report.productSlug}/report.json`) {
4883
+ errors.push("subject name does not match report repository identity");
4884
+ }
4885
+ if (parsed.data.predicate.report.sha256 !== expectedReportHash) {
4886
+ errors.push("predicate report digest does not match report.json");
4887
+ }
4888
+ if (parsed.data.predicate.rubricVersion !== report.rubricVersion) {
4889
+ errors.push("rubric version does not match report.json");
4890
+ }
4891
+ if (parsed.data.predicate.generatedAt !== report.generatedAt) {
4892
+ errors.push("generated timestamp does not match report.json");
4893
+ }
4894
+ if (parsed.data.predicate.repository.headSha !== report.repo.headSha) {
4895
+ errors.push("repository revision does not match report.json");
4896
+ }
4897
+ if (parsed.data.predicate.repository.productSlug !== report.productSlug) {
4898
+ errors.push("repository product slug does not match report.json");
4899
+ }
4900
+ if (parsed.data.predicate.repository.url !== report.repo.url) {
4901
+ errors.push("repository URL does not match report.json");
4902
+ }
4903
+ const outcome = parsed.data.predicate.outcome;
4904
+ if (report.verdict === "insufficient_source") {
4905
+ if (outcome.status !== "abstained" || outcome.reason !== report.insufficientSourceReason) {
4906
+ errors.push("attestation does not preserve the report abstention");
4907
+ }
4908
+ } else if (outcome.status !== "scored" || outcome.overallScore !== report.overallScore || outcome.codeTrustScore !== report.codeTrustScore || outcome.processTrustScore !== report.processTrustScore) {
4909
+ errors.push("attested scores do not match report.json");
4910
+ }
4911
+ return { valid: errors.length === 0, errors };
4912
+ }
4866
4913
 
4867
4914
  // src/witan/repo-signals.ts
4868
4915
  import { execFileSync } from "child_process";
4869
4916
  import { createHash as createHash2 } from "crypto";
4870
- import { readFileSync, readdirSync, realpathSync, statSync } from "fs";
4917
+ import { lstatSync, readFileSync, readdirSync, realpathSync, statSync } from "fs";
4871
4918
  import { basename, join, relative, resolve, sep } from "path";
4872
4919
 
4873
4920
  // src/witan/json-safe.ts
@@ -6442,7 +6489,9 @@ function listGitTrackedFiles(repoPath) {
6442
6489
  stdio: ["ignore", "pipe", "ignore"]
6443
6490
  }).trim();
6444
6491
  if (!output) return [];
6445
- return output.split("\n").filter((file) => file.length > 0 && !isHardExcludedPath(file)).sort();
6492
+ return output.split("\n").filter(
6493
+ (file) => file.length > 0 && !isHardExcludedPath(file) && isRegularFile(join(repoPath, file))
6494
+ ).sort();
6446
6495
  } catch {
6447
6496
  return null;
6448
6497
  }
@@ -6960,7 +7009,7 @@ function findRootPackageJson(repoFiles) {
6960
7009
  }
6961
7010
  function isRegularFile(path) {
6962
7011
  try {
6963
- return statSync(path).isFile();
7012
+ return lstatSync(path).isFile();
6964
7013
  } catch {
6965
7014
  return false;
6966
7015
  }
@@ -7030,7 +7079,7 @@ function readCredentialHistoryEntries(repoPath) {
7030
7079
  entries.push(entry);
7031
7080
  };
7032
7081
  try {
7033
- const commits = execFileSync("git", ["rev-list", "--all"], {
7082
+ const commits = execFileSync("git", ["rev-list", "HEAD"], {
7034
7083
  cwd: repoPath,
7035
7084
  encoding: "utf8",
7036
7085
  stdio: ["ignore", "pipe", "ignore"]
@@ -7089,7 +7138,7 @@ function readDeletedCredentialHistoryPaths(repoPath) {
7089
7138
  try {
7090
7139
  const output = execFileSync(
7091
7140
  "git",
7092
- ["log", "--all", "--diff-filter=D", "--name-status", "--format=commit:%H"],
7141
+ ["log", "HEAD", "--diff-filter=D", "--name-status", "--format=commit:%H"],
7093
7142
  {
7094
7143
  cwd: repoPath,
7095
7144
  encoding: "utf8",
@@ -7115,7 +7164,7 @@ function readHistoryCommitsForPath(repoPath, path) {
7115
7164
  try {
7116
7165
  const output = execFileSync(
7117
7166
  "git",
7118
- ["log", "--all", "--diff-filter=AM", "--format=%H", "--", path],
7167
+ ["log", "HEAD", "--diff-filter=AM", "--format=%H", "--", path],
7119
7168
  {
7120
7169
  cwd: repoPath,
7121
7170
  encoding: "utf8",
@@ -7273,7 +7322,7 @@ function hasEnvPathInGitHistory(repoPath) {
7273
7322
  try {
7274
7323
  const output = execFileSync(
7275
7324
  "git",
7276
- ["log", "--all", "--diff-filter=AM", "--name-only", "--format="],
7325
+ ["log", "HEAD", "--diff-filter=AM", "--name-only", "--format="],
7277
7326
  {
7278
7327
  cwd: repoPath,
7279
7328
  encoding: "utf8",
@@ -7615,7 +7664,7 @@ function rawToolName(raw) {
7615
7664
  if (typeof obj.tool === "string" && obj.tool.length > 0) return obj.tool;
7616
7665
  return "scorecard";
7617
7666
  }
7618
- function parseIngestFile(filePath) {
7667
+ function parseIngestFile(filePath, options = {}) {
7619
7668
  let raw;
7620
7669
  try {
7621
7670
  raw = JSON.parse(stripBom(readFileSync5(filePath, "utf8")));
@@ -7624,7 +7673,7 @@ function parseIngestFile(filePath) {
7624
7673
  throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
7625
7674
  }
7626
7675
  let signals;
7627
- if (looksLikeSarif(raw)) signals = parseSarifJson(raw);
7676
+ if (looksLikeSarif(raw)) signals = parseSarifJson(raw, options.extraSarifDimensionRules);
7628
7677
  else if (looksLikeScorecard(raw)) signals = parseScorecardJson(raw);
7629
7678
  else if (isGenericSignalDocument(raw)) signals = parseGenericJson(raw);
7630
7679
  if (!signals) {
@@ -7888,7 +7937,7 @@ function mergeSignalsByCriterion(signals) {
7888
7937
  return merged;
7889
7938
  }
7890
7939
  function usesMetricScoring(rubricVersion) {
7891
- return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
7940
+ return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
7892
7941
  }
7893
7942
  function unmeasuredStatus(rubricVersion) {
7894
7943
  return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
@@ -8186,11 +8235,13 @@ export {
8186
8235
  ZodFirstPartyTypeKind,
8187
8236
  objectType,
8188
8237
  external_exports,
8238
+ WitanReportSchema,
8189
8239
  formatExternalSourceLine,
8190
8240
  renderWitanHtmlReport,
8191
8241
  renderWitanBadgeEndpoint,
8192
8242
  renderWitanBadgeSvg,
8193
8243
  serializeWitanReport,
8194
8244
  createWitanAttestation,
8245
+ verifyWitanAttestationBinding,
8195
8246
  runCejelScan
8196
8247
  };
package/dist/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ WitanReportSchema,
3
4
  createWitanAttestation,
4
5
  formatExternalSourceLine,
5
6
  renderWitanBadgeEndpoint,
6
7
  renderWitanBadgeSvg,
7
8
  renderWitanHtmlReport,
8
9
  runCejelScan,
9
- serializeWitanReport
10
- } from "./chunk-A7NZKJ5H.js";
10
+ serializeWitanReport,
11
+ verifyWitanAttestationBinding
12
+ } from "./chunk-OMIRVZSF.js";
11
13
 
12
14
  // src/index.ts
15
+ import { createHash } from "crypto";
13
16
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
14
17
  import { join, resolve } from "path";
15
18
  import { pathToFileURL } from "url";
@@ -121,18 +124,31 @@ var CLI_FLAG_TOKENS = CLI_FLAG_SPECS.flatMap(
121
124
  var CLI_FLAG_KIND_BY_TOKEN = new Map(
122
125
  CLI_FLAG_SPECS.flatMap((spec) => spec.tokens.map((token) => [token, spec.kind]))
123
126
  );
127
+ function readCliManifest() {
128
+ return JSON.parse(
129
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
130
+ );
131
+ }
124
132
  function cliVersion() {
125
133
  if (typeof __CEJEL_SEA_VERSION__ === "string" && __CEJEL_SEA_VERSION__.length > 0) {
126
134
  return __CEJEL_SEA_VERSION__;
127
135
  }
128
- const manifest = JSON.parse(
129
- readFileSync(new URL("../package.json", import.meta.url), "utf8")
130
- );
136
+ const manifest = readCliManifest();
131
137
  if (typeof manifest.version !== "string" || manifest.version.length === 0) {
132
138
  throw new Error("Cejel package manifest has no version.");
133
139
  }
134
140
  return manifest.version;
135
141
  }
142
+ function cliPackageName() {
143
+ if (typeof __CEJEL_SEA_PACKAGE_NAME__ === "string" && __CEJEL_SEA_PACKAGE_NAME__.length > 0) {
144
+ return __CEJEL_SEA_PACKAGE_NAME__;
145
+ }
146
+ const manifest = readCliManifest();
147
+ if (typeof manifest.name !== "string" || manifest.name.length === 0) {
148
+ throw new Error("Cejel package manifest has no name.");
149
+ }
150
+ return manifest.name;
151
+ }
136
152
  var usageOptionLabels = CLI_FLAG_SPECS.map(
137
153
  (spec) => `${spec.tokens.join(", ")}${"value" in spec && spec.value ? ` ${spec.value}` : ""}`
138
154
  );
@@ -140,11 +156,19 @@ var usageOptionLabelWidth = Math.max(...usageOptionLabels.map((label) => label.l
140
156
  var usageOptions = CLI_FLAG_SPECS.map(
141
157
  (spec, index) => ` ${usageOptionLabels[index]?.padEnd(usageOptionLabelWidth)} ${spec.description}`
142
158
  ).join("\n");
159
+ var NPX_PACKAGE_NAME = cliPackageName();
143
160
  var USAGE = `cejel \u2014 a trust certificate for your codebase
144
161
 
145
- Usage: npx cejel [path] [options]
162
+ Usage:
163
+ npx ${NPX_PACKAGE_NAME} [path] [options]
164
+ npx ${NPX_PACKAGE_NAME} scan [path] [options]
165
+ npx ${NPX_PACKAGE_NAME} verify <report.json> <attestation.json>
166
+
167
+ Commands:
168
+ scan score a repository (default when the command is omitted)
169
+ verify verify report/attestation binding only; no signature or signer identity check
146
170
 
147
- Options:
171
+ Scan options:
148
172
  ${usageOptions}
149
173
 
150
174
  Runs entirely offline. No code leaves your machine.
@@ -155,7 +179,11 @@ async function main() {
155
179
  process.exitCode = exitCode;
156
180
  }
157
181
  async function runWitanFreeCli(args) {
158
- const options = parseArgs(args);
182
+ const invocation = parseCliInvocation(args);
183
+ if (invocation.command === "verify") {
184
+ return runVerifyBinding(invocation.reportPath, invocation.attestationPath);
185
+ }
186
+ const options = invocation.options;
159
187
  if (options.showVersion) {
160
188
  process.stdout.write(`${cliVersion()}
161
189
  `);
@@ -217,6 +245,80 @@ Wrote:
217
245
  }
218
246
  return 0;
219
247
  }
248
+ function parseCliInvocation(args) {
249
+ const command = args[0];
250
+ if (command === "scan") {
251
+ return { command: "scan", options: parseArgs(args.slice(1)) };
252
+ }
253
+ if (command === "verify") {
254
+ const verifyArgs = args.slice(1);
255
+ if (verifyArgs.some((arg) => arg === "-h" || arg === "--help")) {
256
+ return { command: "scan", options: parseArgs(["--help"]) };
257
+ }
258
+ if (verifyArgs.some((arg) => arg === "-v" || arg === "--version")) {
259
+ return { command: "scan", options: parseArgs(["--version"]) };
260
+ }
261
+ if (verifyArgs.length !== 2) {
262
+ throw new Error(`Usage: npx ${NPX_PACKAGE_NAME} verify <report.json> <attestation.json>`);
263
+ }
264
+ const [reportPath, attestationPath] = verifyArgs;
265
+ if (!reportPath || !attestationPath) {
266
+ throw new Error(`Usage: npx ${NPX_PACKAGE_NAME} verify <report.json> <attestation.json>`);
267
+ }
268
+ return {
269
+ command: "verify",
270
+ reportPath: resolve(reportPath),
271
+ attestationPath: resolve(attestationPath)
272
+ };
273
+ }
274
+ return { command: "scan", options: parseArgs(args) };
275
+ }
276
+ function runVerifyBinding(reportPath, attestationPath) {
277
+ const reportArtifact = readJsonArtifact(reportPath, "report");
278
+ const reportResult = WitanReportSchema.safeParse(reportArtifact.value);
279
+ if (!reportResult.success) {
280
+ const members = reportResult.error.issues.map(
281
+ (issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`
282
+ );
283
+ process.stderr.write(
284
+ `Cejel: report validation failed:
285
+ ${members.map((member) => ` - ${member}`).join("\n")}
286
+ `
287
+ );
288
+ return 1;
289
+ }
290
+ const statement = readJsonArtifact(attestationPath, "attestation");
291
+ const reportSha256 = createHash("sha256").update(reportArtifact.contents).digest("hex");
292
+ const result = verifyWitanAttestationBinding(statement.value, reportResult.data, {
293
+ reportSha256
294
+ });
295
+ if (!result.valid) {
296
+ process.stderr.write(
297
+ `Cejel: report/attestation binding verification failed:
298
+ ${result.errors.map((error) => ` - ${error}`).join("\n")}
299
+ `
300
+ );
301
+ return 1;
302
+ }
303
+ process.stdout.write("Cejel: report/attestation binding verified.\n");
304
+ process.stdout.write("Cejel: signature and signer identity were not verified.\n");
305
+ return 0;
306
+ }
307
+ function readJsonArtifact(path, label) {
308
+ let contents;
309
+ try {
310
+ contents = readFileSync(path);
311
+ } catch (error) {
312
+ const message = error instanceof Error ? error.message : String(error);
313
+ throw new Error(`Cejel: could not read ${label} file ${path}: ${message}`);
314
+ }
315
+ try {
316
+ return { contents, value: JSON.parse(contents.toString("utf8")) };
317
+ } catch (error) {
318
+ const message = error instanceof Error ? error.message : String(error);
319
+ throw new Error(`Cejel: invalid JSON in ${label} file ${path}: ${message}`);
320
+ }
321
+ }
220
322
  function parseArgs(args) {
221
323
  let repoPath;
222
324
  let outDir = DEFAULT_OUT_DIR;
@@ -271,7 +373,11 @@ function parseArgs(args) {
271
373
  }
272
374
  continue;
273
375
  }
274
- if (repoPath === void 0) repoPath = arg;
376
+ if (repoPath === void 0) {
377
+ repoPath = arg;
378
+ } else {
379
+ throw new Error(`Unexpected positional argument: ${arg}`);
380
+ }
275
381
  }
276
382
  return {
277
383
  repoPath: resolve(repoPath ?? "."),
@@ -307,5 +413,6 @@ export {
307
413
  CLI_FLAG_TOKENS,
308
414
  USAGE,
309
415
  parseArgs,
416
+ parseCliInvocation,
310
417
  runWitanFreeCli
311
418
  };
package/dist/mcp/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  renderWitanBadgeSvg,
11
11
  renderWitanHtmlReport,
12
12
  runCejelScan
13
- } from "../chunk-A7NZKJ5H.js";
13
+ } from "../chunk-OMIRVZSF.js";
14
14
 
15
15
  // node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js
16
16
  var require_code = __commonJS({
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
+ "mcpName": "io.github.BargLabs/cejel",
4
5
  "private": false,
5
6
  "publishConfig": {
6
7
  "access": "public"
@@ -46,6 +47,7 @@
46
47
  "scripts": {
47
48
  "build": "tsup",
48
49
  "build:sea-js": "tsup --config tsup.public-sea.config.ts",
50
+ "validate:distribution": "node scripts/validate-distribution-metadata.mjs",
49
51
  "prepublishOnly": "pnpm run build",
50
52
  "prepack": "pnpm run build",
51
53
  "dev": "tsx src/index.ts",