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.
Files changed (2) hide show
  1. package/index.js +130 -19
  2. 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.26.0
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
- async function inlineSignup(results) {
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
- // Gate: show prompt when there's something worth monitoring.
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, still skip.
665
- if (results.length < 3 && !hasFindings) return;
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
- : ' 🔔 Lock in this baseline. Get alerted if any of these packages degrade.';
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: 'cli' }),
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
- console.log(clr(c.bold, ' Next steps:'));
714
- // Surface a concrete watch target. CRITICAL first (highest urgency);
715
- // otherwise pick the lowest-score package as the most-likely-to-degrade.
716
- const watchTarget = critPkgs[0]?.name
717
- || results.slice().sort((a, b) => (a.score || 100) - (b.score || 100))[0]?.name;
718
- if (watchTarget) {
719
- console.log(clr(c.dim, ' • ') + clr(c.cyan, `poc watch ${watchTarget}`) + clr(c.dim, ' — monitor this package (free: 3 packages, weekly)'));
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.24.0 — supply chain risk scorer
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.27.0",
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",