pqcheck 0.16.29 → 0.16.31

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.29** — Fixed a high-volume false positive caught by external testing: WAF-mitigated 403s on `/wp-admin` (and any `expect: missing` default) were firing as "WordPress leak" on every Vercel/Cloudflare-hosted non-WP app. The probe now detects `x-vercel-mitigated` / `cf-mitigated` headers and classifies them as `blocked` (not `protected`). And `expect: missing` now semantically means "not reachable to anonymous users"404/401/403/3xx/WAF-block all satisfy; only 200-serving-content fails. Every catastrophic catch (`.env` leak at 200, real WP at 200, public `/api/admin`) preserved. Verified with 23 new tests covering all classification × expectation combinations. [Full changelog →](./CHANGELOG.md)
11
+ > **Latest: v0.16.31** — `--fresh` now actually refreshes posture. External dogfood bug: a customer deployed CSP/HSTS/X-Frame-Options/X-Content-Type-Options/Referrer-Policy/Permissions-Policy via Next.js, verified all six on the wire with curl, and `pqcheck deploy-check --fresh` continued to report `posture_grade=D` + `posture_leaks=x-powered-by: Next.js` directly contradicting reality. Root cause: the CLI silently dropped `--fresh` from the trust-diff request body. Now plumbed through end-to-end. Every response carries `fresh_status` (`applied | rate_limited | unauthenticated | unavailable | not_requested`) so callers route on whether the posture is current no more silent stale reads. `--verbose` emits a `CIPHERWAKE_SCANNER_OBSERVED` block with the actual headers, final URL, and status the grade was computed from, so customers can diff "what Cipherwake saw" vs `curl -I` instantly. [Full changelog →](./CHANGELOG.md)
12
12
 
13
13
  ## Two ways to use it
14
14
 
