proof-of-commitment 1.33.0 → 1.34.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/index.js +130 -29
- package/package.json +1 -1
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.34.0
|
|
4
4
|
* Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
|
|
5
5
|
* Usage: npx proof-of-commitment [packages...] [options]
|
|
6
6
|
*/
|
|
@@ -90,14 +90,49 @@ async function handle429(res) {
|
|
|
90
90
|
// Non-JSON fallback — leave data as {}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const partial = Array.isArray(data.packages_already_scored)
|
|
94
|
+
? data.packages_already_scored
|
|
95
|
+
: [];
|
|
96
|
+
|
|
97
|
+
// Forward-compat: if backend returns partial scoring on 429,
|
|
98
|
+
// print what we have BEFORE the rescue message. Falls back to JSON
|
|
99
|
+
// dump if the row shape isn't a complete table row.
|
|
100
|
+
// (auditBatched aggregates across batches and prints its own table — it
|
|
101
|
+
// calls renderRescueCta() directly, skipping this block.)
|
|
102
|
+
if (partial.length > 0) {
|
|
103
|
+
try {
|
|
104
|
+
console.log();
|
|
105
|
+
console.log(clr(c.dim, ` Partial results scored before the limit hit (${partial.length}):`));
|
|
106
|
+
printTable(partial, { totalScanned: partial.length });
|
|
107
|
+
} catch {
|
|
108
|
+
console.log(JSON.stringify(partial, null, 2));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await renderRescueCta(data);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render the rate-limit rescue CTA from a parsed 429 response body.
|
|
117
|
+
* Separated from handle429() so auditBatched can aggregate partials across
|
|
118
|
+
* 17 parallel batches, print ONE combined table, then surface ONE rescue
|
|
119
|
+
* CTA — instead of letting the first batch to land kill the process with
|
|
120
|
+
* only 3 of N*3 partials visible (where N = #batches hitting overshoot).
|
|
121
|
+
*
|
|
122
|
+
* Exit semantics preserved: this function ALWAYS process.exit(1) at the
|
|
123
|
+
* tail, so any caller that's reached the rescue CTA leaves the program.
|
|
124
|
+
*
|
|
125
|
+
* The shape mirrors `data` from a 429 JSON response:
|
|
126
|
+
* { message?, instant_key_url?, upgrade_url?, retry_after_seconds?,
|
|
127
|
+
* retry_after?, overshoot?, tier_suggestion?, upgrade?, tier?,
|
|
128
|
+
* shared_ip_hint? }
|
|
129
|
+
*/
|
|
130
|
+
async function renderRescueCta(data) {
|
|
93
131
|
const message = data.message || 'Daily free audit limit reached on this network IP.';
|
|
94
132
|
const instantKeyUrl =
|
|
95
133
|
data.instant_key_url ||
|
|
96
134
|
data.upgrade_url ||
|
|
97
135
|
'https://getcommit.dev/get-started?ref=audit-cli-429';
|
|
98
|
-
const partial = Array.isArray(data.packages_already_scored)
|
|
99
|
-
? data.packages_already_scored
|
|
100
|
-
: [];
|
|
101
136
|
// Authenticated keys: retry_after (seconds, used by worker auth-middleware quota path).
|
|
102
137
|
// Anonymous IPs: retry_after_seconds (legacy / overshoot rescue path).
|
|
103
138
|
// Read both so both paths surface a reset-time hint.
|
|
@@ -134,19 +169,6 @@ async function handle429(res) {
|
|
|
134
169
|
typeof data.tier === 'string' &&
|
|
135
170
|
data.tier !== 'anonymous';
|
|
136
171
|
|
|
137
|
-
// Forward-compat: if backend ever returns partial scoring on 429,
|
|
138
|
-
// print what we have BEFORE the rescue message. Falls back to JSON
|
|
139
|
-
// dump if the row shape isn't a complete table row.
|
|
140
|
-
if (partial.length > 0) {
|
|
141
|
-
try {
|
|
142
|
-
console.log();
|
|
143
|
-
console.log(clr(c.dim, ` Partial results scored before the limit hit (${partial.length}):`));
|
|
144
|
-
printTable(partial, { totalScanned: partial.length });
|
|
145
|
-
} catch {
|
|
146
|
-
console.log(JSON.stringify(partial, null, 2));
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
172
|
console.error('');
|
|
151
173
|
console.error(clr(c.yellow + c.bold, `⚠ ${message}`));
|
|
152
174
|
if (data.shared_ip_hint) {
|
|
@@ -989,7 +1011,7 @@ async function inlineSignup(results, opts = {}) {
|
|
|
989
1011
|
|
|
990
1012
|
function printHelp() {
|
|
991
1013
|
console.log(`
|
|
992
|
-
${clr(c.bold, 'proof-of-commitment')} v1.
|
|
1014
|
+
${clr(c.bold, 'proof-of-commitment')} v1.34.0 — supply chain risk scorer
|
|
993
1015
|
|
|
994
1016
|
${clr(c.bold, 'Usage:')}
|
|
995
1017
|
npx proof-of-commitment Auto-detect manifest in current dir
|
|
@@ -1416,27 +1438,81 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
|
|
|
1416
1438
|
let batchedCta = null;
|
|
1417
1439
|
// Resolve auth once so all parallel batches share the same key lookup.
|
|
1418
1440
|
const headers = await auditHeaders();
|
|
1419
|
-
|
|
1441
|
+
|
|
1442
|
+
// Use Promise.allSettled so a single rate-limit hit on one batch doesn't
|
|
1443
|
+
// discard the work the other batches successfully completed. Pre-fix
|
|
1444
|
+
// (2026-06-16 dogfood): backend in overshoot returns 429 with
|
|
1445
|
+
// RATE_LIMIT_TASTE=3 packages_already_scored on EVERY parallel batch
|
|
1446
|
+
// (each batch increments the IP counter, all see auditCount > limit,
|
|
1447
|
+
// each gets trimmed to 3 + 429). The first batch's 429 to land called
|
|
1448
|
+
// handle429() which process.exit(1)'d — discarding the OTHER 16 batches'
|
|
1449
|
+
// 3-package partials (16×3=48 packages of scored value silently dropped).
|
|
1450
|
+
//
|
|
1451
|
+
// Post-fix: aggregate 200 results AND 429 partials across all batches.
|
|
1452
|
+
// User sees up to N×3 packages instead of 3 when fully overshoot, and
|
|
1453
|
+
// (16×20)+(1×3)=323 packages instead of 0 when limit hits mid-scan.
|
|
1454
|
+
// Rescue CTA is surfaced ONCE at the end via _rescue (handed to
|
|
1455
|
+
// renderRescueCta in the calling path), not per-batch via handle429.
|
|
1456
|
+
const settled = await Promise.allSettled(
|
|
1420
1457
|
batches.map(async (batch) => {
|
|
1421
1458
|
const res = await fetch(API, {
|
|
1422
1459
|
method: 'POST',
|
|
1423
1460
|
headers,
|
|
1424
1461
|
body: JSON.stringify({ packages: batch, ecosystem }),
|
|
1425
1462
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
const text = await res.text();
|
|
1429
|
-
throw new Error(`API error ${res.status}: ${text}`);
|
|
1430
|
-
}
|
|
1431
|
-
const data = await res.json();
|
|
1432
|
-
if (data._cta) batchedCta = data._cta;
|
|
1463
|
+
let data = {};
|
|
1464
|
+
try { data = await res.json(); } catch {}
|
|
1433
1465
|
completed += batch.length;
|
|
1434
1466
|
if (onProgress) onProgress(completed, packages.length);
|
|
1435
|
-
|
|
1467
|
+
if (res.ok) {
|
|
1468
|
+
return { kind: 'ok', data };
|
|
1469
|
+
}
|
|
1470
|
+
if (res.status === 429) {
|
|
1471
|
+
return { kind: '429', data };
|
|
1472
|
+
}
|
|
1473
|
+
return { kind: 'error', status: res.status, data };
|
|
1436
1474
|
})
|
|
1437
1475
|
);
|
|
1438
1476
|
|
|
1439
|
-
const
|
|
1477
|
+
const aggregated = [];
|
|
1478
|
+
let rescue = null;
|
|
1479
|
+
let nonRateErrorMsg = null;
|
|
1480
|
+
for (const r of settled) {
|
|
1481
|
+
if (r.status === 'rejected') {
|
|
1482
|
+
nonRateErrorMsg = nonRateErrorMsg || String(r.reason && r.reason.message || r.reason || 'unknown');
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
const v = r.value;
|
|
1486
|
+
if (v.kind === 'ok') {
|
|
1487
|
+
if (v.data._cta) batchedCta = v.data._cta;
|
|
1488
|
+
aggregated.push(...(v.data.results || []));
|
|
1489
|
+
} else if (v.kind === '429') {
|
|
1490
|
+
// First 429's payload carries the rescue metadata; later 429s'
|
|
1491
|
+
// metadata is interchangeable (same overshoot/upgrade context),
|
|
1492
|
+
// so we don't need to merge it.
|
|
1493
|
+
if (!rescue) rescue = v.data;
|
|
1494
|
+
aggregated.push(...(Array.isArray(v.data.packages_already_scored) ? v.data.packages_already_scored : []));
|
|
1495
|
+
} else if (v.kind === 'error') {
|
|
1496
|
+
const errMsg = (v.data && v.data.error) ? v.data.error : `API error ${v.status}`;
|
|
1497
|
+
nonRateErrorMsg = nonRateErrorMsg || errMsg;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Hard failure: zero packages scored AND a non-rate-limit error fired.
|
|
1502
|
+
// (If rescue is set but aggregated is empty, fall through — the caller
|
|
1503
|
+
// will render the rescue CTA and exit; no need to throw.)
|
|
1504
|
+
if (aggregated.length === 0 && !rescue && nonRateErrorMsg) {
|
|
1505
|
+
throw new Error(nonRateErrorMsg);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Dedup by name+ecosystem — defensive against any backend echoing.
|
|
1509
|
+
const seen = new Set();
|
|
1510
|
+
const all = aggregated.filter((p) => {
|
|
1511
|
+
const k = `${p && p.name}:${p && p.ecosystem}`;
|
|
1512
|
+
if (seen.has(k)) return false;
|
|
1513
|
+
seen.add(k);
|
|
1514
|
+
return true;
|
|
1515
|
+
});
|
|
1440
1516
|
|
|
1441
1517
|
// Sort: CRITICAL first, then by score ascending
|
|
1442
1518
|
all.sort((a, b) => {
|
|
@@ -1446,7 +1522,7 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
|
|
|
1446
1522
|
return (a.score || 100) - (b.score || 100);
|
|
1447
1523
|
});
|
|
1448
1524
|
|
|
1449
|
-
return { results: all, _cta: batchedCta };
|
|
1525
|
+
return { results: all, _cta: batchedCta, _rescue: rescue };
|
|
1450
1526
|
}
|
|
1451
1527
|
|
|
1452
1528
|
/** Parse --fail-on=<level>. Returns one of 'critical' | 'risky' | 'none'. */
|
|
@@ -2930,6 +3006,7 @@ async function main() {
|
|
|
2930
3006
|
if (!structuredOutput) process.stdout.write(clr(c.dim, `Scanning ${packages.length} packages (${batches} batches in parallel)...`));
|
|
2931
3007
|
|
|
2932
3008
|
let lastPct = 0;
|
|
3009
|
+
let batchRescue = null;
|
|
2933
3010
|
try {
|
|
2934
3011
|
const batchResult = await auditBatched(packages, ecosystem, {
|
|
2935
3012
|
onProgress: (done, total) => {
|
|
@@ -2942,6 +3019,7 @@ async function main() {
|
|
|
2942
3019
|
});
|
|
2943
3020
|
allResults = batchResult.results;
|
|
2944
3021
|
apiCta = batchResult._cta;
|
|
3022
|
+
batchRescue = batchResult._rescue || null;
|
|
2945
3023
|
} catch (err) {
|
|
2946
3024
|
console.error(`\nError: ${err.message}`);
|
|
2947
3025
|
process.exit(1);
|
|
@@ -2964,6 +3042,7 @@ async function main() {
|
|
|
2964
3042
|
criticalCount,
|
|
2965
3043
|
provenanceCount,
|
|
2966
3044
|
failOn,
|
|
3045
|
+
rateLimited: !!batchRescue,
|
|
2967
3046
|
results: allResults,
|
|
2968
3047
|
}, null, 2));
|
|
2969
3048
|
process.exit(shouldFail(allResults, failOn) ? 1 : 0);
|
|
@@ -2975,6 +3054,28 @@ async function main() {
|
|
|
2975
3054
|
const criticalTotal = allResults.filter(r => hasCritical(r.riskFlags)).length;
|
|
2976
3055
|
printTable(displayed, { totalScanned: allResults.length, totalCritical: criticalTotal, lockfile: true });
|
|
2977
3056
|
if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
|
|
3057
|
+
// Rate-limit aggregation (2026-06-16 fix): if ANY batch hit 429, the
|
|
3058
|
+
// aggregated table above ALREADY includes packages_already_scored from
|
|
3059
|
+
// every 429'd batch (auditBatched flattens 200 + 429 results). Surface
|
|
3060
|
+
// ONE rescue CTA here that ALSO short-circuits the normal inlineSignup
|
|
3061
|
+
// path — the rescue CTA itself runs the TTY email→key prompt for free
|
|
3062
|
+
// tier users (overshoot/keyUpgrade branches print upgrade URL and exit).
|
|
3063
|
+
// renderRescueCta() always process.exit(1)s at its tail, so this is the
|
|
3064
|
+
// terminal path for any rate-limited scan.
|
|
3065
|
+
if (batchRescue) {
|
|
3066
|
+
// Override the per-batch "Scored 3 of 20 packages" message with
|
|
3067
|
+
// an aggregate-aware one before rendering. Pre-fix: user sent 326,
|
|
3068
|
+
// saw a "3 of 20" rescue message that referenced one batch's slice.
|
|
3069
|
+
// Post-fix: reflect what the user actually saw in the aggregated table.
|
|
3070
|
+
const isOvershoot = batchRescue.overshoot === true || batchRescue.tier_suggestion === 'developer';
|
|
3071
|
+
const rescueWithAggregate = {
|
|
3072
|
+
...batchRescue,
|
|
3073
|
+
message: isOvershoot
|
|
3074
|
+
? `Scored ${allResults.length} of ${packages.length} packages — you're past the free-tier daily limit on this IP. A free key gives 200/day but a ${packages.length}-package lockfile would burn through it; Developer ($15/mo) gives 1,000/day + batch API.`
|
|
3075
|
+
: `Scored ${allResults.length} of ${packages.length} packages — free-tier daily limit reached on this IP (often shared via corporate NAT / CI / dev container). A free API key in 30 seconds lifts the limit to 200/day.`,
|
|
3076
|
+
};
|
|
3077
|
+
await renderRescueCta(rescueWithAggregate); // exits 1
|
|
3078
|
+
}
|
|
2978
3079
|
await inlineSignup(displayed, { engagementSignal: !!apiCta });
|
|
2979
3080
|
if (shouldFail(allResults, failOn)) {
|
|
2980
3081
|
console.error(clr(c.red + c.bold, `\n✗ --fail-on=${failOn} threshold met. Exit 1.`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proof-of-commitment",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"mcpName": "io.github.piiiico/proof-of-commitment",
|
|
5
5
|
"description": "Supply chain security risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
|
|
6
6
|
"type": "module",
|