proof-of-commitment 1.13.1 → 1.17.1

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/LICENSE +21 -0
  2. package/index.js +111 -12
  3. package/package.json +4 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Håkon Åmdal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * proof-of-commitment CLI v1.13.1
3
+ * proof-of-commitment CLI v1.17.1
4
4
  * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
@@ -10,6 +10,18 @@ const KEYS_API = 'https://poc-backend.amdal-dev.workers.dev/api/keys';
10
10
  const WATCHLIST_API = 'https://poc-backend.amdal-dev.workers.dev/api/watchlist';
11
11
  const WEB = 'https://getcommit.dev/audit';
12
12
 
13
+ // Backend uses Accept header to decide JSON vs plain-text body on 429
14
+ // (added 2026-05-22 so v1.14.0 CLI, which sends the fetch default `*/*`,
15
+ // gets a readable text body inside its Error wrapper instead of a JSON
16
+ // dump). v1.17.0+ explicitly opts into JSON so handle429() can parse
17
+ // shared_ip_hint, retry_after_seconds, etc. Default fetch in Node 20+
18
+ // sends `Accept: */*` — without this header the backend would assume
19
+ // the legacy raw-dump path.
20
+ const JSON_API_HEADERS = {
21
+ 'Content-Type': 'application/json',
22
+ 'Accept': 'application/json',
23
+ };
24
+
13
25
  // ANSI color helpers