package/bin/pqcheck.js CHANGED
@@ -3770,6 +3770,12 @@ async function runTrustDiffCommand(args) {
3770
3770
  const baseline = parseFlag(args, "--baseline") || "last-week";
3771
3771
  const failOn = parseFlag(args, "--fail-on") || "high";
3772
3772
  const format = parseFlag(args, "--format") || "pretty";
3773
+ // R92 (2026-06-06) — --fresh + --verbose were silently dropped by the
3774
+ // CLI before R92. The result: `pqcheck deploy-check <D> --fresh` ran but
3775
+ // returned stale posture from scan_cache. After R92 they're plumbed
3776
+ // through to /api/trust-diff body so the server can act on them.
3777
+ const fresh = args.includes("--fresh") || args.includes("--force");
3778
+ const verbose = args.includes("--verbose");
3773
3779
  // R86.4 (2026-06-03) — see runOneScan for rationale. Default ship_decision
3774
3780
  // is drift-only (per-deploy regression gate); --strict-posture opts into
3775
3781
  // worst-of(drift, posture). Recommended only after a site reaches A/B
@@ -3799,7 +3805,17 @@ async function runTrustDiffCommand(args) {
3799
3805
  resp = await fetch(`${API_BASE}/api/trust-diff`, {
3800
3806
  method: "POST",
3801
3807
  headers,
3802
- body: JSON.stringify({ domain, baseline, fail_on: failOn, routeAssertionsConfig: _routeCfg }),
3808
+ body: JSON.stringify({
3809
+ domain,
3810
+ baseline,
3811
+ fail_on: failOn,
3812
+ routeAssertionsConfig: _routeCfg,
3813
+ // R92 (2026-06-06) — pass --fresh + --verbose through to server.
3814
+ // Server returns `fresh_status` so caller can route on
3815
+ // applied | rate_limited | unauthenticated | unavailable | not_requested.
3816
+ fresh,
3817
+ verbose,
3818
+ }),
3803
3819
  });
3804
3820
  } catch (err) {
3805
3821
  console.error(color("red", `error: network failure calling /api/trust-diff: ${err.message}`));
@@ -3856,6 +3872,45 @@ async function runTrustDiffCommand(args) {
3856
3872
  const result = await resp.json();
3857
3873
  const verdict = result.verdict || "pass";
3858
3874
  const deltas = Array.isArray(result.deltas) ? result.deltas : [];
3875
+ // R92 (2026-06-06) — surface non-applied fresh status BEFORE any verdict
3876
+ // rendering so the customer sees the staleness warning at the top, not
3877
+ // after the (potentially stale) grade. Three failure modes get their own
3878
+ // explanation since they imply different next actions.
3879
+ const freshStatus = typeof result.fresh_status === "string" ? result.fresh_status : "not_requested";
3880
+ if (fresh && freshStatus !== "applied" && freshStatus !== "not_requested") {
3881
+ const why = freshStatus === "rate_limited"
3882
+ ? "fresh-scan per-IP cap reached (20/hr). The posture below is from the last cached scan — re-run after the cap window, or accept the cached read for now."
3883
+ : freshStatus === "unauthenticated"
3884
+ ? "fresh-scan requires an API key (free tier inclusive). Set CIPHERWAKE_API_KEY or run via OIDC; without it, --fresh silently downgrades to cached. https://cipherwake.io/account#api-keys"
3885
+ : freshStatus === "unavailable"
3886
+ ? "fresh-scan path failed mid-run. The posture below is from the last cached scan — re-run to retry."
3887
+ : `fresh request returned status=${freshStatus}.`;
3888
+ console.error("");
3889
+ console.error(color("yellow", `⚠ --fresh requested but NOT applied: ${why}`));
3890
+ console.error("");
3891
+ }
3892
+ // R92 — scanner_observed (verbose mode). Surfaces the actual headers /
3893
+ // final URL / status that Cipherwake's posture grade was computed from,
3894
+ // so a customer can diff against `curl -I` and catch a stale or
3895
+ // wrong-target read instantly.
3896
+ if (verbose && result.scanner_observed) {
3897
+ const obs = result.scanner_observed;
3898
+ console.log("");
3899
+ console.log(color("dim", "CIPHERWAKE_SCANNER_OBSERVED"));
3900
+ console.log(color("dim", `final_url=${obs.final_url ?? "<unknown>"}`));
3901
+ console.log(color("dim", `final_status=${obs.final_status ?? "<unknown>"}`));
3902
+ console.log(color("dim", `fetched_at=${obs.fetched_at}`));
3903
+ if (obs.headers && typeof obs.headers === "object") {
3904
+ const headerKeys = Object.keys(obs.headers).sort();
3905
+ for (const k of headerKeys) {
3906
+ const v = obs.headers[k];
3907
+ const val = Array.isArray(v) ? v.join(", ") : (v == null ? "" : String(v));
3908
+ console.log(color("dim", `header.${k}=${val}`));
3909
+ }
3910
+ }
3911
+ console.log(color("dim", "END_CIPHERWAKE_SCANNER_OBSERVED"));
3912
+ console.log("");
3913
+ }
3859
3914
 
3860
3915
  // AI Coder Mode — three-layer output (banner / body / structured block).
3861
3916
  if (parseAiMode(args)) {
@@ -4004,7 +4059,7 @@ async function runTrustDiffCommand(args) {
4004
4059
  scanned_at: new Date().toISOString(),
4005
4060
  advisory_only: "true",
4006
4061
  scope: strictPosture ? "trust_surface_drift_plus_absolute_posture_plus_route_assertions_plus_health" : "trust_surface_drift_plus_route_assertions_plus_health",
4007
- scope_note: "ship_decision = worst-of(drift, route_assertions, deploy_health, secret_scan, cookie_invariants" + (strictPosture ? ", absolute_posture)" : ")") + ". Cipherwake checked the public trust surface independently of what your AI coder claims; this is the gate that should fire before the AI announces a deploy. Cipherwake does NOT verify app functionality.",
4062
+ scope_note: "ship_decision = worst-of(drift, route_assertions, deploy_health, secret_scan, cookie_invariants" + (strictPosture ? ", absolute_posture)" : ")") + ". `pass` means: trust/crypto posture stable + declared assertions hold + homepage healthy + no leaked secrets found + declared sensitive paths still gated. `pass` does NOT mean: every public-route inventory is current, nor that no content/authorization leak exists outside the assertion set. Surface-diff additions (new routes / scripts) emit at info severity for human review — they never gate. To make a route class gate, declare a glob assertion (e.g. `/preview/* expect:missing`). Cipherwake does NOT verify app functionality.",
4008
4063
  narrative: routeAssertions
4009
4064
  ? buildTrustDiffNarrative({
4010
4065
  deltaCount: deltas.length,
@@ -4020,6 +4075,13 @@ async function runTrustDiffCommand(args) {
4020
4075
  : undefined,
4021
4076
  ...assertionFields,
4022
4077
  ...postureFields,
4078
+ // R92 — every CIPHERWAKE_AI_GUARD_RESULT block now carries fresh_status
4079
+ // so MCP servers / Aider / Cursor / Claude Code can route programmatically:
4080
+ // applied → safe to interpret the posture as current
4081
+ // rate_limited / unauthenticated / unavailable → posture below is from cache,
4082
+ // do not interpret as a fresh measurement (the customer asked but didn't get it)
4083
+ // not_requested → caller didn't ask for fresh; cached is by design
4084
+ fresh_status: freshStatus,
4023
4085
  }));
4024
4086
  if (routeAssertions) {
4025
4087
  console.log(formatRouteAssertionsBlock(routeAssertions));
@@ -4029,9 +4091,17 @@ async function runTrustDiffCommand(args) {
4029
4091
  const { recordResults, recordSurfaceSnapshot } = await import(new URL("./statsTracker.js", import.meta.url).href);
4030
4092
  await recordResults(extractStatsEntries(routeAssertions));
4031
4093
  // Extract publicRoutes + thirdPartyHosts from the report for snapshot
4032
- const publicRoutes = Array.isArray(currentReport?.publicRoutes?.paths)
4094
+ // R91 (2026-06-06) — broader discovery: merge common-public probe
4095
+ // results with sitemap + homepage-anchor discovered routes so the
4096
+ // surface-diff catches NEW marketing/preview routes (the seatcheck
4097
+ // case from external dogfood feedback).
4098
+ const probedPublic = Array.isArray(currentReport?.publicRoutes?.paths)
4033
4099
  ? currentReport.publicRoutes.paths.filter((p) => p.classification === "public").map((p) => p.path)
4034
4100
  : [];
4101
+ const discovered = Array.isArray(routeAssertions?.discoveredRoutes)
4102
+ ? routeAssertions.discoveredRoutes
4103
+ : [];
4104
+ const publicRoutes = [...new Set([...probedPublic, ...discovered])].sort();
4035
4105
  const thirdPartyHosts = Array.isArray(currentReport?.publicDeps?.thirdParties)
4036
4106
  ? [...new Set(currentReport.publicDeps.thirdParties.map((t) => t.host).filter(Boolean))]
4037
4107
  : [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.16.29",
3
+ "version": "0.16.31",
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",