@cejel/cejel 0.1.6 → 0.1.8

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.8
68
74
  with:
69
75
  min-score: "2.5" # optional: fail the build below this
70
76
  ```
@@ -93,13 +99,21 @@ Microsoft Authenticode code-signing.
93
99
  **Docker / OCI.** The same MCP server is published as a non-root, multi-platform image:
94
100
 
95
101
  ```bash
96
- docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.1.6
102
+ docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.1.8
97
103
  ```
98
104
 
99
105
  The image defaults to `cejel-mcp` over stdio. To use the CLI instead:
100
106
 
101
107
  ```bash
102
- docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.1.6 .
108
+ docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.1.8 .
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.8 --version
116
+ docker run --rm ghcr.io/barglabs/cejel:0.1.8 --help
103
117
  ```
104
118
 
105
119
  The OCI image carries an SBOM, maximum-mode build provenance, and a signed registry
@@ -108,23 +122,25 @@ Official MCP Registry.
108
122
 
109
123
  ## Leaderboard
110
124
 
111
- This repo ships the [Cejel OSS trust leaderboard](./leaderboard/leaderboard.md): the whole
112
- corpus scored and published in the open — elite OSS projects, Cejel itself, and the private
113
- studio monorepo Cejel was built inside — with a per-repository evidence report for every
114
- row under [`leaderboard/reports/`](./leaderboard/reports/). The board is also hosted at
115
- [cejel.dev](https://cejel.dev). Every score on this board is produced by the same sealed
116
- public scorer used by `npx @cejel/cejel .` — check out the source commit printed in the
117
- report, run the tool, and you will get this number. A required guard does exactly that for
118
- every corpus row and compares score, verdict, measured coverage, and evidence. No private
119
- 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.
120
136
 
121
137
  ### Redaction policy
122
138
 
123
139
  A path is published exactly when the reader can check it. Every public repository on the
124
140
  board cites full evidence paths and line numbers, everywhere, in every artifact — a
125
- certificate whose evidence you cannot open is not evidence. The two private-repository
126
- entries (this monorepo's own transparency entry, and Cejel's own source sub-package inside
127
- 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
128
144
  dimension, status, score, and content hash always survive; only the location is withheld,
129
145
  marked uniformly as "path withheld — private repository". Redaction removes a location, never
130
146
  a fact: a private repository failing its own check still shows up on its certificate, by
@@ -152,10 +168,11 @@ better-evidenced rows.
152
168
  ## Usage
153
169
 
154
170
  ```bash
155
- ./cejel .
171
+ ./cejel scan .
156
172
  ```
157
173
 
158
- 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.
159
176
  Prints a concise terminal certificate and writes to `.cejel/`:
160
177
 
161
178
  - `report.json` — the full structured report; scored runs carry numeric headline scores and a
@@ -172,6 +189,16 @@ statement, but it does not pretend to be an independent signer. Until a customer
172
189
  or provenance system signs it, `assurance.status` is `unsigned` and `issuer` is
173
190
  `self-generated`. An abstained scan carries only the refusal reason, never a numeric score.
174
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
+
175
202
  ### Flags
176
203
 
177
204
  - `<path>` — repo to score (default: current directory)
@@ -297,7 +324,7 @@ a configurable `min-score` threshold. The scoring step makes no network calls an
297
324
  secrets.
298
325
 
299
326
  ```yaml
300
- - uses: BargLabs/cejel/action@v1
327
+ - uses: BargLabs/cejel/action@v0.1.8
301
328
  with:
302
329
  min-score: '2.5' # optional; omit to never fail the check
