proof-of-commitment 1.20.0 → 1.21.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 (3) hide show
  1. package/README.md +12 -15
  2. package/index.js +229 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,26 +51,23 @@ npx proof-of-commitment --file go.sum # full transitive set
51
51
 
52
52
  **Web demo (no install):** [getcommit.dev/audit](https://getcommit.dev/audit) — paste your packages, see risk scores in seconds.
53
53
 
54
- **Account + monitoring (v1.10.0):**
54
+ ## Get notified before the next attack
55
+
56
+ The CLI tells you what's risky today. A free API key unlocks **monitoring** — daily score recomputation across the packages you depend on, with alerts when one degrades (publisher drops, release stalls, score falls ≥10 points).
57
+
58
+ [**Get a free API key →**](https://getcommit.dev/get-started?ref=npm-readme-monitoring&utm_source=cli) — no card, 30 seconds · 200 audits/day free · Developer $15/mo unlocks alerts + watchlist.
59
+
55
60
  ```bash
56
61
  # Install once, then use the `poc` alias:
57
62
  npm install -g proof-of-commitment
58
63
 
59
- # Get a free API key at https://getcommit.dev/get-started?utm_source=cli, then:
60
- poc login sk_commit_your_key_here
61
- # Authenticated Tier: Free — Usage: 0/200 requests (daily)
62
-
63
- poc status # check tier + usage anytime
64
- poc logout # remove saved key
65
-
66
- # Monitoring (Developer $15/mo+ — daily scans + alerts):
67
- poc watch chalk
64
+ # After getting your free key:
65
+ poc login sk_commit_your_key_here # save and validate
66
+ poc status # check tier + usage
67
+ poc watch chalk # start monitoring (Developer $15/mo)
68
68
  poc watch requests --ecosystem pypi
69
- poc watch serde --ecosystem cargo
70
- poc watchlist # view scores + risk levels
71
- poc unwatch chalk
72
-
73
- # Enable monitoring: https://getcommit.dev/pricing
69
+ poc watchlist # view all watched packages
70
+ poc init # add CI gate to this project
74
71
  ```
75
72
 
76
73
  Alerts fire on: score drop ≥10 points · package crosses CRITICAL threshold · recovery to HEALTHY.
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * proof-of-commitment CLI v1.20.0
3
+ * proof-of-commitment CLI v1.21.0
4
4
  * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
@@ -22,6 +22,22 @@ const JSON_API_HEADERS = {
22
22
  'Accept': 'application/json',
23
23
  };
24
24
 
25
+ /**
26
+ * Build /api/audit request headers, adding Authorization: Bearer <key>
27
+ * when a key is present in COMMIT_API_KEY or ~/.commit/config.
28
+ *
29
+ * Without this, signed-up users hitting 429 stayed stuck: the inline-signup
30
+ * (v1.20.0) and URL signup flows both save the key locally, but the audit
31
+ * call site never read it — so "Re-run your command" still 429'd. Fixed
32
+ * in v1.20.1 after live dogfood confirmed the dead-end (see commit log).
33
+ */
34
+ async function auditHeaders() {
35
+ const key = await readApiKey();
36
+ return key
37
+ ? { ...JSON_API_HEADERS, Authorization: `Bearer ${key}` }
38
+ : JSON_API_HEADERS;
39
+ }
40
+
25
41
  // ANSI color helpers
26
42
  const c = {
27
43
  reset: '\x1b[0m',
@@ -332,6 +348,7 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
332
348
  console.log(clr(c.cyan, `\n 🔗 Full report: ${WEB}?packages=${encodeURIComponent(topPkgs)}&${utm}`));
333
349
  console.log(clr(c.cyan, ` 🤖 GitHub Action: github.com/piiiico/commit-action — block CRITICAL packages in CI`));
334
350
  console.log(clr(c.dim, ` 📋 Add to this project: `) + clr(c.cyan, `poc init`) + clr(c.dim, ` — creates workflow + README badge`));
351
+ console.log(clr(c.dim, ` 🛡️ Protect every install: `) + clr(c.cyan, `poc hook`) + clr(c.dim, ` — Cursor hook, blocks CRITICAL before npm/pip/cargo runs`));
335
352
 
336
353
  // Per-package profile URLs — drive traffic to permanent, indexable pages
337
354
  const ecoPath = { npm: 'npm', pypi: 'pypi', cargo: 'cargo', golang: 'go' };
@@ -462,7 +479,7 @@ async function inlineSignup(results) {
462
479
 
463
480
  function printHelp() {
464
481
  console.log(`
465
- ${clr(c.bold, 'proof-of-commitment')} v1.20.0 — supply chain risk scorer
482
+ ${clr(c.bold, 'proof-of-commitment')} v1.21.0 — supply chain risk scorer
466
483
 
467
484
  ${clr(c.bold, 'Usage:')}
468
485
  npx proof-of-commitment Auto-detect manifest in current dir
@@ -488,6 +505,11 @@ ${clr(c.bold, 'Reports:')}
488
505
  poc report [pkgs] Same flags as scan — packages, --pypi, --cargo, --file, etc.
489
506
  Saves audit-report.html to cwd + prints Markdown for GitHub issues
490
507
 
508
+ ${clr(c.bold, 'IDE Hooks:')}
509
+ poc hook Install Cursor beforeShellExecution hook (blocks CRITICAL packages)
510
+ poc hook --global Install for all Cursor projects (~/.cursor/hooks.json)
511
+ poc hook --uninstall Remove the hook
512
+
491
513
  ${clr(c.bold, 'Account:')}
492
514
  poc login [key] Save and validate your API key (interactive or direct)
493
515
  poc status Show current tier, usage, and limits
@@ -876,11 +898,13 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
876
898
 
877
899
  let completed = 0;
878
900
  let batchedCta = null;
901
+ // Resolve auth once so all parallel batches share the same key lookup.
902
+ const headers = await auditHeaders();
879
903
  const results = await Promise.all(
880
904
  batches.map(async (batch) => {
881
905
  const res = await fetch(API, {
882
906
  method: 'POST',
883
- headers: JSON_API_HEADERS,
907
+ headers,
884
908
  body: JSON.stringify({ packages: batch, ecosystem }),
885
909
  });
886
910
  if (!res.ok) {
@@ -1107,6 +1131,201 @@ async function cmdLogout() {
1107
1131
  console.log();
1108
1132
  }
1109
1133
 
1134
+ /**
1135
+ * poc hook [--cursor] [--global] [--uninstall]
1136
+ * Install a beforeShellExecution hook for Cursor that scores packages before install.
1137
+ * Writes a standalone Node.js hook script to ~/.commit/cursor-hook.js and
1138
+ * configures .cursor/hooks.json (project) or ~/.cursor/hooks.json (--global).
1139
+ */
1140
+ async function cmdHook(args) {
1141
+ const os = await import('os');
1142
+ const fs = await import('fs');
1143
+ const path = await import('path');
1144
+
1145
+ const isGlobal = args.includes('--global') || args.includes('-g');
1146
+ const uninstall = args.includes('--uninstall') || args.includes('--remove');
1147
+
1148
+ // ── Hook script (plain Node.js, no external deps) ─────────────────────
1149
+ const hookScript = `#!/usr/bin/env node
1150
+ /**
1151
+ * Commit supply chain hook for Cursor (auto-generated by \`poc hook\`)
1152
+ * Intercepts npm/pip/cargo/go install commands and scores packages
1153
+ * against getcommit.dev before they run.
1154
+ *
1155
+ * CRITICAL packages are blocked. HIGH packages trigger confirmation.
1156
+ * Docs: https://getcommit.dev/docs/cursor-hook
1157
+ */
1158
+ const API = 'https://poc-backend.amdal-dev.workers.dev/api/audit';
1159
+ const fs = require('fs');
1160
+ const path = require('path');
1161
+
1162
+ function readKey() {
1163
+ try {
1164
+ if (process.env.COMMIT_API_KEY) return process.env.COMMIT_API_KEY.trim();
1165
+ const cfg = fs.readFileSync(path.join(require('os').homedir(), '.commit', 'config'), 'utf-8');
1166
+ const m = cfg.match(/^api_key\\s*=\\s*(.+)$/m);
1167
+ return m ? m[1].trim() : '';
1168
+ } catch { return ''; }
1169
+ }
1170
+
1171
+ function parseInstall(cmd) {
1172
+ const t = cmd.trim();
1173
+ let m;
1174
+ // npm / pnpm / yarn
1175
+ m = t.match(/^(?:npm\\s+(?:i|install|add)|pnpm\\s+(?:i|install|add)|yarn\\s+add)\\s+(.+)/);
1176
+ if (m) return { eco: 'npm', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-') && a !== 'install' && a !== 'add') };
1177
+ // pip
1178
+ m = t.match(/^(?:pip3?\\s+install|uv\\s+pip\\s+install|python3?\\s+-m\\s+pip\\s+install)\\s+(.+)/);
1179
+ if (m) return { eco: 'pypi', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')).map(a => a.split('==')[0].split('>=')[0]) };
1180
+ // cargo
1181
+ m = t.match(/^cargo\\s+(?:add|install)\\s+(.+)/);
1182
+ if (m) return { eco: 'cargo', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')) };
1183
+ // go
1184
+ m = t.match(/^go\\s+(?:get|install)\\s+(.+)/);
1185
+ if (m) return { eco: 'golang', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')).map(a => a.split('@')[0]) };
1186
+ return null;
1187
+ }
1188
+
1189
+ async function main() {
1190
+ let input;
1191
+ try { input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf-8')); } catch { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
1192
+ const parsed = parseInstall(input.command || '');
1193
+ if (!parsed || parsed.pkgs.length === 0) { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
1194
+
1195
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
1196
+ const key = readKey();
1197
+ if (key) headers['Authorization'] = 'Bearer ' + key;
1198
+
1199
+ try {
1200
+ const ctrl = new AbortController();
1201
+ const timer = setTimeout(() => ctrl.abort(), 4000);
1202
+ const res = await fetch(API, { method: 'POST', headers, body: JSON.stringify({ packages: parsed.pkgs, ecosystem: parsed.eco }), signal: ctrl.signal });
1203
+ clearTimeout(timer);
1204
+ if (!res.ok && res.status !== 429) { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
1205
+ const data = await res.json();
1206
+ const results = data.results || data.packages_already_scored || [];
1207
+
1208
+ const critical = results.filter(r => (r.riskFlags || []).some(f => f.startsWith('CRITICAL')));
1209
+ const high = results.filter(r => (r.riskFlags || []).some(f => f.startsWith('HIGH')));
1210
+ const url = 'https://getcommit.dev/audit?packages=' + parsed.pkgs.join(',') + '&ecosystem=' + parsed.eco;
1211
+
1212
+ if (critical.length > 0) {
1213
+ const lines = critical.map(r => ' \\u{1F534} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
1214
+ process.stdout.write(JSON.stringify({
1215
+ permission: 'deny',
1216
+ user_message: '\\u{1F534} Commit blocked: ' + critical.map(r=>r.name).join(', ') + ' flagged CRITICAL\\n\\n' + lines.join('\\n') + '\\n\\n\\u2192 ' + url,
1217
+ agent_message: 'Package install blocked by Commit. CRITICAL = sole publisher + high downloads (attack surface of axios/node-ipc incidents). ' + critical.map(r=>r.name).join(', ') + '. Report: ' + url,
1218
+ }));
1219
+ return;
1220
+ }
1221
+ if (high.length > 0) {
1222
+ const lines = high.map(r => ' \\u{1F7E1} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
1223
+ process.stdout.write(JSON.stringify({
1224
+ permission: 'ask',
1225
+ user_message: '\\u{1F7E1} Commit: ' + high.map(r=>r.name).join(', ') + ' scored HIGH risk\\n\\n' + lines.join('\\n') + '\\n\\nProceed? \\u2192 ' + url,
1226
+ }));
1227
+ return;
1228
+ }
1229
+ process.stdout.write(JSON.stringify({ permission: 'allow' }));
1230
+ } catch { process.stdout.write(JSON.stringify({ permission: 'allow' })); }
1231
+ }
1232
+ main();
1233
+ `;
1234
+
1235
+ const commitDir = path.join(os.homedir(), '.commit');
1236
+ const hookPath = path.join(commitDir, 'cursor-hook.js');
1237
+
1238
+ // ── Uninstall ──────────────────────────────────────────────────────────
1239
+ if (uninstall) {
1240
+ let removed = false;
1241
+ if (fs.existsSync(hookPath)) {
1242
+ fs.unlinkSync(hookPath);
1243
+ removed = true;
1244
+ }
1245
+ // Remove from hooks.json
1246
+ for (const hooksDir of [
1247
+ path.join(process.cwd(), '.cursor'),
1248
+ path.join(os.homedir(), '.cursor'),
1249
+ ]) {
1250
+ const hooksFile = path.join(hooksDir, 'hooks.json');
1251
+ if (fs.existsSync(hooksFile)) {
1252
+ try {
1253
+ const cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
1254
+ const hooks = cfg.hooks?.beforeShellExecution || [];
1255
+ const filtered = hooks.filter(h => !h.command?.includes('cursor-hook.js'));
1256
+ if (filtered.length !== hooks.length) {
1257
+ cfg.hooks.beforeShellExecution = filtered;
1258
+ fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
1259
+ removed = true;
1260
+ console.log(clr(c.dim, ` Updated: ${hooksFile}`));
1261
+ }
1262
+ } catch {}
1263
+ }
1264
+ }
1265
+ if (removed) {
1266
+ console.log(clr(c.green, '\n ✓ Cursor hook uninstalled.'));
1267
+ } else {
1268
+ console.log(clr(c.dim, '\n No hook found to remove.'));
1269
+ }
1270
+ console.log();
1271
+ return;
1272
+ }
1273
+
1274
+ // ── Install ────────────────────────────────────────────────────────────
1275
+ // 1. Write hook script
1276
+ if (!fs.existsSync(commitDir)) fs.mkdirSync(commitDir, { recursive: true });
1277
+ fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
1278
+
1279
+ // 2. Configure hooks.json
1280
+ const hooksDir = isGlobal
1281
+ ? path.join(os.homedir(), '.cursor')
1282
+ : path.join(process.cwd(), '.cursor');
1283
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
1284
+
1285
+ const hooksFile = path.join(hooksDir, 'hooks.json');
1286
+ let cfg = { version: 1, hooks: {} };
1287
+ if (fs.existsSync(hooksFile)) {
1288
+ try { cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8')); } catch {}
1289
+ }
1290
+ if (!cfg.hooks) cfg.hooks = {};
1291
+ if (!cfg.hooks.beforeShellExecution) cfg.hooks.beforeShellExecution = [];
1292
+
1293
+ // Avoid duplicates
1294
+ const existing = cfg.hooks.beforeShellExecution.some(h => h.command?.includes('cursor-hook.js'));
1295
+ if (!existing) {
1296
+ cfg.hooks.beforeShellExecution.push({
1297
+ command: `node ${hookPath}`,
1298
+ });
1299
+ }
1300
+ fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
1301
+
1302
+ // 3. Report
1303
+ console.log(clr(c.green, '\n ✓ Cursor supply chain hook installed'));
1304
+ console.log();
1305
+ console.log(clr(c.bold, ' What happens now:'));
1306
+ console.log(clr(c.dim, ' Every ') + clr(c.cyan, 'npm install') + clr(c.dim, ', ') +
1307
+ clr(c.cyan, 'pip install') + clr(c.dim, ', ') + clr(c.cyan, 'cargo add') + clr(c.dim, ', and ') +
1308
+ clr(c.cyan, 'go get') + clr(c.dim, ' in Cursor'));
1309
+ console.log(clr(c.dim, ' is scored against Commit before it runs.'));
1310
+ console.log(clr(c.dim, ' CRITICAL packages are blocked. HIGH packages ask for confirmation.'));
1311
+ console.log();
1312
+ console.log(clr(c.bold, ' Files:'));
1313
+ console.log(clr(c.dim, ` Hook script: ${hookPath}`));
1314
+ console.log(clr(c.dim, ` Config: ${hooksFile}`));
1315
+ console.log();
1316
+
1317
+ const key = await readApiKey();
1318
+ if (!key) {
1319
+ console.log(clr(c.yellow, ' ⚠ No API key found.') + clr(c.dim, ' Anonymous limit: 15 audits/day.'));
1320
+ console.log(clr(c.dim, ' Get a free key (200/day): ') + clr(c.cyan, 'poc login'));
1321
+ console.log(clr(c.dim, ' Or: ') + clr(c.cyan, 'https://getcommit.dev/get-started?ref=cursor-hook&utm_source=cli'));
1322
+ console.log();
1323
+ }
1324
+
1325
+ console.log(clr(c.dim, ' Uninstall: ') + clr(c.cyan, 'poc hook --uninstall'));
1326
+ console.log();
1327
+ }
1328
+
1110
1329
  function tierLabel(tier) {
1111
1330
  if (tier === 'pro') return clr(c.cyan + c.bold, 'Pro');
1112
1331
  if (tier === 'enterprise') return clr(c.magenta + c.bold, 'Enterprise');
@@ -1479,7 +1698,7 @@ async function cmdReport(packages, ecosystem, { filePath, isLockfile, totalScann
1479
1698
  if (packages.length <= 20) {
1480
1699
  const res = await fetch(API, {
1481
1700
  method: 'POST',
1482
- headers: JSON_API_HEADERS,
1701
+ headers: await auditHeaders(),
1483
1702
  body: JSON.stringify({ packages, ecosystem }),
1484
1703
  });
1485
1704
  if (!res.ok) {
@@ -1705,6 +1924,11 @@ async function main() {
1705
1924
  process.exit(0);
1706
1925
  }
1707
1926
 
1927
+ if (subcmd === 'hook') {
1928
+ await cmdHook(args.slice(1));
1929
+ process.exit(0);
1930
+ }
1931
+
1708
1932
  if (subcmd === 'report') {
1709
1933
  // Parse report args (same flags as main scan)
1710
1934
  const reportArgs = args.slice(1);
@@ -1881,7 +2105,7 @@ async function main() {
1881
2105
  try {
1882
2106
  const res = await fetch(API, {
1883
2107
  method: 'POST',
1884
- headers: JSON_API_HEADERS,
2108
+ headers: await auditHeaders(),
1885
2109
  body: JSON.stringify({ packages, ecosystem }),
1886
2110
  });
1887
2111
  if (!res.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "mcpName": "io.github.piiiico/proof-of-commitment",
5
5
  "description": "Supply chain risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
6
6
  "type": "module",