proof-of-commitment 1.6.0 → 1.7.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.
Files changed (3) hide show
  1. package/README.md +264 -68
  2. package/index.js +144 -72
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -1,78 +1,58 @@
1
- # proof-of-commitment
1
+ # Proof of Commitment
2
2
 
3
- > Supply chain risk scorer for npm, PyPI, Cargo (Rust), and Go modules. Behavioral signals that can't be faked.
3
+ [![Commitment Score](https://poc-backend.amdal-dev.workers.dev/badge/npm/proof-of-commitment)](https://getcommit.dev/audit?packages=proof-of-commitment)
4
4
 
5
- ```
6
- npx proof-of-commitment axios zod chalk
7
- ```
5
+ > **Stars lie. Behavioral signals don't.**
8
6
 
9
- ```
10
- ──────────────────────────────────────────────────────────────────────────
11
- Package Risk Score Publishers Downloads Age
12
- ──────────────────────────────────────────────────────────────────────────
13
- axios 🔴 CRITICAL 89 1 102.0M/wk 11.6y
14
- ↳ 30+ GitHub contributors — publish-access concentration risk despite active community
15
- └ longevity=25 momentum=25 releases=20 publishers=4 github=15
16
- zod 🔴 CRITICAL 83 1 154.0M/wk 6.1y
17
- ↳ 30+ GitHub contributors — publish-access concentration risk despite active community
18
- └ longevity=25 momentum=25 releases=18 publishers=4 github=11
19
- chalk 🔴 CRITICAL 75 1 414.6M/wk 12.7y
20
- ↳ 30+ GitHub contributors — publish-access concentration risk despite active community
21
- └ longevity=25 momentum=22 releases=13 publishers=4 github=11
22
- ──────────────────────────────────────────────────────────────────────────
23
-
24
- ⚠ 3 CRITICAL packages found.
25
- CRITICAL = sole npm publisher + >10M weekly downloads (publish-access concentration risk)
26
- Full breakdown: https://getcommit.dev/audit?packages=axios,zod,chalk
27
- ```
7
+ An MCP server and web tool that scores npm packages, PyPI packages, Rust crates, Go modules, and GitHub repos on **behavioral commitment** — signals that are harder to fake than stars, READMEs, or download counts.
28
8
 
29
- ## What this does
9
+ ## The supply chain problem
30
10
 
31
- `npm audit` finds *known* CVEs vulnerabilities already catalogued in a database. This scores *structural risk before it becomes a CVE*.
11
+ 26 of the 91 npm packages with >10M weekly downloads have a **single npm publisher**. Together they account for over 3 billion downloads per week. `npm audit` doesn't surface this. Stars don't either.
32
12
 
33
- The axios attack on April 1st, 2026: `npm audit` showed zero issues beforehand. Proof of Commitment flagged axios as CRITICAL (1 npm publisher, 96M downloads/week) — the exact publish-access concentration profile that made it a high-value target.
13
+ Four packages in a typical Node.js project are CRITICAL right now:
14
+ - **chalk** — 413M downloads/week, **1 npm publisher**
15
+ - **zod** — 163M downloads/week, **1 npm publisher** (30+ GitHub contributors)
16
+ - **lodash** — 145M downloads/week, **1 npm publisher**
17
+ - **axios** — 99M downloads/week, **1 npm publisher** (attacked March 30, 2026)
34
18
 
35
- **Score dimensions:**
36
- - **Longevity** (25 pts) years in production
37
- - **Download Momentum** (25 pts) weekly download trend
38
- - **Release Consistency** (20 pts) days since last release
39
- - **Publisher Depth** (15 pts) — npm publish-access holders
40
- - **GitHub Backing** (15 pts) — organization/team support
19
+ They won't appear in your `package.json` either — but these are in almost every project:
20
+ - **minimatch** 562M downloads/week, **1 npm publisher**
21
+ - **glob** 333M downloads/week, **1 npm publisher**
22
+ - **cross-spawn** 190M downloads/week, **1 npm publisher**
41
23
 
42
- **CRITICAL** = sole npm publisher + >10M weekly downloads (publish-access concentration risk)
24
+ Behavioral signals surface this. Stars and READMEs don't.
43
25
 
44
- ## Usage
26
+ ## Try it now
45
27
 
28
+ **Terminal (zero install):**
46
29
  ```bash
47
- # Score npm packages
48
- npx proof-of-commitment axios zod chalk lodash express
49
-
50
- # Score PyPI packages
51
- npx proof-of-commitment --pypi litellm langchain requests numpy
52
-
53
- # Score Rust crates
30
+ npx proof-of-commitment axios zod chalk
31
+ # scan your own project:
32
+ npx proof-of-commitment --file package.json
33
+ # scan ALL transitive dependencies via lock file (finds the hidden CRITICAL packages):
34
+ npx proof-of-commitment --file package-lock.json # npm
35
+ npx proof-of-commitment --file yarn.lock # yarn
36
+ npx proof-of-commitment --file pnpm-lock.yaml # pnpm
37
+ # pnpm monorepo — scans all workspace packages, deduplicates:
38
+ npx proof-of-commitment --file pnpm-workspace.yaml # pnpm workspaces
39
+ # JSON output for CI/CD pipelines (exits 1 if CRITICAL found):
40
+ npx proof-of-commitment --file package-lock.json --json | jq '.criticalCount'
41
+ # PyPI too:
42
+ npx proof-of-commitment --pypi litellm langchain requests
43
+ # Cargo (Rust) via CLI:
54
44
  npx proof-of-commitment --cargo serde tokio reqwest
55
-
56
- # Score Go modules (full module path required — host/owner/repo)
45
+ # Go modules via CLI (full module path required):
57
46
  npx proof-of-commitment --golang github.com/gin-gonic/gin golang.org/x/net
58
-
59
- # Auto-detect from manifest / lock / module file
60
- npx proof-of-commitment --file package.json # npm
61
- npx proof-of-commitment --file package-lock.json # full transitive
62
- npx proof-of-commitment --file requirements.txt # PyPI
63
- npx proof-of-commitment --file Cargo.toml # Rust direct deps
64
- npx proof-of-commitment --file go.mod # Go direct + indirect
65
- npx proof-of-commitment --file go.sum # Go full transitive set
66
-
67
- # Short alias
68
- npx poc axios zod chalk
47
+ # Or scan a go.mod / go.sum file directly:
48
+ npx proof-of-commitment --file go.mod
49
+ npx proof-of-commitment --file go.sum # full transitive set
69
50
  ```
70
51
 
71
- **Go modules note:** Go has no centralized download counter and no publisher registry. Scoring is GitHub-primary: longevity, release cadence, GitHub contributor count, OpenSSF Scorecard, and stars (popularity proxy). The "publishers" column in Go output shows GitHub push-access contributors — the closest equivalent to npm publishers.
52
+ **Web demo (no install):** [getcommit.dev/audit](https://getcommit.dev/audit) paste your packages, see risk scores in seconds.
72
53
 
73
- ## Zero-install MCP server (for Claude, Cursor, Windsurf)
54
+ **MCP server (zero install):**
74
55
 
75
- Add to your AI tool's config:
76
56
  ```json
77
57
  {
78
58
  "mcpServers": {
@@ -84,20 +64,236 @@ Add to your AI tool's config:
84
64
  }
85
65
  ```
86
66
 
87
- Then ask: *"Audit the dependencies in my package.json"* or *"What's the risk profile of vercel/ai?"*
67
+ Add to Claude Desktop, Cursor, Windsurf, or any MCP-compatible AI tool. Then ask:
68
+
69
+ > "Audit my package.json for supply chain risk"
70
+ > "Score axios, zod, chalk, lodash — which is highest risk?"
71
+ > "Is vercel/ai actively maintained?"
88
72
 
89
73
  ## GitHub Action
90
74
 
91
- Posts audit results directly on your PR:
75
+ Add supply chain auditing to any CI pipeline in 30 seconds — auto-detects packages from `package.json` or `requirements.txt`, **posts results as a PR comment**, writes to GitHub Step Summary, and optionally fails on CRITICAL packages.
76
+
77
+ Use the dedicated action at [piiiico/commit-action](https://github.com/piiiico/commit-action):
78
+
92
79
  ```yaml
93
- - uses: piiiico/proof-of-commitment@main
94
- with:
95
- fail-on-critical: false
96
- comment-on-pr: true
80
+ # .github/workflows/supply-chain.yml
81
+ name: Supply Chain Audit
82
+ on:
83
+ pull_request:
84
+ paths: ['package.json', 'package-lock.json', 'bun.lock']
85
+
86
+ jobs:
87
+ audit:
88
+ runs-on: ubuntu-latest
89
+ permissions:
90
+ pull-requests: write
91
+ steps:
92
+ - uses: actions/checkout@v4
93
+ - uses: piiiico/commit-action@v1
94
+ with:
95
+ fail-on-critical: true # blocks merges on CRITICAL packages
96
+ comment-on-pr: true # posts results as a PR comment
97
+ ```
98
+
99
+ When `comment-on-pr: true` (default), the action automatically posts the audit table as a comment on the pull request — and **updates the same comment** on re-run, so you don't get comment spam. Reviewers see the risk table without leaving the PR.
100
+
101
+ **Inputs:**
102
+
103
+ | Input | Default | Description |
104
+ |-------|---------|-------------|
105
+ | `packages` | _(auto)_ | Comma-separated package names (auto-detected from `package.json`/`requirements.txt` if not set) |
106
+ | `packages-file` | _(auto)_ | Path to `package.json` or `requirements.txt` (default: auto-detect in workspace root) |
107
+ | `fail-on-critical` | `true` | Fail the workflow if CRITICAL packages are found |
108
+ | `max-packages` | `20` | Max packages to audit when auto-detecting |
109
+ | `include-dev-dependencies` | `false` | Include `devDependencies` from `package.json` |
110
+ | `comment-on-pr` | `true` | Post audit results as a PR comment (requires `pull-requests: write` permission) |
111
+ | `api-key` | _(none)_ | [Commit Pro](https://getcommit.dev/pricing) API key — enables batch requests and 10K requests/month |
112
+ | `api-url` | _(prod)_ | Override API endpoint (useful for self-hosting) |
113
+
114
+ **Outputs:** `has-critical`, `critical-count`, `audit-summary` (markdown table, also written to Step Summary).
115
+
116
+ **Free vs Pro:** Without an API key, packages are audited one at a time (with delays to respect rate limits). With a Pro API key, all packages are audited in a single batch request — faster and with higher monthly limits.
117
+
118
+ Example PR comment / Step Summary output:
119
+
120
+ ```
121
+ | Package | Risk | Score | Publishers | Downloads/wk | Age |
122
+ |---------|-------------|-------|------------|--------------|-------|
123
+ | chalk | 🔴 CRITICAL | 75 | 1 | 380M | 12.7y |
124
+ | zod | 🔴 CRITICAL | 83 | 1 | 133M | 6.1y |
125
+ | axios | 🔴 CRITICAL | 89 | 1 | 93M | 11.6y |
126
+ ```
127
+
128
+ ## README Badges
129
+
130
+ Add a Commit Trust badge to any npm package you maintain or depend on:
131
+
132
+ ```markdown
133
+ ![Commit Trust](https://poc-backend.amdal-dev.workers.dev/badge/YOUR-PACKAGE)
134
+ ```
135
+
136
+ Examples:
137
+
138
+ | Package | Badge URL |
139
+ |---------|-----------|
140
+ | chalk | `![Commit Trust](https://poc-backend.amdal-dev.workers.dev/badge/chalk)` |
141
+ | react | `![Commit Trust](https://poc-backend.amdal-dev.workers.dev/badge/react)` |
142
+ | express | `![Commit Trust](https://poc-backend.amdal-dev.workers.dev/badge/express)` |
143
+ | @babel/core | `![Commit Trust](https://poc-backend.amdal-dev.workers.dev/badge/@babel/core)` |
144
+
145
+ Grades: 🟢 OK (75+) · 🟠 WARNING (40–74) · 🔴 CRITICAL (<40 or sole npm publisher with 10M+ weekly downloads)
146
+
147
+ Badges are cached 1 hour. No API key needed.
148
+
149
+ Also supports PyPI, Cargo, Go modules, and the full ecosystem-specific format:
150
+
151
+ ```markdown
152
+ ![commit score](https://poc-backend.amdal-dev.workers.dev/api/badge/npm/YOUR-PACKAGE)
153
+ ![commit score](https://poc-backend.amdal-dev.workers.dev/api/badge/pypi/YOUR-PACKAGE)
154
+ ![commit score](https://poc-backend.amdal-dev.workers.dev/api/badge/cargo/YOUR-CRATE)
155
+ ![commit score](https://poc-backend.amdal-dev.workers.dev/api/badge/golang/github.com/owner/repo)
97
156
  ```
98
157
 
99
- ## Links
158
+ ## REST API
100
159
 
101
- - **Web demo:** https://getcommit.dev/audit
102
- - **Live watchlist:** https://getcommit.dev/watchlist (top 25 npm packages by structural risk)
103
- - **GitHub:** https://github.com/piiiico/proof-of-commitment
160
+ No API key. No install.
161
+
162
+ ```bash
163
+ curl https://poc-backend.amdal-dev.workers.dev/api/audit \
164
+ -X POST \
165
+ -H "Content-Type: application/json" \
166
+ -d '{"packages": ["axios", "zod", "chalk", "lodash", "express"]}'
167
+ ```
168
+
169
+ ```json
170
+ {
171
+ "count": 5,
172
+ "results": [
173
+ {
174
+ "name": "chalk",
175
+ "ecosystem": "npm",
176
+ "score": 75,
177
+ "maintainers": 1,
178
+ "weeklyDownloads": 398397580,
179
+ "ageYears": 12.7,
180
+ "trend": "stable",
181
+ "riskFlags": ["CRITICAL"],
182
+ "scorecardScore": 3.6, // null if no GitHub repo
183
+ "hasDangerousWorkflow": false // null if no Scorecard data
184
+ },
185
+ ...
186
+ ]
187
+ }
188
+ ```
189
+
190
+ ## 9 MCP tools
191
+
192
+ | Tool | Description |
193
+ |------|-------------|
194
+ | `audit_dependencies` | Batch risk audit for up to 20 npm/PyPI/Cargo/Go packages |
195
+ | `lookup_npm_package` | Single npm package behavioral profile |
196
+ | `lookup_pypi_package` | Single PyPI package behavioral profile |
197
+ | `lookup_cargo_crate` | Single Rust crate behavioral profile (crates.io) |
198
+ | `lookup_go_module` | Single Go module behavioral profile (proxy.golang.org + GitHub) |
199
+ | `lookup_github_repo` | GitHub repo commitment score (longevity, commit frequency, contributor depth) |
200
+ | `lookup_business` | Norwegian business register — operating years, employees, financials |
201
+ | `lookup_business_by_org` | Same, by org number |
202
+ | `query_commitment` | Browser extension behavioral data (unique verified visitors, repeat rate) |
203
+
204
+ ## What the score measures
205
+
206
+ Each package is scored 0–100 across:
207
+
208
+ - **Longevity** — How long has the package existed? Abandoned packages get reactivated for attacks.
209
+ - **Publisher depth** — Single npm publisher + millions of weekly downloads = the attack surface LiteLLM exploited. (Publisher = person with npm publish access, distinct from GitHub contributors.)
210
+ - **Release consistency** — Regular releases signal active oversight. Long gaps = vulnerability accumulation.
211
+ - **Download trend** — Growing packages attract more scrutiny (and attacks). Stable = lower profile.
212
+ - **OpenSSF Scorecard** — Process security (code review enforcement, branch protection, CI/CD safety). Separate from behavioral signals. High Scorecard ≠ safe from credential theft attacks.
213
+
214
+ > Both axios (8.1/10 Scorecard) and chalk (3.6/10 Scorecard) score CRITICAL on behavioral signals. They measure different attack surfaces — Scorecard catches process gaps, behavioral signals catch publisher concentration.
215
+
216
+ **Risk flags:**
217
+ - `CRITICAL` — single npm publisher + >10M weekly downloads (exact LiteLLM/axios attack profile)
218
+ - `HIGH` — package <1yr old + rapid adoption
219
+ - `WARN` — no release in 12+ months
220
+
221
+ ## Real data points
222
+
223
+ ```
224
+ # packages you know about:
225
+ chalk — score 75, 1 publisher, 413M/week ⚑ CRITICAL
226
+ zod — score 86, 1 publisher, 163M/week ⚑ CRITICAL (30+ GitHub contributors)
227
+ lodash — score 81, 1 publisher, 145M/week ⚑ CRITICAL
228
+ axios — score 86, 1 publisher, 99M/week ⚑ CRITICAL (attacked Mar 30 2026)
229
+ express — score 90, 5 publishers, 95M/week
230
+
231
+ # packages probably not in your package.json, definitely in your lock file:
232
+ minimatch — score 78, 1 publisher, 562M/week ⚑ CRITICAL
233
+ glob — score 80, 1 publisher, 333M/week ⚑ CRITICAL
234
+ cross-spawn — score 72, 1 publisher, 190M/week ⚑ CRITICAL
235
+
236
+ # post-attack:
237
+ litellm — score 74, 1 publisher ⚑ CRITICAL (supply chain attack Mar 2026)
238
+
239
+ # Rust crates (new in v1.3.0):
240
+ serde — score 78, 1 owner, 13M/week ⚑ CRITICAL (dtolnay sole owner)
241
+ tokio — score 89, 2 owners, 10M/week
242
+ reqwest — score 85, 1 owner, 8M/week ⚑ HIGH
243
+ ```
244
+
245
+ ## Why behavioral signals
246
+
247
+ The LiteLLM attack (March 2026) and axios attack (March 30, 2026) followed the same pattern: stolen credentials → malicious package pushed → 97M+ machines exposed. Both packages scored CRITICAL by these metrics *before* the attacks.
248
+
249
+ Declarative signals (stars, README quality, CI badges) don't capture this risk. Behavioral commitment does.
250
+
251
+ ## Stack
252
+
253
+ | Layer | Technology |
254
+ |-------|-----------|
255
+ | Backend | Cloudflare Workers + D1 |
256
+ | MCP | Model Context Protocol SDK |
257
+ | Data | npm registry, PyPI, crates.io, proxy.golang.org, deps.dev, GitHub API, Brønnøysund (NO) |
258
+ | Landing | Astro + Cloudflare Pages |
259
+
260
+ ## Roadmap
261
+
262
+ Planned, not promised. The project is early-stage — contributions welcome on any of these.
263
+
264
+ | Feature | Status | Notes |
265
+ |---------|--------|-------|
266
+ | **Cargo (Rust) registry support** | ✅ Live | MCP tool, REST API, badge endpoint — `ecosystem: "cargo"` |
267
+ | **Go modules support** | ✅ Live | proxy.golang.org + deps.dev + GitHub-primary scoring — `ecosystem: "golang"` |
268
+ | **Score breakdown visualization** | Planned | Chart component for the 5 dimensions on getcommit.dev/audit |
269
+ | **`--json` flag for CLI** | ✅ Live | `npx proof-of-commitment --file package-lock.json --json \| jq '.criticalCount'` |
270
+ | **pnpm workspace monorepo support** | ✅ Live | `--file pnpm-workspace.yaml` or auto-detected from `pnpm-lock.yaml` |
271
+ | **Historical score tracking** | Planned | Trend charts — was this package getting riskier over time? |
272
+ | **Org-level dashboards** | Planned | Aggregate risk view across all repos in a GitHub org |
273
+
274
+ See [open issues](https://github.com/piiiico/proof-of-commitment/issues) for things you can help with today.
275
+
276
+ ## The broader vision
277
+
278
+ Supply chain auditing is the first tool. The underlying primitive is a **commitment graph** — behavioral signals that replace content-based trust across any domain.
279
+
280
+ When content is free to fake (reviews, stars, READMEs), commitment becomes the signal. A publisher who has shipped 847 releases over 12 years is a different kind of commitment than one who published once in 2023.
281
+
282
+ The same logic applies to websites, businesses, and AI agents. Two card networks have independently named this gap: Mastercard Verifiable Intent §9.2 explicitly lists behavioral trust as "not covered." Visa TAP identifies agents without answering whether to trust them.
283
+
284
+ Proof of Commitment is the trust layer they're pointing at.
285
+
286
+ → [getcommit.dev](https://getcommit.dev)
287
+
288
+ ## Run locally
289
+
290
+ ```bash
291
+ bun install
292
+ bun run dev:backend # local server with SQLite
293
+ bun run test:e2e # E2E test with mock World ID
294
+ ```
295
+
296
+ Deploy:
297
+ ```bash
298
+ bun run deploy # deploys to Cloudflare Workers
299
+ ```
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * proof-of-commitment CLI
4
- * Scores npm/PyPI packages on behavioral commitment signals.
3
+ * proof-of-commitment CLI v1.7.0
4
+ * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
7
7
 
@@ -20,6 +20,7 @@ const c = {
20
20
  white: '\x1b[37m',
21
21
  bgRed: '\x1b[41m',
22
22
  bgYellow: '\x1b[43m',
23
+ magenta: '\x1b[35m',
23
24
  };
24
25
 
25
26
  const NO_COLOR = process.env.NO_COLOR || !process.stdout.isTTY;
@@ -29,15 +30,20 @@ function clr(code, text) {
29
30
  return `${code}${text}${c.reset}`;
30
31
  }
31
32
 
33
+ /** Check if riskFlags array contains a CRITICAL-level flag (handles both "CRITICAL" and "CRITICAL: ..." formats) */
34
+ function hasCritical(flags) {
35
+ return flags && flags.some(f => typeof f === 'string' && f.startsWith('CRITICAL'));
36
+ }
37
+
32
38
  function riskColor(flags, score) {
33
- if (flags && flags.includes('CRITICAL')) return c.red + c.bold;
39
+ if (hasCritical(flags)) return c.red + c.bold;
34
40
  if (score < 40) return c.yellow + c.bold;
35
41
  if (score < 60) return c.yellow;
36
42
  return c.green;
37
43
  }
38
44
 
39
45
  function riskLabel(flags, score) {
40
- if (flags && flags.includes('CRITICAL')) return '🔴 CRITICAL';
46
+ if (hasCritical(flags)) return '🔴 CRITICAL';
41
47
  if (score < 40) return '🟠 HIGH';
42
48
  if (score < 60) return '🟡 MODERATE';
43
49
  if (score < 75) return '🟡 GOOD';
@@ -57,20 +63,28 @@ function padEnd(str, len) {
57
63
  }
58
64
 
59
65
  function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
66
+ const isNpm = !results[0] || results[0].ecosystem !== 'golang';
60
67
  const COL = {
61
- name: 20, risk: 14, score: 7, maintainers: 12, downloads: 12, age: 8,
68
+ name: 20, risk: 14, score: 7, maintainers: 12, downloads: 12, age: 8, provenance: 10,
62
69
  };
63
70
 
64
- const header = [
71
+ const headerParts = [
65
72
  padEnd(clr(c.bold, 'Package'), COL.name),
66
73
  padEnd(clr(c.bold, 'Risk'), COL.risk),
67
74
  padEnd(clr(c.bold, 'Score'), COL.score),
68
75
  padEnd(clr(c.bold, 'Publishers'), COL.maintainers),
69
76
  padEnd(clr(c.bold, 'Downloads'), COL.downloads),
70
77
  padEnd(clr(c.bold, 'Age'), COL.age),
71
- ].join(' ');
78
+ ];
72
79
 
73
- const divider = '─'.repeat(COL.name + COL.risk + COL.score + COL.maintainers + COL.downloads + COL.age + 10);
80
+ // Show Provenance column for npm packages
81
+ if (isNpm) {
82
+ headerParts.push(padEnd(clr(c.bold, 'Provenance'), COL.provenance));
83
+ }
84
+
85
+ const header = headerParts.join(' ');
86
+ const divWidth = COL.name + COL.risk + COL.score + COL.maintainers + COL.downloads + COL.age + (isNpm ? COL.provenance + 2 : 0) + 10;
87
+ const divider = '─'.repeat(divWidth);
74
88
 
75
89
  console.log('\n' + divider);
76
90
  if (lockfile && totalScanned && results.length < totalScanned) {
@@ -81,41 +95,46 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
81
95
  console.log(divider);
82
96
 
83
97
  let criticalInDisplay = 0;
98
+ let provenanceCount = 0;
84
99
 
85
100
  for (const pkg of results) {
86
101
  const rc = riskColor(pkg.riskFlags, pkg.score);
87
102
  const label = riskLabel(pkg.riskFlags, pkg.score);
88
- if (pkg.riskFlags && pkg.riskFlags.includes('CRITICAL')) criticalInDisplay++;
103
+ if (hasCritical(pkg.riskFlags)) criticalInDisplay++;
104
+ if (pkg.hasProvenance) provenanceCount++;
89
105
 
90
- // Go modules have no download data — show "—" instead. The maintainers
91
- // column for Go shows GitHub contributor count (the closest equivalent
92
- // to publish access since Go has no centralized publisher concept).
106
+ // Go modules have no download data
93
107
  const isGo = pkg.ecosystem === 'golang';
94
- const dlDisplay = isGo
95
- ? ''
96
- : fmtDownloads(pkg.weeklyDownloads || 0);
97
- const maintDisplay = pkg.maintainers === 35
98
- ? '30+'
99
- : String(pkg.maintainers || '?');
100
-
101
- const row = [
108
+ const dlDisplay = isGo ? '—' : fmtDownloads(pkg.weeklyDownloads || 0);
109
+ const maintDisplay = pkg.maintainers === 35 ? '30+' : String(pkg.maintainers || '?');
110
+
111
+ // Provenance indicator
112
+ const provDisplay = pkg.hasProvenance
113
+ ? clr(c.green, '🔐 verified')
114
+ : clr(c.dim, '—');
115
+
116
+ const rowParts = [
102
117
  padEnd(pkg.name, COL.name),
103
118
  padEnd(clr(rc, label), COL.risk),
104
119
  padEnd(String(pkg.score), COL.score),
105
120
  padEnd(maintDisplay, COL.maintainers),
106
121
  padEnd(dlDisplay, COL.downloads),
107
122
  padEnd((pkg.ageYears || '?').toString().replace(/(\.\d).*/, '$1') + 'y', COL.age),
108
- ].join(' ');
123
+ ];
124
+
125
+ if (isNpm) {
126
+ rowParts.push(padEnd(provDisplay, COL.provenance));
127
+ }
109
128
 
110
- console.log(row);
129
+ console.log(rowParts.join(' '));
111
130
 
112
131
  // Show GitHub contributor context for CRITICAL packages with active communities
113
- if (pkg.riskFlags && pkg.riskFlags.includes('CRITICAL') && pkg.githubContributors && pkg.githubContributors > 1) {
132
+ if (hasCritical(pkg.riskFlags) && pkg.githubContributors && pkg.githubContributors > 1) {
114
133
  const ghCount = pkg.githubContributors === 35 ? '30+' : pkg.githubContributors;
115
134
  console.log(clr(c.dim, ` ↳ ${ghCount} GitHub contributors — publish-access concentration risk despite active community`));
116
135
  }
117
136
 
118
- // Score breakdown if available — keys differ across ecosystems
137
+ // Score breakdown if available
119
138
  if (pkg.scoreBreakdown) {
120
139
  const b = pkg.scoreBreakdown;
121
140
  const breakdown = isGo
@@ -125,7 +144,8 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
125
144
  )
126
145
  : clr(c.dim,
127
146
  ` └ longevity=${b.longevity} momentum=${b.downloadMomentum} ` +
128
- `releases=${b.releaseConsistency} publishers=${b.maintainerDepth} github=${b.githubBacking}`
147
+ `releases=${b.releaseConsistency} publishers=${b.maintainerDepth} github=${b.githubBacking}` +
148
+ (b.trustedPublishing ? ` provenance=${b.trustedPublishing}` : '')
129
149
  );
130
150
  console.log(breakdown);
131
151
  }
@@ -138,20 +158,24 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
138
158
  const suffix = totalScanned ? ` (in ${totalScanned} packages scanned)` : '';
139
159
  console.log('\n' + clr(c.red + c.bold, `⚠ ${effectiveCritical} CRITICAL package${effectiveCritical > 1 ? 's' : ''} found${suffix}.`));
140
160
  console.log(clr(c.dim, ' CRITICAL = sole npm publisher + >10M weekly downloads (publish-access concentration risk)'));
161
+ if (provenanceCount > 0 && provenanceCount < results.length) {
162
+ console.log(clr(c.cyan, ` 🔐 ${provenanceCount}/${results.length} use Trusted Publishing (OIDC provenance) — partial mitigation`));
163
+ }
141
164
  } else {
142
165
  const suffix = totalScanned ? ` (${totalScanned} packages scanned)` : '';
143
166
  console.log('\n' + clr(c.green, `✓ No CRITICAL packages found${suffix}.`));
144
167
  }
145
168
 
146
- // Always show the web URL with top critical packages first
169
+ // Footer with web link + CI integration CTA
147
170
  const topPkgs = results.slice(0, 10).map(r => r.name).join(',');
148
- console.log(clr(c.dim, `\n🔗 Web view: ${WEB}?packages=${encodeURIComponent(topPkgs)}`));
171
+ console.log(clr(c.cyan, `\n 🔗 Full report: ${WEB}?packages=${encodeURIComponent(topPkgs)}`));
172
+ console.log(clr(c.cyan, ` 🤖 GitHub Action: github.com/piiiico/commit-action — block CRITICAL packages in CI`));
149
173
  console.log();
150
174
  }
151
175
 
152
176
  function printHelp() {
153
177
  console.log(`
154
- ${clr(c.bold, 'proof-of-commitment')} — supply chain risk scorer
178
+ ${clr(c.bold, 'proof-of-commitment')} v1.7.0 — supply chain risk scorer
155
179
 
156
180
  ${clr(c.bold, 'Usage:')}
157
181
  npx proof-of-commitment [packages...] Score npm packages
@@ -184,39 +208,42 @@ ${clr(c.bold, 'Examples:')}
184
208
  npx proof-of-commitment axios chalk --json | jq '.criticalCount'
185
209
 
186
210
  ${clr(c.bold, 'Score meaning:')}
187
- 🔴 CRITICAL Sole npm publisher + >10M downloads/wk (publish-access concentration risk)
211
+ 🔴 CRITICAL Sole publisher + >10M downloads/wk (publish-access concentration risk)
188
212
  🟠 HIGH Score < 40
189
213
  🟡 MODERATE Score 40–59
190
214
  🟡 GOOD Score 60–74
191
215
  🟢 HEALTHY Score 75+
192
216
 
193
- ${clr(c.bold, 'Score dimensions (npm/PyPI/Cargo):')} longevity · download momentum · release consistency · publisher depth · GitHub backing
194
- ${clr(c.bold, 'Score dimensions (Go):')} longevity · release consistency · maintainer depth · GitHub backing · stars (Go has no centralized download counter)
217
+ ${clr(c.bold, 'Provenance (npm):')}
218
+ 🔐 verified Package uses Trusted Publishing (OIDC provenance from CI not a human credential)
219
+ — No provenance attestation detected
220
+
221
+ ${clr(c.bold, 'Score dimensions (npm/PyPI/Cargo):')} longevity · download momentum · release consistency · publisher depth · GitHub backing · provenance
222
+ ${clr(c.bold, 'Score dimensions (Go):')} longevity · release consistency · maintainer depth · GitHub backing · stars
223
+
224
+ ${clr(c.bold, 'CI integration:')}
225
+ GitHub Action: ${clr(c.cyan, 'github.com/piiiico/commit-action')} — fails PRs on CRITICAL packages
226
+ MCP server: Add to Claude Desktop / Cursor for AI-assisted auditing
195
227
 
196
228
  ${clr(c.bold, 'Web:')} ${WEB}
197
- ${clr(c.bold, 'MCP:')} ${clr(c.dim, 'Add to Claude Desktop / Cursor for AI-assisted auditing')}
198
229
  `);
199
230
  }
200
231
 
201
232
  /**
202
233
  * Parse package-lock.json (npm lockfileVersion 2 or 3)
203
- * Returns all package names found in the lock file.
204
234
  */
205
235
  function parseLockNpm(content) {
206
236
  const lock = JSON.parse(content);
207
237
  const pkgs = new Set();
208
238
 
209
239
  if (lock.packages) {
210
- // lockfileVersion 2+: keys are "node_modules/pkg" or "node_modules/@scope/pkg"
211
240
  for (const key of Object.keys(lock.packages)) {
212
- if (!key || key === '') continue; // root package
213
- // Strip "node_modules/" prefix, handle nested paths like "node_modules/foo/node_modules/bar"
241
+ if (!key || key === '') continue;
214
242
  const parts = key.split('node_modules/');
215
243
  const pkgPath = parts[parts.length - 1];
216
244
  if (pkgPath) pkgs.add(pkgPath);
217
245
  }
218
246
  } else if (lock.dependencies) {
219
- // lockfileVersion 1: flat dependencies object
220
247
  for (const name of Object.keys(lock.dependencies)) {
221
248
  pkgs.add(name);
222
249
  }
@@ -227,11 +254,9 @@ function parseLockNpm(content) {
227
254
 
228
255
  /**
229
256
  * Parse yarn.lock (v1 format)
230
- * Returns all unique package names.
231
257
  */
232
258
  function parseLockYarn(content) {
233
259
  const pkgs = new Set();
234
- // Each block starts with "name@version:" or "name@range1, name@range2:"
235
260
  const headerRe = /^"?(@?[^@\s"]+)@/gm;
236
261
  let match;
237
262
  while ((match = headerRe.exec(content)) !== null) {
@@ -242,11 +267,9 @@ function parseLockYarn(content) {
242
267
 
243
268
  /**
244
269
  * Parse pnpm-lock.yaml (v6+)
245
- * Returns all unique package names.
246
270
  */
247
271
  function parseLockPnpm(content) {
248
272
  const pkgs = new Set();
249
- // packages section has entries like " /chalk@5.3.0:" or " chalk@5.3.0:"
250
273
  const pkgRe = /^\s+\/?(@?[^@\s/]+(?:\/[^@\s]+)?)@/gm;
251
274
  let match;
252
275
  while ((match = pkgRe.exec(content)) !== null) {
@@ -255,12 +278,74 @@ function parseLockPnpm(content) {
255
278
  return [...pkgs];
256
279
  }
257
280
 
281
+ /**
282
+ * Parse pnpm-workspace.yaml — find all workspace packages and aggregate their deps.
283
+ * Expects format:
284
+ * packages:
285
+ * - "packages/*"
286
+ * - "apps/*"
287
+ */
288
+ async function parsePnpmWorkspace(content, filePath) {
289
+ const fs = await import('fs');
290
+ const path = await import('path');
291
+ const dir = path.dirname(filePath);
292
+ const pkgs = new Set();
293
+
294
+ // Extract glob patterns from YAML
295
+ const patterns = [];
296
+ const lines = content.split('\n');
297
+ let inPackages = false;
298
+ for (const raw of lines) {
299
+ const line = raw.trim();
300
+ if (line === 'packages:') { inPackages = true; continue; }
301
+ if (inPackages && line.startsWith('-')) {
302
+ const pattern = line.replace(/^-\s*["']?/, '').replace(/["']?\s*$/, '');
303
+ patterns.push(pattern);
304
+ } else if (inPackages && !line.startsWith('#') && line !== '') {
305
+ break;
306
+ }
307
+ }
308
+
309
+ // For each pattern, look for package.json files
310
+ for (const pattern of patterns) {
311
+ const globDir = path.join(dir, pattern.replace('/*', '').replace('/**', ''));
312
+ try {
313
+ const entries = fs.readdirSync(globDir, { withFileTypes: true });
314
+ for (const entry of entries) {
315
+ if (!entry.isDirectory()) continue;
316
+ const pkgJsonPath = path.join(globDir, entry.name, 'package.json');
317
+ try {
318
+ const pkgContent = fs.readFileSync(pkgJsonPath, 'utf-8');
319
+ const pkg = JSON.parse(pkgContent);
320
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
321
+ for (const name of Object.keys(deps)) pkgs.add(name);
322
+ } catch {}
323
+ }
324
+ } catch {}
325
+ }
326
+
327
+ // Also check root package.json
328
+ try {
329
+ const rootPkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
330
+ const deps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
331
+ for (const name of Object.keys(deps)) pkgs.add(name);
332
+ } catch {}
333
+
334
+ return [...pkgs];
335
+ }
336
+
258
337
  async function readPackagesFromFile(filePath) {
259
338
  const fs = await import('fs');
260
339
  const path = await import('path');
261
340
  const content = fs.readFileSync(filePath, 'utf-8');
262
341
  const basename = path.basename(filePath).toLowerCase();
263
342
 
343
+ // pnpm-workspace.yaml
344
+ if (basename === 'pnpm-workspace.yaml' || basename === 'pnpm-workspace.yml') {
345
+ const pkgs = await parsePnpmWorkspace(content, filePath);
346
+ return { packages: pkgs, ecosystem: 'npm', lockfile: false, totalInFile: pkgs.length };
347
+ }
348
+
264
349
  // package-lock.json
265
350
  if (basename === 'package-lock.json') {
266
351
  const pkgs = parseLockNpm(content);
@@ -302,30 +387,27 @@ async function readPackagesFromFile(filePath) {
302
387
 
303
388
  // Cargo.toml
304
389
  if (basename === 'cargo.toml') {
305
- // Crude TOML parse — extract [dependencies] section keys
306
390
  const pkgs = parseCargoToml(content);
307
391
  return { packages: pkgs, ecosystem: 'cargo', lockfile: false };
308
392
  }
309
393
 
310
- // go.mod — Go module file
394
+ // go.mod
311
395
  if (basename === 'go.mod') {
312
396
  const pkgs = parseGoMod(content);
313
397
  return { packages: pkgs, ecosystem: 'golang', lockfile: false, totalInFile: pkgs.length };
314
398
  }
315
399
 
316
- // go.sum — Go module checksum file (lockfile-equivalent — captures full transitive set)
400
+ // go.sum
317
401
  if (basename === 'go.sum') {
318
402
  const pkgs = parseGoSum(content);
319
403
  return { packages: pkgs, ecosystem: 'golang', lockfile: true, totalInFile: pkgs.length };
320
404
  }
321
405
 
322
- throw new Error(`Unsupported file: ${basename}. Supported: package.json, package-lock.json, yarn.lock, pnpm-lock.yaml, requirements.txt, Cargo.toml, go.mod, go.sum`);
406
+ throw new Error(`Unsupported file: ${basename}. Supported: package.json, package-lock.json, yarn.lock, pnpm-lock.yaml, pnpm-workspace.yaml, requirements.txt, Cargo.toml, go.mod, go.sum`);
323
407
  }
324
408
 
325
409
  /**
326
- * Parse a Cargo.toml — extract direct deps from [dependencies] / [dev-dependencies].
327
- * Crude — only handles the common "name = version" / "name = { ... }" lines under
328
- * [dependencies] / [dev-dependencies]. Doesn't expand workspace members.
410
+ * Parse Cargo.toml
329
411
  */
330
412
  function parseCargoToml(content) {
331
413
  const pkgs = new Set();
@@ -345,13 +427,7 @@ function parseCargoToml(content) {
345
427
  }
346
428
 
347
429
  /**
348
- * Parse a go.mod file — extract direct + indirect dependencies from require blocks.
349
- * Format:
350
- * require (
351
- * github.com/gin-gonic/gin v1.9.1
352
- * golang.org/x/net v0.20.0 // indirect
353
- * )
354
- * require github.com/foo/bar v1.0.0
430
+ * Parse go.mod
355
431
  */
356
432
  function parseGoMod(content) {
357
433
  const pkgs = new Set();
@@ -362,7 +438,6 @@ function parseGoMod(content) {
362
438
  const line = raw.trim();
363
439
  if (!line || line.startsWith('//')) continue;
364
440
 
365
- // Block-form require: "require ("
366
441
  if (/^require\s*\(\s*$/.test(line)) {
367
442
  inRequireBlock = true;
368
443
  continue;
@@ -372,14 +447,12 @@ function parseGoMod(content) {
372
447
  continue;
373
448
  }
374
449
 
375
- // Inside a block: "<modulepath> <version>" optionally followed by comments
376
450
  if (inRequireBlock) {
377
451
  const match = line.match(/^([^\s]+)\s+v[^\s]+/);
378
452
  if (match) pkgs.add(match[1]);
379
453
  continue;
380
454
  }
381
455
 
382
- // Single-line require: "require <modulepath> <version>"
383
456
  const single = line.match(/^require\s+([^\s]+)\s+v[^\s]+/);
384
457
  if (single) pkgs.add(single[1]);
385
458
  }
@@ -388,8 +461,7 @@ function parseGoMod(content) {
388
461
  }
389
462
 
390
463
  /**
391
- * Parse a go.sum file — module path is the first column, version + suffix follow.
392
- * Each module appears multiple times (once per artifact); dedupe.
464
+ * Parse go.sum
393
465
  */
394
466
  function parseGoSum(content) {
395
467
  const pkgs = new Set();
@@ -404,7 +476,6 @@ function parseGoSum(content) {
404
476
 
405
477
  /**
406
478
  * Audit packages in batches of 20, in parallel.
407
- * Returns all results sorted by risk score (highest risk first).
408
479
  */
409
480
  async function auditBatched(packages, ecosystem, { onProgress } = {}) {
410
481
  const BATCH_SIZE = 20;
@@ -434,10 +505,10 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
434
505
 
435
506
  const all = results.flat();
436
507
 
437
- // Sort: CRITICAL first, then by score ascending (lower score = higher risk)
508
+ // Sort: CRITICAL first, then by score ascending
438
509
  all.sort((a, b) => {
439
- const aCrit = a.riskFlags?.includes('CRITICAL') ? 1 : 0;
440
- const bCrit = b.riskFlags?.includes('CRITICAL') ? 1 : 0;
510
+ const aCrit = hasCritical(a.riskFlags) ? 1 : 0;
511
+ const bCrit = hasCritical(b.riskFlags) ? 1 : 0;
441
512
  if (aCrit !== bCrit) return bCrit - aCrit;
442
513
  return (a.score || 100) - (b.score || 100);
443
514
  });
@@ -503,7 +574,6 @@ async function main() {
503
574
  let allResults;
504
575
 
505
576
  if (packages.length <= 20) {
506
- // Single batch — existing behavior
507
577
  if (!jsonOutput) process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
508
578
 
509
579
  try {
@@ -527,7 +597,6 @@ async function main() {
527
597
  if (!jsonOutput) process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n`));
528
598
 
529
599
  } else {
530
- // Multi-batch for lock files
531
600
  const batches = Math.ceil(packages.length / 20);
532
601
  if (!jsonOutput) process.stdout.write(clr(c.dim, `Scanning ${packages.length} packages (${batches} batches in parallel)...`));
533
602
 
@@ -550,28 +619,29 @@ async function main() {
550
619
  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
551
620
  if (!jsonOutput) process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n`));
552
621
 
553
- // JSON output: emit all results with summary
554
622
  if (jsonOutput) {
555
- const criticalCount = allResults.filter(r => r.riskFlags?.includes('CRITICAL')).length;
623
+ const criticalCount = allResults.filter(r => hasCritical(r.riskFlags)).length;
624
+ const provenanceCount = allResults.filter(r => r.hasProvenance).length;
556
625
  console.log(JSON.stringify({
557
626
  totalScanned: allResults.length,
558
627
  criticalCount,
628
+ provenanceCount,
559
629
  results: allResults,
560
630
  }, null, 2));
561
631
  process.exit(criticalCount > 0 ? 1 : 0);
562
632
  }
563
633
 
564
- // For lock files: show top 25 highest-risk packages
634
+ // Lock files: show top 25 highest-risk
565
635
  const MAX_DISPLAY = 25;
566
636
  const displayed = allResults.slice(0, MAX_DISPLAY);
567
- const criticalTotal = allResults.filter(r => r.riskFlags?.includes('CRITICAL')).length;
637
+ const criticalTotal = allResults.filter(r => hasCritical(r.riskFlags)).length;
568
638
  printTable(displayed, { totalScanned: allResults.length, totalCritical: criticalTotal, lockfile: true });
569
639
  return;
570
640
  }
571
641
 
572
642
  if (!allResults || allResults.length === 0) {
573
643
  if (jsonOutput) {
574
- console.log(JSON.stringify({ totalScanned: 0, criticalCount: 0, results: [] }, null, 2));
644
+ console.log(JSON.stringify({ totalScanned: 0, criticalCount: 0, provenanceCount: 0, results: [] }, null, 2));
575
645
  } else {
576
646
  console.log('No results returned. Check package names and try again.');
577
647
  }
@@ -579,10 +649,12 @@ async function main() {
579
649
  }
580
650
 
581
651
  if (jsonOutput) {
582
- const criticalCount = allResults.filter(r => r.riskFlags?.includes('CRITICAL')).length;
652
+ const criticalCount = allResults.filter(r => hasCritical(r.riskFlags)).length;
653
+ const provenanceCount = allResults.filter(r => r.hasProvenance).length;
583
654
  console.log(JSON.stringify({
584
655
  totalScanned: allResults.length,
585
656
  criticalCount,
657
+ provenanceCount,
586
658
  results: allResults,
587
659
  }, null, 2));
588
660
  process.exit(criticalCount > 0 ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Supply chain risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,9 @@
28
28
  "behavioral",
29
29
  "commitment",
30
30
  "maintainer",
31
- "publisher"
31
+ "publisher",
32
+ "provenance",
33
+ "trusted-publishing"
32
34
  ],
33
35
  "author": "piiiico",
34
36
  "license": "MIT",