303
330
  ```
@@ -340,7 +367,7 @@ and the server writes no files.
340
367
 
341
368
  ## What "offline" means here
342
369
 
343
- 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
344
371
  calls: no telemetry, no signup, no model call. Fetching the `@cejel/cejel` package the
345
372
  first time (like any npm-distributed CLI, including this Action's own dependency install)
346
373
  does need network; that's a one-time install cost, not part of the scoring guarantee.
@@ -376,7 +403,7 @@ enforced structurally — the public artifacts are built from filtered data rath
376
403
  rendered and then scrubbed — and a build that would emit a private path fails rather than
377
404
  publishes.
378
405
 
379
- **What we exclude from ranking.** The v3 repository scanner does not evaluate B1 (dispatch
406
+ **What we exclude from ranking.** The v6 repository scanner does not evaluate B1 (dispatch
380
407
  trace completeness) or B5 (verified learning trace) for repository inputs, including ours:
381
408
  both are always *not applicable* in a repository certificate. They remain defined in the
382
409
  rubric for structured substrate evidence, but that evidence is not accepted by this scanner.
@@ -421,6 +448,18 @@ scoring tool that has not been checked:
421
448
  and a required guard re-scores every row and compares score, verdict, coverage, and
422
449
  evidence. The honest correction moved Alfred's Code score from 2.5 to 2.4 and A5 from 2.9
423
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.
424
463
  - **Version 0.1.1 rejected `--help` and `--version` as unknown flags, and read `-h` as a
425
464
  directory path.** Version 0.1.2 handles both aliases before positional-path parsing and
426
465
  derives its printed version from the package manifest.
@@ -441,6 +480,11 @@ scoring tool that has not been checked:
441
480
  "Unverified". Every real legacy repository has a deploy script, so the fix that passed every
442
481
  test was unreachable for essentially all of them. Recognised source must now be *dominant*,
443
482
  not merely present. **A fixture cleaner than reality proves nothing.**
483
+ - **Version 0.1.7 missed AVA's root-level `test.js` convention.** A smoke test against
484
+ `sindresorhus/slugify` reported that a runner was configured but no concrete test files were
485
+ detected even though `test.js` was present at the repository root. That was a detection gap
486
+ in Cejel, not evidence about the repository. Rubric v6 recognizes AVA's `test.js` and
487
+ `test-*.js` conventions; a regression fixture preserves the correction.
444
488
 
445
489
  Every one was a trust failure produced by *us* — false alarms about other people's code,
446
490
  silent omissions, inconsistent presentation, or a home-only scoring path — and we would
@@ -4074,7 +4074,10 @@ 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_V6 = "witan-rubric-v6-2026-07-21";
4080
+ var WITAN_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V6;
4078
4081
  var WITAN_TRADING_RUBRIC_VERSION_V0 = "witan-trading-rubric-v0-2026-07-01";
4079
4082
  var WITAN_CRITERION_IDS = [
4080
4083
  "A1",
@@ -4509,12 +4512,13 @@ function criteriaByCategory(report, category) {
4509
4512
  function renderCriterionColumn(title, criteria) {
4510
4513
  const applicable = criteria.filter((criterion) => criterion.status !== "not_applicable");
4511
4514
  const notApplicable = criteria.filter((criterion) => criterion.status === "not_applicable");
4515
+ const notApplicableGroup = notApplicable.length > 0 ? `
4516
+ ${renderNotApplicableGroup(notApplicable)}` : "";
4512
4517
  return `<section class="criteria-column">
4513
4518
  <h2>${escapeHtml(title)}</h2>
4514
4519
  <div class="criteria-list">
4515
4520
  ${applicable.map(renderCriterionCard).join("")}
