pqcheck 0.16.30 → 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.30** — Three asks from external dogfood feedback after a draft-route resolver bug shipped 9 new `/preview/*` routes to prod undetected. **(1)** Broader surface-diff: sitemap.xml + homepage-anchor discovery feed into the snapshot so "+9 new public routes since baseline" surfaces at info severity (default-quiet, never gates). **(2)** Glob support: `.cipherwake.json` now accepts `/preview/*` and `/admin/**` patterns escalation is opt-in via declared assertion. **(3)** Scope honesty: `scope_note` now explicit that `pass` doesn't claim every-route inventory is current. The customer's "low-noise discipline is the asset" preserved: route-surface drift is info-only; gating requires explicit declared assertions. [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)) {
@@ -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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.16.30",
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",