pqcheck 0.16.18 → 0.16.20

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dm/pqcheck.svg?style=flat-square&color=06b6d4)](https://www.npmjs.com/package/pqcheck)
9
9
  [![license](https://img.shields.io/npm/l/pqcheck.svg?style=flat-square&color=06b6d4)](./LICENSE)
10
10
 
11
- > **Latest: v0.16.18** — `--version` now reads dynamically from `package.json` instead of a hardcoded constant. Prior releases (0.16.16, 0.16.17) shipped with the constant left at `0.16.15`, so AI agents citing "I ran pqcheck X.Y.Z" were citing the wrong version. Single source of truth from here on; locked by a unit test against future drift. [Full changelog →](./CHANGELOG.md)
11
+ > **Latest: v0.16.20** — `pqcheck deploy-check --ai` now adds an absolute posture grade (A+ F, strict SSL-Labs rubric over CSP / HSTS / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy + `x-powered-by` leak) alongside the existing drift-only `ship_decision`. Grade D/F sets `posture_decision=block` so AI coders gate posture failures even on a clean drift. Adds `scope_note` (drift pass functional verification) and surfaces ready-to-paste fix snippets (Next.js / vercel.json / Express). Watched-domain monitoring also gains `posture_regression` + `cert_expiring` alerts. [Full changelog →](./CHANGELOG.md)
12
12
 
13
13
  ## Two ways to use it
14
14
 
@@ -21,10 +21,56 @@
21
21
  import { readFileSync, existsSync } from "node:fs";
22
22
  import { join, dirname } from "node:path";
23
23
  import { homedir } from "node:os";
24
+ import { execSync } from "node:child_process";
24
25
 
25
26
  const GLOBAL_STATE_FILE = join(homedir(), ".config", "cipherwake", "last-scan.json");
26
27
  const STALE_THRESHOLD_HOURS = 24;
27
28
 
29
+ // v0.16.19 — `--prepend=<command>` flag for statusLine composition. Claude
30
+ // Code's `statusLine.command` only supports ONE command, which means two
31
+ // tools (e.g. Cipherwake + PinnedAI) both writing their own statusLine
32
+ // silently clobber each other depending on install order. When a customer
33
+ // runs both, they only see whichever installed last. With --prepend, the
34
+ // Cipherwake binary runs the other tool's command first, joins its output
35
+ // with a separator, and renders Cipherwake's own line after. Result:
36
+ // `[pinned output] · ◆ Cipherwake · domain ✓ PASS · 5m ago` — both
37
+ // surfaces visible in the single statusLine slot.
38
+ //
39
+ // pqcheck setup detects an existing statusLine.command at install time
40
+ // and writes the wrapper form automatically:
41
+ // "command": "npx --package=pqcheck@latest cipherwake-statusline --prepend='<prior-command>'"
42
+ //
43
+ // If the prior command errors (or the tool was uninstalled), this binary
44
+ // swallows the failure and just renders Cipherwake's part — never causes
45
+ // the whole statusLine to break.
46
+ const PREPEND_SEPARATOR = " · ";
47
+ const prependArg = process.argv.find((a) => a.startsWith("--prepend="));
48
+ if (prependArg) {
49
+ const prependCmd = prependArg.slice("--prepend=".length);
50
+ if (prependCmd) {
51
+ try {
52
+ // 5s soft timeout — statusLine rendering must stay snappy. If the
53
+ // other tool hangs, we cut it off and continue with Cipherwake's part.
54
+ // stdio: ignore on stderr so a noisy prior tool doesn't pollute the
55
+ // bar; we want only its rendered stdout output.
56
+ const out = execSync(prependCmd, {
57
+ encoding: "utf8",
58
+ timeout: 5_000,
59
+ stdio: ["ignore", "pipe", "ignore"],
60
+ env: process.env,
61
+ }).trim();
62
+ if (out) {
63
+ process.stdout.write(out);
64
+ process.stdout.write(PREPEND_SEPARATOR);
65
+ }
66
+ } catch {
67
+ // Prepend command failed (uninstalled, timeout, errored). Skip
68
+ // silently — never break the whole statusLine because of a
69
+ // composition partner.
70
+ }
71
+ }
72
+ }
73
+
28
74
  // v0.16.6 — project-aware state lookup. Walk up from CWD looking for a
29
75
  // repo-local `.cipherwake/last-scan.json`. This way each project shows
30
76
  // its own last scan, and switching projects doesn't bleed the previous
package/bin/pqcheck.js CHANGED
@@ -495,6 +495,22 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
495
495
  }
496
496
  }
497
497
 
498
+ // R86 — surface absolute posture grade + remediation alongside drift verdict.
499
+ // The grade= field carries DBR grade; posture_grade= is the new absolute
500
+ // posture (A+/A/B/C/D/F based on header presence/strength).
501
+ // posture_decision separates the absolute-state routing signal from the
502
+ // drift-based ship_decision so a weak-but-unchanged site gets a review
503
+ // nudge distinct from drift.
504
+ const posture = report.posture || null;
505
+ const postureFields = posture ? {
506
+ posture_grade: posture.grade,
507
+ posture_score: posture.score,
508
+ posture_decision: posture.decision,
509
+ posture_missing: (posture.missing || []).join(",") || "none",
510
+ posture_leaks: (posture.info_leaks || []).join("; ") || "none",
511
+ posture_findings_count: (posture.findings || []).length,
512
+ posture_fixes_count: (posture.fixes || []).length,
513
+ } : {};
498
514
  console.log(formatAiFooterBlock({
499
515
  status: shipDecision,
500
516
  domain,
@@ -509,6 +525,13 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
509
525
  findings_critical: findings.filter((f) => severityRank(f.severity) === 4).length,
510
526
  scanned_at: new Date().toISOString(),
511
527
  advisory_only: "true",
528
+ // R85 (2026-06-03) — scope honesty disclaimer. pass = trust surface
529
+ // stable, NOT app functionality. Agents that follow the protocol must
530
+ // see this so they don't announce a deploy whose signup is broken
531
+ // (the exact bug the user reported on socialideagen 2026-06-02).
532
+ scope: "trust_surface_drift",
533
+ scope_note: "ship_decision=pass means public trust surface stable. Does NOT verify app functionality. Pair with functional checks (e.g. Playwright e2e) for full deploy safety.",
534
+ ...postureFields,
512
535
  }));
513
536
  console.log("");
514
537
 
@@ -3400,6 +3423,20 @@ async function runTrustDiffCommand(args) {
3400
3423
  console.log(color("dim", " Run with --verbose to see all verified signals."));
3401
3424
  }
3402
3425
 
3426
+ // R86 — posture grade + remediation alongside drift. trust-diff API returns
3427
+ // result.current_report which contains the full scan; if so, read posture
3428
+ // from it. Otherwise gracefully omit (older API responses without posture).
3429
+ const currentReport = result.current_report || result.report || null;
3430
+ const posture = currentReport?.posture || null;
3431
+ const postureFields = posture ? {
3432
+ posture_grade: posture.grade,
3433
+ posture_score: posture.score,
3434
+ posture_decision: posture.decision,
3435
+ posture_missing: (posture.missing || []).join(",") || "none",
3436
+ posture_leaks: (posture.info_leaks || []).join("; ") || "none",
3437
+ posture_findings_count: (posture.findings || []).length,
3438
+ posture_fixes_count: (posture.fixes || []).length,
3439
+ } : {};
3403
3440
  console.log(formatAiFooterBlock({
3404
3441
  status: shipDecision,
3405
3442
  domain,
@@ -3417,6 +3454,13 @@ async function runTrustDiffCommand(args) {
3417
3454
  quota_limit: result.quota?.monthly_limit ?? "",
3418
3455
  scanned_at: new Date().toISOString(),
3419
3456
  advisory_only: "true",
3457
+ // R85 — scope honesty disclaimer (per build brief 2026-06-03).
3458
+ // ship_decision=pass means trust surface stable, NOT app functions.
3459
+ // Agents must see this so they don't announce a deploy whose signup
3460
+ // is broken (the exact bug socialideagen had on 2026-06-02).
3461
+ scope: "trust_surface_drift",
3462
+ scope_note: "ship_decision=pass means public trust surface stable. Does NOT verify app functionality. Pair with functional checks for full deploy safety.",
3463
+ ...postureFields,
3420
3464
  }));
3421
3465
  console.log("");
3422
3466
 
@@ -6317,19 +6361,54 @@ async function runSetupCommand(args) {
6317
6361
  settings = JSON.parse(raw);
6318
6362
  existed = true;
6319
6363
  } catch { /* will create */ }
6320
- if (existed && settings.statusLine && typeof settings.statusLine === "object") {
6321
- // Already has a statusLine config don't overwrite.
6322
- console.log(color("dim", ` ⊝ ${displayPath} already has a statusLine entry leaving alone`));
6323
- console.log(color("dim", ` To use the Cipherwake statusline instead, set: "command": "npx --package=pqcheck@latest cipherwake-statusline"`));
6324
- installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: "skipped-existing-config" });
6364
+ // v0.16.19 statusLine composition. Previous behavior was: if any
6365
+ // statusLine.command already existed, SKIP. That was safe (never
6366
+ // clobbered other tools) but invisible a user with PinnedAI's
6367
+ // statusline installed would never see Cipherwake's after running
6368
+ // `pqcheck setup`. Now: detect the prior command and wrap it via
6369
+ // `cipherwake-statusline --prepend=<prior>` so both render in the
6370
+ // single statusLine slot. The prepend logic in cipherwake-statusline.js
6371
+ // swallows prepend-command failures silently, so this composition
6372
+ // survives the user later uninstalling the partner tool.
6373
+ //
6374
+ // Skip the wrap when the existing command IS already our wrapper
6375
+ // (idempotent re-install — don't recursively nest).
6376
+ const CIPHERWAKE_CMD_PREFIX = "npx --package=pqcheck@latest cipherwake-statusline";
6377
+ const priorCmd = (existed && settings.statusLine && typeof settings.statusLine === "object")
6378
+ ? String(settings.statusLine.command || "").trim()
6379
+ : "";
6380
+ const priorIsOurs = priorCmd.startsWith("npx --package=pqcheck") && priorCmd.includes("cipherwake-statusline");
6381
+
6382
+ if (priorIsOurs) {
6383
+ console.log(color("dim", ` ⊝ ${displayPath} statusLine already points at cipherwake-statusline — leaving alone`));
6384
+ installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: "skipped-already-ours" });
6325
6385
  } else {
6326
6386
  const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
6327
6387
  if (backupPath) console.log(color("dim", ` backup: ${backupPath}`));
6328
- settings.statusLine = { type: "command", command: "npx --package=pqcheck@latest cipherwake-statusline" };
6388
+ let command;
6389
+ if (priorCmd) {
6390
+ // Compose: wrap the existing command via --prepend.
6391
+ // Single-quote the prior command + escape any single quotes
6392
+ // (rare in practice but defensive).
6393
+ const safe = priorCmd.replace(/'/g, `'\\''`);
6394
+ command = `${CIPHERWAKE_CMD_PREFIX} --prepend='${safe}'`;
6395
+ console.log(color("green", ` ✓ composed statusLine: existing command wrapped via --prepend → ${displayPath}`));
6396
+ console.log(color("dim", ` prior command: ${priorCmd.slice(0, 80)}${priorCmd.length > 80 ? "..." : ""}`));
6397
+ console.log(color("dim", ` both surfaces now render in the single statusLine slot`));
6398
+ } else {
6399
+ // Fresh install — no prior command to compose with.
6400
+ command = CIPHERWAKE_CMD_PREFIX;
6401
+ console.log(color("green", ` ✓ added statusLine config → ${displayPath}`));
6402
+ }
6403
+ settings.statusLine = { type: "command", command };
6329
6404
  await fs.mkdir(path.dirname(settingsPath), { recursive: true });
6330
6405
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
6331
- console.log(color("green", ` ✓ added statusLine config → ${displayPath}`));
6332
- installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
6406
+ installSummary.push({
6407
+ component: "Claude Code statusLine",
6408
+ path: settingsPath,
6409
+ status: existed && priorCmd ? "installed-composed" : (existed ? "installed-updated" : "installed-created"),
6410
+ backup: backupPath,
6411
+ });
6333
6412
  }
6334
6413
  } catch (err) {
6335
6414
  console.log(color("red", ` ✗ statusLine config install failed: ${err.message}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.16.18",
3
+ "version": "0.16.20",
4
4
  "description": "Deploy gate for AI-coded web apps. `pqcheck deploy-check --ai` returns ship_decision=pass|review|block for Claude Code / Cursor / Copilot / Aider to gate deploys before they ship. Anonymous, no signup, free for first use.",
5
5
  "keywords": [
6
6
  "ai-coder",