14
26
  const c = {
15
27
  reset: '\x1b[0m',
@@ -42,6 +54,76 @@ function clr(code, text) {
42
54
  return `${code}${text}${c.reset}`;
43
55
  }
44
56
 
57
+ /**
58
+ * Renders a human-readable rate-limit message to stderr and exits with code 1.
59
+ * Parses JSON body from a 429 response.
60
+ *
61
+ * v1.17.0: reads structured `shared_ip_hint` / `instant_key_url` /
62
+ * `packages_already_scored` / `retry_after_seconds` fields (see /api/audit
63
+ * 429 response). Older backends just return `message` + `upgrade_url`;
64
+ * the helper degrades gracefully. Single CTA only — paid upgrade is removed
65
+ * here because dogfood (2026-05-21) found that splitting attention between
66
+ * "free key" and "paid upgrade" lowers free-key conversion on the rescue
67
+ * step. The user is hitting the *free* wall — surface the free fix.
68
+ */
69
+ async function handle429(res) {
70
+ let data = {};
71
+ try {
72
+ data = await res.json();
73
+ } catch {
74
+ // Non-JSON fallback — leave data as {}
75
+ }
76
+
77
+ const message = data.message || 'Daily free audit limit reached on this network IP.';
78
+ const instantKeyUrl =
79
+ data.instant_key_url ||
80
+ data.upgrade_url ||
81
+ 'https://getcommit.dev/get-started?ref=audit-cli-429';
82
+ const partial = Array.isArray(data.packages_already_scored)
83
+ ? data.packages_already_scored
84
+ : [];
85
+ const retryAfter = Number.isFinite(data.retry_after_seconds)
86
+ ? data.retry_after_seconds
87
+ : null;
88
+
89
+ // Forward-compat: if backend ever returns partial scoring on 429,
90
+ // print what we have BEFORE the rescue message. Falls back to JSON
91
+ // dump if the row shape isn't a complete table row.
92
+ if (partial.length > 0) {
93
+ try {
94
+ console.log();
95
+ console.log(clr(c.dim, ` Partial results scored before the limit hit (${partial.length}):`));
96
+ printTable(partial, { totalScanned: partial.length });
97
+ } catch {
98
+ console.log(JSON.stringify(partial, null, 2));
99
+ }
100
+ }
101
+
102
+ console.error('');
103
+ console.error(clr(c.yellow + c.bold, `⚠ ${message}`));
104
+ if (data.shared_ip_hint) {
105
+ console.error(
106
+ clr(
107
+ c.dim,
108
+ ' Heads up: corporate NAT, CI runners, and dev containers all share egress IPs,'
109
+ )
110
+ );
111
+ console.error(
112
+ clr(c.dim, ' so the free-tier counter ticks faster than your personal usage suggests.')
113
+ );
114
+ }
115
+ console.error('');
116
+ console.error(clr(c.cyan + c.bold, ` → Free API key in 30 seconds (no card): ${instantKeyUrl}`));
117
+ if (retryAfter && retryAfter > 0) {
118
+ const hours = Math.floor(retryAfter / 3600);
119
+ const mins = Math.floor((retryAfter % 3600) / 60);
120
+ const resetIn = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
121
+ console.error(clr(c.dim, ` or wait — free-tier resets in ${resetIn} (00:00 UTC).`));
122
+ }
123
+ console.error('');
124
+ process.exit(1);
125
+ }
126
+
45
127
  /** Check if riskFlags array contains a CRITICAL-level flag (handles both "CRITICAL" and "CRITICAL: ..." formats) */
46
128
  function hasCritical(flags) {
47
129
  return flags && flags.some(f => typeof f === 'string' && f.startsWith('CRITICAL'));
@@ -221,12 +303,15 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
221
303
  * into a single CLI prompt.
222
304
  */
223
305
  async function inlineSignup(results) {
224
- // Only prompt in interactive TTY when CRITICAL packages found and no key saved
306
+ // Only prompt in interactive TTY when findings make monitoring relevant and no key saved
225
307
  if (!process.stdin.isTTY || !process.stdout.isTTY) return;
226
308
  const hasKey = !!process.env.COMMIT_API_KEY || _cachedHasKey;
227
309
  if (hasKey) return;
228
310
  const critPkgs = results.filter(r => hasCritical(r.riskFlags));
229
- if (critPkgs.length === 0) return;
311
+ const lowScorePkgs = results.filter(r => typeof r.score === 'number' && r.score < 60);
312
+ // Gate: ≥1 CRITICAL, OR ≥2 packages with score<60, OR large scan (≥50 packages)
313
+ const shouldPrompt = critPkgs.length >= 1 || lowScorePkgs.length >= 2 || results.length >= 50;
314
+ if (!shouldPrompt) return;
230
315
 
231
316
  console.log(clr(c.dim, ' ─────────────────────────────────────────────'));
232
317
  console.log(clr(c.bold, ' 🔔 Get alerts when these scores change?'));
@@ -286,7 +371,7 @@ async function inlineSignup(results) {
286
371
 
287
372
  function printHelp() {
288
373
  console.log(`
289
- ${clr(c.bold, 'proof-of-commitment')} v1.13.1 — supply chain risk scorer
374
+ ${clr(c.bold, 'proof-of-commitment')} v1.16.0 — supply chain risk scorer
290
375
 
291
376
  ${clr(c.bold, 'Usage:')}
292
377
  npx proof-of-commitment Auto-detect manifest in current dir
@@ -388,7 +473,8 @@ ${clr(c.bold, 'Provenance (npm):')}
388
473
  ${clr(c.bold, 'Score dimensions (npm/PyPI/Cargo):')} longevity · download momentum · release consistency · publisher depth · GitHub backing · provenance
389
474
  ${clr(c.bold, 'Score dimensions (Go):')} longevity · release consistency · maintainer depth · GitHub backing · stars
390
475
 
391
- ${clr(c.bold, 'MCP:')} Add to Claude Desktop / Cursor for AI-assisted auditing — see homepage.
476
+ ${clr(c.bold, 'MCP:')} https://poc-backend.amdal-dev.workers.dev/mcp connect from Claude Desktop / Cursor / Cline.
477
+ Free tier: 100 queries/IP/UTC day. Power users: API key for 200/day. ${clr(c.dim, '(Authorization: Bearer sk_commit_…)')}
392
478
 
393
479
  ${clr(c.bold, 'Web:')} ${WEB}
394
480
  `);
@@ -698,18 +784,21 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
698
784
  }
699
785
 
700
786
  let completed = 0;
787
+ let batchedCta = null;
701
788
  const results = await Promise.all(
702
789
  batches.map(async (batch) => {
703
790
  const res = await fetch(API, {
704
791
  method: 'POST',
705
- headers: { 'Content-Type': 'application/json' },
792
+ headers: JSON_API_HEADERS,
706
793
  body: JSON.stringify({ packages: batch, ecosystem }),
707
794
  });
708
795
  if (!res.ok) {
796
+ if (res.status === 429) await handle429(res);
709
797
  const text = await res.text();
710
798
  throw new Error(`API error ${res.status}: ${text}`);
711
799
  }
712
800
  const data = await res.json();
801
+ if (data._cta) batchedCta = data._cta;
713
802
  completed += batch.length;
714
803
  if (onProgress) onProgress(completed, packages.length);
715
804
  return data.results || [];
@@ -726,7 +815,7 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
726
815
  return (a.score || 100) - (b.score || 100);
727
816
  });
728
817
 
729
- return all;
818
+ return { results: all, _cta: batchedCta };
730
819
  }
731
820
 
732
821
  /** Parse --fail-on=<level>. Returns one of 'critical' | 'risky' | 'none'. */
@@ -1284,14 +1373,17 @@ async function cmdReport(packages, ecosystem, { filePath, isLockfile, totalScann
1284
1373
  if (packages.length <= 20) {
1285
1374
  const res = await fetch(API, {
1286
1375
  method: 'POST',
1287
- headers: { 'Content-Type': 'application/json' },
1376
+ headers: JSON_API_HEADERS,
1288
1377
  body: JSON.stringify({ packages, ecosystem }),
1289
1378
  });
1290
- if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
1379
+ if (!res.ok) {
1380
+ if (res.status === 429) await handle429(res);
1381
+ throw new Error(`API error ${res.status}: ${await res.text()}`);
1382
+ }
1291
1383
  const data = await res.json();
1292
1384
  allResults = data.results || [];
1293
1385
  } else {
1294
- allResults = await auditBatched(packages, ecosystem);
1386
+ allResults = (await auditBatched(packages, ecosystem)).results;
1295
1387
  }
1296
1388
  } catch (err) {
1297
1389
  console.error(`\nError: ${err.message}`);
@@ -1673,6 +1765,7 @@ async function main() {
1673
1765
  const t0 = Date.now();
1674
1766
 
1675
1767
  let allResults;
1768
+ let apiCta = null;
1676
1769
 
1677
1770
  if (packages.length <= 20) {
1678
1771
  if (!jsonOutput) process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
@@ -1680,15 +1773,17 @@ async function main() {
1680
1773
  try {
1681
1774
  const res = await fetch(API, {
1682
1775
  method: 'POST',
1683
- headers: { 'Content-Type': 'application/json' },
1776
+ headers: JSON_API_HEADERS,
1684
1777
  body: JSON.stringify({ packages, ecosystem }),
1685
1778
  });
1686
1779
  if (!res.ok) {
1780
+ if (res.status === 429) await handle429(res);
1687
1781
  const text = await res.text();
1688
1782
  throw new Error(`API error ${res.status}: ${text}`);
1689
1783
  }
1690
1784
  const data = await res.json();
1691
1785
  allResults = data.results || [];
1786
+ apiCta = data._cta || null;
1692
1787
  } catch (err) {
1693
1788
  console.error(`\nError: ${err.message}`);
1694
1789
  process.exit(1);
@@ -1703,7 +1798,7 @@ async function main() {
1703
1798
 
1704
1799
  let lastPct = 0;
1705
1800
  try {
1706
- allResults = await auditBatched(packages, ecosystem, {
1801
+ const batchResult = await auditBatched(packages, ecosystem, {
1707
1802
  onProgress: (done, total) => {
1708
1803
  const pct = Math.round((done / total) * 100);
1709
1804
  if (pct >= lastPct + 20) {
@@ -1712,6 +1807,8 @@ async function main() {
1712
1807
  }
1713
1808
  }
1714
1809
  });
1810
+ allResults = batchResult.results;
1811
+ apiCta = batchResult._cta;
1715
1812
  } catch (err) {
1716
1813
  console.error(`\nError: ${err.message}`);
1717
1814
  process.exit(1);
@@ -1738,6 +1835,7 @@ async function main() {
1738
1835
  const displayed = allResults.slice(0, MAX_DISPLAY);
1739
1836
  const criticalTotal = allResults.filter(r => hasCritical(r.riskFlags)).length;
1740
1837
  printTable(displayed, { totalScanned: allResults.length, totalCritical: criticalTotal, lockfile: true });
1838
+ if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
1741
1839
  await inlineSignup(displayed);
1742
1840
  if (shouldFail(allResults, failOn)) {
1743
1841
  console.error(clr(c.red + c.bold, `\n✗ --fail-on=${failOn} threshold met. Exit 1.`));
@@ -1769,6 +1867,7 @@ async function main() {
1769
1867
  }
1770
1868
 
1771
1869
  printTable(allResults);
1870
+ if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
1772
1871
  await inlineSignup(allResults);
1773
1872
  if (shouldFail(allResults, failOn)) {
1774
1873
  console.error(clr(c.red + c.bold, `✗ --fail-on=${failOn} threshold met. Exit 1.`));
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.13.1",
3
+ "version": "1.17.1",
4
+ "mcpName": "io.github.piiiico/proof-of-commitment",
4
5
  "description": "Supply chain risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
5
6
  "type": "module",
6
7
  "bin": {
@@ -10,7 +11,8 @@
10
11
  "main": "./index.js",
11
12
  "files": [
12
13
  "index.js",
13
- "README.md"
14
+ "README.md",
15
+ "LICENSE"
14
16
  ],
15
17
  "keywords": [
16
18
  "supply-chain",