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.
- package/LICENSE +21 -0
- package/index.js +111 -12
- 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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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:')}
|
|
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:
|
|
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:
|
|
1376
|
+
headers: JSON_API_HEADERS,
|
|
1288
1377
|
body: JSON.stringify({ packages, ecosystem }),
|
|
1289
1378
|
});
|
|
1290
|
-
if (!res.ok)
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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",
|