proof-of-commitment 1.14.0 → 1.18.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.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/index.js +130 -19
- 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/README.md
CHANGED
|
@@ -56,7 +56,7 @@ npx proof-of-commitment --file go.sum # full transitive set
|
|
|
56
56
|
# Install once, then use the `poc` alias:
|
|
57
57
|
npm install -g proof-of-commitment
|
|
58
58
|
|
|
59
|
-
# Get a free API key at https://getcommit.dev/get-started, then:
|
|
59
|
+
# Get a free API key at https://getcommit.dev/get-started?utm_source=cli, then:
|
|
60
60
|
poc login sk_commit_your_key_here
|
|
61
61
|
# ✓ Authenticated — Tier: Free — Usage: 0/200 requests (daily)
|
|
62
62
|
|
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.18.0
|
|
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'));
|
|
@@ -197,10 +279,10 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
197
279
|
}
|
|
198
280
|
}
|
|
199
281
|
|
|
200
|
-
// Contextual upsell — show when findings make monitoring relevant
|
|
201
|
-
// In TTY mode, inlineSignup() handles the upsell interactively — skip static text
|
|
282
|
+
// Contextual upsell — show when findings make monitoring relevant.
|
|
283
|
+
// In TTY mode, inlineSignup() handles the CRITICAL/risky upsell interactively — skip static text there.
|
|
284
|
+
const hasKey = !!process.env.COMMIT_API_KEY || _cachedHasKey;
|
|
202
285
|
if (effectiveCritical > 0) {
|
|
203
|
-
const hasKey = !!process.env.COMMIT_API_KEY || _cachedHasKey;
|
|
204
286
|
if (hasKey) {
|
|
205
287
|
console.log(clr(c.dim, `\n 📊 Monitor ${effectiveCritical === 1 ? 'this package' : 'these packages'}: `) +
|
|
206
288
|
clr(c.cyan, `poc watch ${results.find(r => hasCritical(r.riskFlags))?.name || results[0]?.name}`));
|
|
@@ -211,6 +293,19 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
211
293
|
console.log(clr(c.dim, ' Then run: ') + clr(c.cyan, 'poc login'));
|
|
212
294
|
}
|
|
213
295
|
// else: TTY mode — inlineSignup() will prompt interactively after printTable
|
|
296
|
+
} else if (!hasKey) {
|
|
297
|
+
// HEALTHY case + no saved key: soft watchlist CTA. The all-healthy
|
|
298
|
+
// footer previously surfaced only CI-shaped CTAs (Action, `poc init`)
|
|
299
|
+
// which both require active commitment — workflow change + repo edit.
|
|
300
|
+
// The lowest-friction conversion (email → API key → watchlist) was
|
|
301
|
+
// hidden behind the CRITICAL gate of inlineSignup(). Buyer-journey
|
|
302
|
+
// dogfood 2026-05-24 found 1472 weekly downloads → 0 organic signups;
|
|
303
|
+
// the watchlist value prop ("alert me when these degrade") is real
|
|
304
|
+
// for healthy packages too — that's exactly when monitoring matters.
|
|
305
|
+
// ref=audit-baseline distinguishes this funnel from audit-cli-429
|
|
306
|
+
// (rate-limit rescue) and from the static utm_source=cli help-line.
|
|
307
|
+
console.log(clr(c.dim, '\n 📊 Lock in this baseline — get alerted if any package degrades:'));
|
|
308
|
+
console.log(clr(c.dim, ' ') + clr(c.cyan, 'https://getcommit.dev/get-started?ref=audit-baseline&utm_source=cli') + clr(c.dim, ' (free, no card, 10s)'));
|
|
214
309
|
}
|
|
215
310
|
console.log();
|
|
216
311
|
}
|
|
@@ -221,16 +316,19 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
221
316
|
* into a single CLI prompt.
|
|
222
317
|
*/
|
|
223
318
|
async function inlineSignup(results) {
|
|
224
|
-
// Only prompt in interactive TTY when
|
|
319
|
+
// Only prompt in interactive TTY when findings make monitoring relevant and no key saved
|
|
225
320
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
226
321
|
const hasKey = !!process.env.COMMIT_API_KEY || _cachedHasKey;
|
|
227
322
|
if (hasKey) return;
|
|
228
323
|
const critPkgs = results.filter(r => hasCritical(r.riskFlags));
|
|
229
|
-
|
|
324
|
+
const lowScorePkgs = results.filter(r => typeof r.score === 'number' && r.score < 60);
|
|
325
|
+
// Gate: ≥1 CRITICAL, OR ≥2 packages with score<60, OR large scan (≥50 packages)
|
|
326
|
+
const shouldPrompt = critPkgs.length >= 1 || lowScorePkgs.length >= 2 || results.length >= 50;
|
|
327
|
+
if (!shouldPrompt) return;
|
|
230
328
|
|
|
231
329
|
console.log(clr(c.dim, ' ─────────────────────────────────────────────'));
|
|
232
|
-
console.log(clr(c.bold, ' 🔔 Get alerts
|
|
233
|
-
console.log(clr(c.dim, ' Free
|
|
330
|
+
console.log(clr(c.bold, ' 🔔 Lock in this audit. Get alerts if these packages get worse.'));
|
|
331
|
+
console.log(clr(c.dim, ' Free, no card, 10 seconds. Saves to ~/.commit/config.\n'));
|
|
234
332
|
|
|
235
333
|
const { createInterface } = await import('readline');
|
|
236
334
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -286,7 +384,7 @@ async function inlineSignup(results) {
|
|
|
286
384
|
|
|
287
385
|
function printHelp() {
|
|
288
386
|
console.log(`
|
|
289
|
-
${clr(c.bold, 'proof-of-commitment')} v1.
|
|
387
|
+
${clr(c.bold, 'proof-of-commitment')} v1.18.0 — supply chain risk scorer
|
|
290
388
|
|
|
291
389
|
${clr(c.bold, 'Usage:')}
|
|
292
390
|
npx proof-of-commitment Auto-detect manifest in current dir
|
|
@@ -323,7 +421,7 @@ ${clr(c.bold, 'Monitoring (Pro):')}
|
|
|
323
421
|
poc watchlist List monitored packages with current scores + risk
|
|
324
422
|
poc unwatch <pkg> Remove a package from monitoring
|
|
325
423
|
|
|
326
|
-
Get a free key: https://getcommit.dev/get-started
|
|
424
|
+
Get a free key: https://getcommit.dev/get-started?utm_source=cli
|
|
327
425
|
Upgrade to Pro: https://getcommit.dev/pricing
|
|
328
426
|
|
|
329
427
|
${clr(c.bold, 'Options:')}
|
|
@@ -699,18 +797,21 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
|
|
|
699
797
|
}
|
|
700
798
|
|
|
701
799
|
let completed = 0;
|
|
800
|
+
let batchedCta = null;
|
|
702
801
|
const results = await Promise.all(
|
|
703
802
|
batches.map(async (batch) => {
|
|
704
803
|
const res = await fetch(API, {
|
|
705
804
|
method: 'POST',
|
|
706
|
-
headers:
|
|
805
|
+
headers: JSON_API_HEADERS,
|
|
707
806
|
body: JSON.stringify({ packages: batch, ecosystem }),
|
|
708
807
|
});
|
|
709
808
|
if (!res.ok) {
|
|
809
|
+
if (res.status === 429) await handle429(res);
|
|
710
810
|
const text = await res.text();
|
|
711
811
|
throw new Error(`API error ${res.status}: ${text}`);
|
|
712
812
|
}
|
|
713
813
|
const data = await res.json();
|
|
814
|
+
if (data._cta) batchedCta = data._cta;
|
|
714
815
|
completed += batch.length;
|
|
715
816
|
if (onProgress) onProgress(completed, packages.length);
|
|
716
817
|
return data.results || [];
|
|
@@ -727,7 +828,7 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
|
|
|
727
828
|
return (a.score || 100) - (b.score || 100);
|
|
728
829
|
});
|
|
729
830
|
|
|
730
|
-
return all;
|
|
831
|
+
return { results: all, _cta: batchedCta };
|
|
731
832
|
}
|
|
732
833
|
|
|
733
834
|
/** Parse --fail-on=<level>. Returns one of 'critical' | 'risky' | 'none'. */
|
|
@@ -842,7 +943,7 @@ async function cmdLogin(keyArg) {
|
|
|
842
943
|
|
|
843
944
|
if (!key || !key.startsWith('sk_commit_')) {
|
|
844
945
|
console.error(clr(c.red, '\n Invalid API key format. Keys start with sk_commit_'));
|
|
845
|
-
console.error(clr(c.dim, ' Get one at https://getcommit.dev/get-started\n'));
|
|
946
|
+
console.error(clr(c.dim, ' Get one at https://getcommit.dev/get-started?utm_source=cli\n'));
|
|
846
947
|
process.exit(1);
|
|
847
948
|
}
|
|
848
949
|
|
|
@@ -884,7 +985,7 @@ async function cmdStatus() {
|
|
|
884
985
|
if (!key) {
|
|
885
986
|
console.log(clr(c.dim, '\n Not logged in.'));
|
|
886
987
|
console.log(clr(c.dim, ' Run ') + clr(c.cyan, 'poc login') + clr(c.dim, ' to authenticate.'));
|
|
887
|
-
console.log(clr(c.dim, ' Get a free key at https://getcommit.dev/get-started\n'));
|
|
988
|
+
console.log(clr(c.dim, ' Get a free key at https://getcommit.dev/get-started?utm_source=cli\n'));
|
|
888
989
|
return;
|
|
889
990
|
}
|
|
890
991
|
|
|
@@ -1285,14 +1386,17 @@ async function cmdReport(packages, ecosystem, { filePath, isLockfile, totalScann
|
|
|
1285
1386
|
if (packages.length <= 20) {
|
|
1286
1387
|
const res = await fetch(API, {
|
|
1287
1388
|
method: 'POST',
|
|
1288
|
-
headers:
|
|
1389
|
+
headers: JSON_API_HEADERS,
|
|
1289
1390
|
body: JSON.stringify({ packages, ecosystem }),
|
|
1290
1391
|
});
|
|
1291
|
-
if (!res.ok)
|
|
1392
|
+
if (!res.ok) {
|
|
1393
|
+
if (res.status === 429) await handle429(res);
|
|
1394
|
+
throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
1395
|
+
}
|
|
1292
1396
|
const data = await res.json();
|
|
1293
1397
|
allResults = data.results || [];
|
|
1294
1398
|
} else {
|
|
1295
|
-
allResults = await auditBatched(packages, ecosystem);
|
|
1399
|
+
allResults = (await auditBatched(packages, ecosystem)).results;
|
|
1296
1400
|
}
|
|
1297
1401
|
} catch (err) {
|
|
1298
1402
|
console.error(`\nError: ${err.message}`);
|
|
@@ -1674,6 +1778,7 @@ async function main() {
|
|
|
1674
1778
|
const t0 = Date.now();
|
|
1675
1779
|
|
|
1676
1780
|
let allResults;
|
|
1781
|
+
let apiCta = null;
|
|
1677
1782
|
|
|
1678
1783
|
if (packages.length <= 20) {
|
|
1679
1784
|
if (!jsonOutput) process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
|
|
@@ -1681,15 +1786,17 @@ async function main() {
|
|
|
1681
1786
|
try {
|
|
1682
1787
|
const res = await fetch(API, {
|
|
1683
1788
|
method: 'POST',
|
|
1684
|
-
headers:
|
|
1789
|
+
headers: JSON_API_HEADERS,
|
|
1685
1790
|
body: JSON.stringify({ packages, ecosystem }),
|
|
1686
1791
|
});
|
|
1687
1792
|
if (!res.ok) {
|
|
1793
|
+
if (res.status === 429) await handle429(res);
|
|
1688
1794
|
const text = await res.text();
|
|
1689
1795
|
throw new Error(`API error ${res.status}: ${text}`);
|
|
1690
1796
|
}
|
|
1691
1797
|
const data = await res.json();
|
|
1692
1798
|
allResults = data.results || [];
|
|
1799
|
+
apiCta = data._cta || null;
|
|
1693
1800
|
} catch (err) {
|
|
1694
1801
|
console.error(`\nError: ${err.message}`);
|
|
1695
1802
|
process.exit(1);
|
|
@@ -1704,7 +1811,7 @@ async function main() {
|
|
|
1704
1811
|
|
|
1705
1812
|
let lastPct = 0;
|
|
1706
1813
|
try {
|
|
1707
|
-
|
|
1814
|
+
const batchResult = await auditBatched(packages, ecosystem, {
|
|
1708
1815
|
onProgress: (done, total) => {
|
|
1709
1816
|
const pct = Math.round((done / total) * 100);
|
|
1710
1817
|
if (pct >= lastPct + 20) {
|
|
@@ -1713,6 +1820,8 @@ async function main() {
|
|
|
1713
1820
|
}
|
|
1714
1821
|
}
|
|
1715
1822
|
});
|
|
1823
|
+
allResults = batchResult.results;
|
|
1824
|
+
apiCta = batchResult._cta;
|
|
1716
1825
|
} catch (err) {
|
|
1717
1826
|
console.error(`\nError: ${err.message}`);
|
|
1718
1827
|
process.exit(1);
|
|
@@ -1739,6 +1848,7 @@ async function main() {
|
|
|
1739
1848
|
const displayed = allResults.slice(0, MAX_DISPLAY);
|
|
1740
1849
|
const criticalTotal = allResults.filter(r => hasCritical(r.riskFlags)).length;
|
|
1741
1850
|
printTable(displayed, { totalScanned: allResults.length, totalCritical: criticalTotal, lockfile: true });
|
|
1851
|
+
if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
|
|
1742
1852
|
await inlineSignup(displayed);
|
|
1743
1853
|
if (shouldFail(allResults, failOn)) {
|
|
1744
1854
|
console.error(clr(c.red + c.bold, `\n✗ --fail-on=${failOn} threshold met. Exit 1.`));
|
|
@@ -1770,6 +1880,7 @@ async function main() {
|
|
|
1770
1880
|
}
|
|
1771
1881
|
|
|
1772
1882
|
printTable(allResults);
|
|
1883
|
+
if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
|
|
1773
1884
|
await inlineSignup(allResults);
|
|
1774
1885
|
if (shouldFail(allResults, failOn)) {
|
|
1775
1886
|
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.18.0",
|
|
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",
|