proof-of-commitment 1.27.0 → 1.29.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 -19
- 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.29.0
|
|
4
4
|
* Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
|
|
5
5
|
* Usage: npx proof-of-commitment [packages...] [options]
|
|
6
6
|
*/
|
|
@@ -649,7 +649,60 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
649
649
|
* intent. Copy adapts to context: degradation alerts (CRITICAL) vs baseline
|
|
650
650
|
* lock-in (healthy). Quick lookups (<3 packages) still skip the prompt.
|
|
651
651
|
*/
|
|
652
|
-
|
|
652
|
+
/**
|
|
653
|
+
* Build the top-3-by-risk-priority watch seeds for /api/keys/create body.watch.
|
|
654
|
+
*
|
|
655
|
+
* Mirrors the web-side buildWatchSeeds at
|
|
656
|
+
* commit-landing-v2/src/pages/audit.astro:1299 so the two signup paths
|
|
657
|
+
* (web audit form vs CLI inline-prompt) feed the backend with the same
|
|
658
|
+
* shape — backend then writes the user's default project's
|
|
659
|
+
* monitored_packages BEFORE the welcome email so step 1 names actual
|
|
660
|
+
* packages instead of hardcoded `poc watch express / lodash` examples.
|
|
661
|
+
*
|
|
662
|
+
* Priority: compromised > CRITICAL > HIGH > others (any flags) > clean.
|
|
663
|
+
* The free-tier cap (3) is enforced both here and again on the backend
|
|
664
|
+
* (PACKAGE_LIMITS.free) — defense in depth across client-server drift.
|
|
665
|
+
* Validates ecosystem against the backend ECOSYSTEMS set
|
|
666
|
+
* (npm/pypi/cargo/golang); unknown ecosystems fall back to npm because
|
|
667
|
+
* the backend rejects unknowns and we want to surface SOMETHING rather
|
|
668
|
+
* than nothing. Filters out names that fail npm's 214-char max and
|
|
669
|
+
* dedupes by (name, ecosystem).
|
|
670
|
+
*
|
|
671
|
+
* Closes the second proposition-gap layer (CLI-side mirror of the
|
|
672
|
+
* 2026-06-11 audit-page watchlist auto-seed at abe53f1/df8a8be).
|
|
673
|
+
*/
|
|
674
|
+
function buildCliWatchSeeds(results) {
|
|
675
|
+
if (!Array.isArray(results) || results.length === 0) return [];
|
|
676
|
+
const VALID_ECOS = new Set(['npm', 'pypi', 'cargo', 'golang']);
|
|
677
|
+
function priority(r) {
|
|
678
|
+
if (r.compromised) return 0;
|
|
679
|
+
if (Array.isArray(r.riskFlags) && r.riskFlags.some(f => typeof f === 'string' && f.startsWith('CRITICAL'))) return 1;
|
|
680
|
+
if (Array.isArray(r.riskFlags) && r.riskFlags.some(f => typeof f === 'string' && f.startsWith('HIGH'))) return 2;
|
|
681
|
+
if (Array.isArray(r.riskFlags) && r.riskFlags.length > 0) return 3;
|
|
682
|
+
return 4;
|
|
683
|
+
}
|
|
684
|
+
const filtered = results.filter(r => typeof r.name === 'string' && r.name.length > 0 && r.name.length < 215);
|
|
685
|
+
const sorted = filtered.slice().sort((a, b) => {
|
|
686
|
+
const pa = priority(a);
|
|
687
|
+
const pb = priority(b);
|
|
688
|
+
if (pa !== pb) return pa - pb;
|
|
689
|
+
return filtered.indexOf(a) - filtered.indexOf(b);
|
|
690
|
+
});
|
|
691
|
+
const seeds = [];
|
|
692
|
+
const seen = new Set();
|
|
693
|
+
for (const r of sorted) {
|
|
694
|
+
const rawEco = typeof r.ecosystem === 'string' ? r.ecosystem.toLowerCase() : 'npm';
|
|
695
|
+
const eco = VALID_ECOS.has(rawEco) ? rawEco : 'npm';
|
|
696
|
+
const key = `${r.name}|${eco}`;
|
|
697
|
+
if (seen.has(key)) continue;
|
|
698
|
+
seen.add(key);
|
|
699
|
+
seeds.push({ name: r.name, ecosystem: eco });
|
|
700
|
+
if (seeds.length >= 3) break; // free-tier cap (backend re-validates)
|
|
701
|
+
}
|
|
702
|
+
return seeds;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function inlineSignup(results, opts = {}) {
|
|
653
706
|
// Only prompt in interactive TTY when no key saved
|
|
654
707
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
655
708
|
const hasKey = !!process.env.COMMIT_API_KEY || _cachedHasKey;
|
|
@@ -657,20 +710,36 @@ async function inlineSignup(results) {
|
|
|
657
710
|
const critPkgs = results.filter(r => hasCritical(r.riskFlags));
|
|
658
711
|
const lowScorePkgs = results.filter(r => typeof r.score === 'number' && r.score < 60);
|
|
659
712
|
const hasFindings = critPkgs.length >= 1 || lowScorePkgs.length >= 2;
|
|
660
|
-
//
|
|
713
|
+
// engagementSignal: true when the backend returned an `_cta` field, which
|
|
714
|
+
// means this IP has scored ≥ AUDIT_SOFT_CTA_AT (5) packages today.
|
|
715
|
+
// That's a server-confirmed engagement signal independent of local result
|
|
716
|
+
// shape — the user is approaching the daily wall (15) and will hit it
|
|
717
|
+
// soon. Pre-this fix, single-package healthy scans at counts 5–14 saw
|
|
718
|
+
// only a dim URL: the strongest in-terminal conversion moment dropped to
|
|
719
|
+
// a copy-paste task. With engagementSignal=true we bypass the findings
|
|
720
|
+
// gate so the inline email→key prompt fires at the moment of warmest
|
|
721
|
+
// engagement. Closes the leak found 2026-06-10 dogfooding /api/keys/stats:
|
|
722
|
+
// 4 IPs hit AUDIT_SOFT_CTA_AT in 7d, 0 organic signups.
|
|
723
|
+
const engagementSignal = !!opts.engagementSignal;
|
|
724
|
+
// Gate: show prompt when there's something worth monitoring OR the user
|
|
725
|
+
// has demonstrated repeat-use engagement today (server _cta signal).
|
|
661
726
|
// Old gate (results.length < 3) blocked the most common entry point:
|
|
662
727
|
// `npx proof-of-commitment axios` after reading about an attack.
|
|
663
728
|
// A single CRITICAL result IS the high-intent moment — don't skip it.
|
|
664
|
-
// For healthy single-package checks with no findings
|
|
665
|
-
|
|
729
|
+
// For healthy single-package checks with no findings AND no engagement
|
|
730
|
+
// signal, still skip.
|
|
731
|
+
if (results.length < 3 && !hasFindings && !engagementSignal) return;
|
|
666
732
|
|
|
667
733
|
// Copy adapts to context. Findings → degradation framing.
|
|
668
734
|
// Healthy → baseline-lock framing (still real value: alert me if any score drops).
|
|
735
|
+
// engagementSignal without findings → soft-CTA wall-approach framing.
|
|
669
736
|
const heading = hasFindings
|
|
670
737
|
? (results.length === 1
|
|
671
738
|
? ' 🔔 Monitor this package. Get alerted if it gets worse.'
|
|
672
739
|
: ' 🔔 Lock in this audit. Get alerted if these packages get worse.')
|
|
673
|
-
:
|
|
740
|
+
: engagementSignal
|
|
741
|
+
? ' 🔔 Past the free anonymous quota on this network — lift it to 200/day.'
|
|
742
|
+
: ' 🔔 Lock in this baseline. Get alerted if any of these packages degrade.';
|
|
674
743
|
|
|
675
744
|
console.log(clr(c.dim, ' ─────────────────────────────────────────────'));
|
|
676
745
|
console.log(clr(c.bold, heading));
|
|
@@ -696,10 +765,29 @@ async function inlineSignup(results) {
|
|
|
696
765
|
process.stdout.write(clr(c.dim, ' Creating key...'));
|
|
697
766
|
|
|
698
767
|
try {
|
|
768
|
+
// Funnel attribution: 'cli-soft-cta' for engagement-signal path,
|
|
769
|
+
// 'cli' for findings-driven inline prompts. Lets api_keys.source
|
|
770
|
+
// measure 2026-06-10 engagementSignal gate-bypass lift separately
|
|
771
|
+
// from baseline inline-CLI signups. Backend VALID_SOURCES gains
|
|
772
|
+
// 'cli-soft-cta' in this same commit; older worker versions drop
|
|
773
|
+
// unknown sources to 'web' (safe degradation, no error).
|
|
774
|
+
const source = engagementSignal && !hasFindings ? 'cli-soft-cta' : 'cli';
|
|
775
|
+
// Proposition shift (2026-06-11, second layer): same defect the audit-page
|
|
776
|
+
// welcome email had until df8a8be — pre-fix, every CLI signup got hardcoded
|
|
777
|
+
// "poc watch express / lodash" in their welcome email regardless of what
|
|
778
|
+
// they actually scanned. Web side now seeds top-3-by-risk-priority on POST
|
|
779
|
+
// and the backend writes them to the user's default project before sending
|
|
780
|
+
// the welcome email. CLI must mirror or signups via `npx proof-of-commitment
|
|
781
|
+
// <some-package>` still get an email orthogonal to their intent. Backend
|
|
782
|
+
// accepts body.watch = [{name, ecosystem}], caps at PACKAGE_LIMITS.free=3,
|
|
783
|
+
// echoes seededPackages back as data.watched_packages. Priority order
|
|
784
|
+
// (compromised > CRITICAL > HIGH > others) matches the web-side
|
|
785
|
+
// buildWatchSeeds at commit-landing-v2/src/pages/audit.astro:1299.
|
|
786
|
+
const watch = buildCliWatchSeeds(results);
|
|
699
787
|
const res = await fetch('https://poc-backend.amdal-dev.workers.dev/api/keys/create', {
|
|
700
788
|
method: 'POST',
|
|
701
789
|
headers: { 'Content-Type': 'application/json' },
|
|
702
|
-
body: JSON.stringify({ email, source
|
|
790
|
+
body: JSON.stringify({ email, source, watch }),
|
|
703
791
|
});
|
|
704
792
|
|
|
705
793
|
const data = await res.json();
|
|
@@ -710,16 +798,39 @@ async function inlineSignup(results) {
|
|
|
710
798
|
console.log(clr(c.green, ' ✓ Saved to ~/.commit/config'));
|
|
711
799
|
console.log(clr(c.dim, ` Backup sent to ${email}`));
|
|
712
800
|
console.log();
|
|
713
|
-
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
801
|
+
// Render the backend echo (data.watched_packages) — the user sees
|
|
802
|
+
// "Now watching: foo, bar, baz" before the first weekly digest fires
|
|
803
|
+
// (~7d). Mirrors the audit-page renderInlineForm success state at
|
|
804
|
+
// commit-landing-v2/src/pages/audit.astro:1971 so on-context-switch the
|
|
805
|
+
// user does not see contradictory "you have nothing watched" messaging
|
|
806
|
+
// in `poc list`. Trust the server echo, not our pre-submit array (the
|
|
807
|
+
// server caps + dedups). Older backend versions that predate body.watch
|
|
808
|
+
// simply omit watched_packages — we fall through to the legacy
|
|
809
|
+
// single-target hint, no regression.
|
|
810
|
+
const watched = Array.isArray(data.watched_packages) ? data.watched_packages : [];
|
|
811
|
+
if (watched.length > 0) {
|
|
812
|
+
const names = watched.map(w => w.name).join(', ');
|
|
813
|
+
const noun = watched.length === 1 ? 'package' : 'packages';
|
|
814
|
+
console.log(clr(c.green, ` ✓ Now watching ${watched.length} ${noun}: ${names}`));
|
|
815
|
+
console.log(clr(c.dim, ' Mondays we email you if any score drops a tier or a watched package gets attacked.\n'));
|
|
816
|
+
console.log(clr(c.bold, ' Next steps:'));
|
|
817
|
+
console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc list') + clr(c.dim, ' — confirm your watchlist'));
|
|
818
|
+
console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc init') + clr(c.dim, ' — add CI gate to this project'));
|
|
819
|
+
console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc status') + clr(c.dim, ' — check your account'));
|
|
820
|
+
} else {
|
|
821
|
+
console.log(clr(c.bold, ' Next steps:'));
|
|
822
|
+
// Legacy fallback: backend did not seed (old worker, empty seeds, or
|
|
823
|
+
// seed failure swallowed). Surface a concrete watch target. CRITICAL
|
|
824
|
+
// first (highest urgency); otherwise pick the lowest-score package as
|
|
825
|
+
// the most-likely-to-degrade.
|
|
826
|
+
const watchTarget = critPkgs[0]?.name
|
|
827
|
+
|| results.slice().sort((a, b) => (a.score || 100) - (b.score || 100))[0]?.name;
|
|
828
|
+
if (watchTarget) {
|
|
829
|
+
console.log(clr(c.dim, ' • ') + clr(c.cyan, `poc watch ${watchTarget}`) + clr(c.dim, ' — monitor this package (free: 3 packages, weekly)'));
|
|
830
|
+
}
|
|
831
|
+
console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc init') + clr(c.dim, ' — add CI gate to this project'));
|
|
832
|
+
console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc status') + clr(c.dim, ' — check your account'));
|
|
720
833
|
}
|
|
721
|
-
console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc init') + clr(c.dim, ' — add CI gate to this project'));
|
|
722
|
-
console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc status') + clr(c.dim, ' — check your account'));
|
|
723
834
|
} else if (data.message) {
|
|
724
835
|
console.log(clr(c.green, ` ✓ ${data.message}`));
|
|
725
836
|
} else {
|
|
@@ -734,7 +845,7 @@ async function inlineSignup(results) {
|
|
|
734
845
|
|
|
735
846
|
function printHelp() {
|
|
736
847
|
console.log(`
|
|
737
|
-
${clr(c.bold, 'proof-of-commitment')} v1.
|
|
848
|
+
${clr(c.bold, 'proof-of-commitment')} v1.29.0 — supply chain risk scorer
|
|
738
849
|
|
|
739
850
|
${clr(c.bold, 'Usage:')}
|
|
740
851
|
npx proof-of-commitment Auto-detect manifest in current dir
|
|
@@ -2635,7 +2746,7 @@ async function main() {
|
|
|
2635
2746
|
const criticalTotal = allResults.filter(r => hasCritical(r.riskFlags)).length;
|
|
2636
2747
|
printTable(displayed, { totalScanned: allResults.length, totalCritical: criticalTotal, lockfile: true });
|
|
2637
2748
|
if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
|
|
2638
|
-
await inlineSignup(displayed);
|
|
2749
|
+
await inlineSignup(displayed, { engagementSignal: !!apiCta });
|
|
2639
2750
|
if (shouldFail(allResults, failOn)) {
|
|
2640
2751
|
console.error(clr(c.red + c.bold, `\n✗ --fail-on=${failOn} threshold met. Exit 1.`));
|
|
2641
2752
|
process.exit(1);
|
|
@@ -2676,7 +2787,7 @@ async function main() {
|
|
|
2676
2787
|
|
|
2677
2788
|
printTable(allResults);
|
|
2678
2789
|
if (apiCta) console.log(clr(c.dim + c.cyan, `\n ${apiCta}`));
|
|
2679
|
-
await inlineSignup(allResults);
|
|
2790
|
+
await inlineSignup(allResults, { engagementSignal: !!apiCta });
|
|
2680
2791
|
if (shouldFail(allResults, failOn)) {
|
|
2681
2792
|
console.error(clr(c.red + c.bold, `✗ --fail-on=${failOn} threshold met. Exit 1.`));
|
|
2682
2793
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proof-of-commitment",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.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",
|