proof-of-commitment 1.14.0 → 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 +109 -11
  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.14.0
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.14.0 — 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
@@ -699,18 +784,21 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
699
784
  }
700
785
 
701
786
  let completed = 0;
787
+ let batchedCta = null;
702
788
  const results = await Promise.all(
703
789
  batches.map(async (batch) => {
704
790
  const res = await fetch(API, {
705
791
  method: 'POST',
706
- headers: { 'Content-Type': 'application/json' },
792
+ headers: JSON_API_HEADERS,
707
793
  body: JSON.stringify({ packages: batch, ecosystem }),
708
794
  });
709
795
  if (!res.ok) {
796
+ if (res.status === 429) await handle429(res);
710
797
  const text = await res.text();
711
798
  throw new Error(`API error ${res.status}: ${text}`);
712
799
  }
713
800
  const data = await res.json();
801
+ if (data._cta) batchedCta = data._cta;
714
802
  completed += batch.length;
715
803
  if (onProgress) onProgress(completed, packages.length);
716
804
  return data.results || [];
@@ -727,7 +815,7 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
727
815
  return (a.score || 100) - (b.score || 100);
728
816
  });
729
817
 
730
- return all;
818
+ return { results: all, _cta: batchedCta };
731
819
  }
732
820
 
733
821
  /** Parse --fail-on=<level>. Returns one of 'critical' | 'risky' | 'none'. */
@@ -1285,14 +1373,17 @@ async function cmdReport(packages, ecosystem, { filePath, isLockfile, totalScann
1285
1373
  if (packages.length <= 20) {
1286
1374
  const res = await fetch(API, {
1287
1375
  method: 'POST',
1288
- headers: { 'Content-Type': 'application/json' },
1376
+ headers: JSON_API_HEADERS,
1289
1377
  body: JSON.stringify({ packages, ecosystem }),
1290
1378
  });
1291
- 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
+ }
1292
1383
  const data = await res.json();
1293
1384
  allResults = data.results || [];
1294
1385
  } else {
1295
- allResults = await auditBatched(packages, ecosystem);
1386
+ allResults = (await auditBatched(packages, ecosystem)).results;
1296
1387
  }
1297
1388
  } catch (err) {
1298
1389
  console.error(`\nError: ${err.message}`);
@@ -1674,6 +1765,7 @@ async function main() {
1674
1765
  const t0 = Date.now();
1675
1766
 
1676
1767
  let allResults;
1768
+ let apiCta = null;
1677
1769
 
1678
1770
  if (packages.length <= 20) {
1679
1771
  if (!jsonOutput) process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
@@ -1681,15 +1773,17 @@ async function main() {
1681
1773
  try {
1682
1774
  const res = await fetch(API, {
1683
1775
  method: 'POST',
1684
- headers: { 'Content-Type': 'application/json' },
1776
+ headers: JSON_API_HEADERS,
1685
1777
  body: JSON.stringify({ packages, ecosystem }),
1686
1778
  });
1687
1779
  if (!res.ok) {
1780
+ if (res.status === 429) await handle429(res);
1688
1781
  const text = await res.text();
1689
1782
  throw new Error(`API error ${res.status}: ${text}`);
1690
1783
  }
1691
1784
  const data = await res.json();
1692
1785
  allResults = data.results || [];
1786
+ apiCta = data._cta || null;
1693
1787
  } catch (err) {
1694
1788
  console.error(`\nError: ${err.message}`);
1695
1789
  process.exit(1);
@@ -1704,7 +1798,7 @@ async function main() {
1704
1798
 
1705
1799
  let lastPct = 0;
1706
1800
  try {
1707
- allResults = await auditBatched(packages, ecosystem, {
1801
+ const batchResult = await auditBatched(packages, ecosystem, {
1708
1802
  onProgress: (done, total) => {
1709
1803
  const pct = Math.round((done / total) * 100);
1710
1804
  if (pct >= lastPct + 20) {
@@ -1713,6 +1807,8 @@ async function main() {
1713
1807
  }
1714
1808
  }
1715
1809
  });
1810
+ allResults = batchResult.results;
1811
+ apiCta = batchResult._cta;
1716
1812
  } catch (err) {
1717
1813
  console.error(`\nError: ${err.message}`);
1718
1814
  process.exit(1);
@@ -1739,6 +1835,7 @@ async function main() {
1739
1835
  const displayed = allResults.slice(0, MAX_DISPLAY);
1740
1836
  const criticalTotal = allResults.filter(r => hasCritical(r.riskFlags)).length;
1741
1837
  printTable(displayed, { totalScanned: allResults.length, totalCritical: criticalTotal, lockfile: true });
1838
+ if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
1742
1839
  await inlineSignup(displayed);
1743
1840
  if (shouldFail(allResults, failOn)) {
1744
1841
  console.error(clr(c.red + c.bold, `\n✗ --fail-on=${failOn} threshold met. Exit 1.`));
@@ -1770,6 +1867,7 @@ async function main() {
1770
1867
  }
1771
1868
 
1772
1869
  printTable(allResults);
1870
+ if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
1773
1871
  await inlineSignup(allResults);
1774
1872
  if (shouldFail(allResults, failOn)) {
1775
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.14.0",
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",