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.
- package/LICENSE +21 -0
- package/index.js +109 -11
- 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
|
|
@@ -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:
|
|
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:
|
|
1376
|
+
headers: JSON_API_HEADERS,
|
|
1289
1377
|
body: JSON.stringify({ packages, ecosystem }),
|
|
1290
1378
|
});
|
|
1291
|
-
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
|
+
}
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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",
|