4516
- </div>
4517
- ${notApplicable.length > 0 ? renderNotApplicableGroup(notApplicable) : ""}
4521
+ </div>${notApplicableGroup}
4518
4522
  </section>`;
4519
4523
  }
4520
4524
  function renderNotApplicableGroup(criteria) {
@@ -4863,11 +4867,55 @@ function createWitanAttestation(report, options) {
4863
4867
  }
4864
4868
  });
4865
4869
  }
4870
+ function verifyWitanAttestationBinding(statement, report, options = {}) {
4871
+ const parsed = WitanAttestationStatementSchema.safeParse(statement);
4872
+ if (!parsed.success) {
4873
+ return {
4874
+ valid: false,
4875
+ errors: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
4876
+ };
4877
+ }
4878
+ const errors = [];
4879
+ const expectedReportHash = options.reportSha256 ?? hashWitanReport(report);
4880
+ const subject = parsed.data.subject[0];
4881
+ const subjectHash = subject?.digest.sha256;
4882
+ if (subjectHash !== expectedReportHash) errors.push("subject digest does not match report.json");
4883
+ if (subject?.name !== `${report.productSlug}/report.json`) {
4884
+ errors.push("subject name does not match report repository identity");
4885
+ }
4886
+ if (parsed.data.predicate.report.sha256 !== expectedReportHash) {
4887
+ errors.push("predicate report digest does not match report.json");
4888
+ }
4889
+ if (parsed.data.predicate.rubricVersion !== report.rubricVersion) {
4890
+ errors.push("rubric version does not match report.json");
4891
+ }
4892
+ if (parsed.data.predicate.generatedAt !== report.generatedAt) {
4893
+ errors.push("generated timestamp does not match report.json");
4894
+ }
4895
+ if (parsed.data.predicate.repository.headSha !== report.repo.headSha) {
4896
+ errors.push("repository revision does not match report.json");
4897
+ }
4898
+ if (parsed.data.predicate.repository.productSlug !== report.productSlug) {
4899
+ errors.push("repository product slug does not match report.json");
4900
+ }
4901
+ if (parsed.data.predicate.repository.url !== report.repo.url) {
4902
+ errors.push("repository URL does not match report.json");
4903
+ }
4904
+ const outcome = parsed.data.predicate.outcome;
4905
+ if (report.verdict === "insufficient_source") {
4906
+ if (outcome.status !== "abstained" || outcome.reason !== report.insufficientSourceReason) {
4907
+ errors.push("attestation does not preserve the report abstention");
4908
+ }
4909
+ } else if (outcome.status !== "scored" || outcome.overallScore !== report.overallScore || outcome.codeTrustScore !== report.codeTrustScore || outcome.processTrustScore !== report.processTrustScore) {
4910
+ errors.push("attested scores do not match report.json");
4911
+ }
4912
+ return { valid: errors.length === 0, errors };
4913
+ }
4866
4914
 
4867
4915
  // src/witan/repo-signals.ts
4868
4916
  import { execFileSync } from "child_process";
4869
4917
  import { createHash as createHash2 } from "crypto";
4870
- import { readFileSync, readdirSync, realpathSync, statSync } from "fs";
4918
+ import { lstatSync, readFileSync, readdirSync, realpathSync, statSync } from "fs";
4871
4919
  import { basename, join, relative, resolve, sep } from "path";
4872
4920
 
4873
4921
  // src/witan/json-safe.ts
@@ -6282,9 +6330,11 @@ function isProductionSourcePath(path) {
6282
6330
  function isTestFile(file) {
6283
6331
  return /(^|\/)(__tests__|test|tests|spec)\/.*\.(?:[cm]?[jt]sx?|py|go|rs|java|rb|cpp|cc|cxx)$/.test(
6284
6332
  file
6285
- ) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(file) || /(^|\/)test_.*\.py$/.test(file) || /_test\.go$/.test(file) || /(^|\/)test_.*\.(cpp|cc|cxx)$/.test(file) || /_(test|tests)\.(cpp|cc|cxx)$/.test(file) || /Tests?\.(java|kt)$/.test(file) || /(_test|_spec)\.(rs|rb|php|swift|kt)$/.test(file);
6333
+ ) || // AVA's default discovery includes test.js and test-*.js at the scanned package root.
6334
+ // Keep this root-anchored: nested files need an existing test-directory/suffix convention.
6335
+ /^test(?:-[^/]+)?\.[cm]?[jt]sx?$/.test(file) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(file) || /(^|\/)test_.*\.py$/.test(file) || /_test\.go$/.test(file) || /(^|\/)test_.*\.(cpp|cc|cxx)$/.test(file) || /_(test|tests)\.(cpp|cc|cxx)$/.test(file) || /Tests?\.(java|kt)$/.test(file) || /(_test|_spec)\.(rs|rb|php|swift|kt)$/.test(file);
6286
6336
  }
6287
- var NAME_SHAPED_TEST_FILE_PATTERN = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)__tests__\/|(^|\/)test_[^/]*\.py$|(^|\/)tests?\.py$|_test\.go$|(^|\/)test_[^/]*\.(cpp|cc|cxx)$|_(test|tests)\.(cpp|cc|cxx)$|Tests?\.(java|kt)$|(_test|_spec)\.(rs|rb|php|swift|kt)$/;
6337
+ var NAME_SHAPED_TEST_FILE_PATTERN = /^test(?:-[^/]+)?\.[cm]?[jt]sx?$|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)__tests__\/|(^|\/)test_[^/]*\.py$|(^|\/)tests?\.py$|_test\.go$|(^|\/)test_[^/]*\.(cpp|cc|cxx)$|_(test|tests)\.(cpp|cc|cxx)$|Tests?\.(java|kt)$|(_test|_spec)\.(rs|rb|php|swift|kt)$/;
6288
6338
  var TEST_RUNNER_PATTERN = /\b(vitest|jest|mocha|ava|tap|pytest|go test|cargo test|rspec|phpunit|gradle test|mvn test)\b/i;
6289
6339
  var CI_TEST_COMMAND_PATTERN = /\b(pytest|go\s+test|cargo\s+test|mvn\s+test|gradle\s+test|ctest|make\s+test|jest|vitest|mocha|rspec|phpunit|pnpm\s+test|npm\s+test|yarn\s+test|node\s+--test|node:test)\b/i;
6290
6340
  var CI_LINT_COMMAND_PATTERN = /\b(eslint|pylint|flake8|black|ruff)\b/i;
@@ -6442,7 +6492,9 @@ function listGitTrackedFiles(repoPath) {
6442
6492
  stdio: ["ignore", "pipe", "ignore"]
6443
6493
  }).trim();
6444
6494
  if (!output) return [];
6445
- return output.split("\n").filter((file) => file.length > 0 && !isHardExcludedPath(file)).sort();
6495
+ return output.split("\n").filter(
6496
+ (file) => file.length > 0 && !isHardExcludedPath(file) && isRegularFile(join(repoPath, file))
6497
+ ).sort();
6446
6498
  } catch {
6447
6499
  return null;
6448
6500
  }
@@ -6960,7 +7012,7 @@ function findRootPackageJson(repoFiles) {
6960
7012
  }
6961
7013
  function isRegularFile(path) {
6962
7014
  try {
6963
- return statSync(path).isFile();
7015
+ return lstatSync(path).isFile();
6964
7016
  } catch {
6965
7017
  return false;
6966
7018
  }
@@ -7030,7 +7082,7 @@ function readCredentialHistoryEntries(repoPath) {
7030
7082
  entries.push(entry);
7031
7083
  };
7032
7084
  try {
7033
- const commits = execFileSync("git", ["rev-list", "--all"], {
7085
+ const commits = execFileSync("git", ["rev-list", "HEAD"], {
7034
7086
  cwd: repoPath,
7035
7087
  encoding: "utf8",
7036
7088
  stdio: ["ignore", "pipe", "ignore"]
@@ -7089,7 +7141,7 @@ function readDeletedCredentialHistoryPaths(repoPath) {
7089
7141
  try {
7090
7142
  const output = execFileSync(
7091
7143
  "git",
7092
- ["log", "--all", "--diff-filter=D", "--name-status", "--format=commit:%H"],
7144
+ ["log", "HEAD", "--diff-filter=D", "--name-status", "--format=commit:%H"],
7093
7145
  {
7094
7146
  cwd: repoPath,
7095
7147
  encoding: "utf8",
@@ -7115,7 +7167,7 @@ function readHistoryCommitsForPath(repoPath, path) {
7115
7167
  try {
7116
7168
  const output = execFileSync(
7117
7169
  "git",
7118
- ["log", "--all", "--diff-filter=AM", "--format=%H", "--", path],
7170
+ ["log", "HEAD", "--diff-filter=AM", "--format=%H", "--", path],
7119
7171
  {
7120
7172
  cwd: repoPath,
7121
7173
  encoding: "utf8",
@@ -7273,7 +7325,7 @@ function hasEnvPathInGitHistory(repoPath) {
7273
7325
  try {
7274
7326
  const output = execFileSync(
7275
7327
  "git",
7276
- ["log", "--all", "--diff-filter=AM", "--name-only", "--format="],
7328
+ ["log", "HEAD", "--diff-filter=AM", "--name-only", "--format="],
7277
7329
  {
7278
7330
  cwd: repoPath,
7279
7331
  encoding: "utf8",
@@ -7615,7 +7667,7 @@ function rawToolName(raw) {
7615
7667
  if (typeof obj.tool === "string" && obj.tool.length > 0) return obj.tool;
7616
7668
  return "scorecard";
7617
7669
  }
7618
- function parseIngestFile(filePath) {
7670
+ function parseIngestFile(filePath, options = {}) {
7619
7671
  let raw;
7620
7672
  try {
7621
7673
  raw = JSON.parse(stripBom(readFileSync5(filePath, "utf8")));
@@ -7624,7 +7676,7 @@ function parseIngestFile(filePath) {
7624
7676
  throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
7625
7677
  }
7626
7678
  let signals;
7627
- if (looksLikeSarif(raw)) signals = parseSarifJson(raw);
7679
+ if (looksLikeSarif(raw)) signals = parseSarifJson(raw, options.extraSarifDimensionRules);
7628
7680
  else if (looksLikeScorecard(raw)) signals = parseScorecardJson(raw);
7629
7681
  else if (isGenericSignalDocument(raw)) signals = parseGenericJson(raw);
7630
7682
  if (!signals) {
@@ -7888,7 +7940,7 @@ function mergeSignalsByCriterion(signals) {
7888
7940
  return merged;
7889
7941
  }
7890
7942
  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;
7943
+ 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_RUBRIC_VERSION_V6 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
7892
7944
  }
7893
7945
  function unmeasuredStatus(rubricVersion) {
7894
7946
  return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
@@ -8186,11 +8238,13 @@ export {
8186
8238
  ZodFirstPartyTypeKind,
8187
8239
  objectType,
8188
8240
  external_exports,
8241
+ WitanReportSchema,
8189
8242
  formatExternalSourceLine,
8190
8243
  renderWitanHtmlReport,
8191
8244
  renderWitanBadgeEndpoint,
8192
8245
  renderWitanBadgeSvg,
8193
8246
  serializeWitanReport,
8194
8247
  createWitanAttestation,
8248
+ verifyWitanAttestationBinding,
8195
8249
  runCejelScan
8196
8250
  };
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-SFEPABAX.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-SFEPABAX.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,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "mcpName": "io.github.BargLabs/cejel",
5
5
  "private": false,
6
6
  "publishConfig": {