pqcheck 0.7.8 → 0.10.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 +34 -27
  2. package/bin/pqcheck.js +657 -62
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -8,10 +8,14 @@ npx pqcheck stripe.com
8
8
 
9
9
  Zero install. Works in any terminal with Node 18+. Free, no signup, no API key.
10
10
 
11
- The same scanner that powers [quantapact.com](https://quantapact.com), the browser extension, and the GitHub Action.
11
+ The same scanner that powers [cipherwake.io](https://cipherwake.io), the browser extension, and the GitHub Action.
12
12
 
13
13
  ---
14
14
 
15
+ ## What's new in 0.7.9
16
+
17
+ **CSP verdict + vendor labels on `pqcheck deps`.** The supply-chain table now shows a friendly vendor label (`New Relic · errors` / `Cloudflare · cdn` / `Adobe Fonts · fonts`) per host instead of raw `bam.nr-data.net`-style hostnames, plus a one-line site-wide CSP verdict above the table (`✗ No CSP enforcement` / `⚠ CSP is permissive` / `✓ Strict CSP enforced`). Same data shape ships on `/r/<domain>` and in the browser extension — cross-surface parity for the supply-chain story. See [CHANGELOG.md](./CHANGELOG.md).
18
+
15
19
  ## What's new in 0.7.8
16
20
 
17
21
  **Supply-chain change detection in CI** — `pqcheck deps <domain> --baseline file.json` compares the current third-party host list to a stored baseline. New hosts since the last accepted state are flagged `*NEW*` in the pretty table and `"isNew": true` in JSON. Add `--fail-on-new` to exit `4` if anything new appeared — the Polyfill.io-style CI gate that fails PRs introducing third-party scripts until you deliberately accept them with `--write-baseline`. Each row also shows an `SRI` column (on/off/n/a) so you can see which scripts allow silent vendor-side content swaps. See [CHANGELOG.md](./CHANGELOG.md).
@@ -38,10 +42,11 @@ Plus a full ASM check suite for credibility:
38
42
 
39
43
  ```
40
44
  npx pqcheck <domain> Scan + print human-readable report
41
- npx pqcheck lock <domain> Generate quantapact.lock (QXM) committable manifest
45
+ npx pqcheck lock <domain> Generate cipherwake.lock (QXM) committable manifest
42
46
  npx pqcheck deps <domain> Scan all third-party origins on the page (supply-chain HNDL)
43
47
  npx pqcheck diff <old.lock> <new.lock> Compare two QXM lockfiles; exit 2 on regression
44
48
  npx pqcheck history <domain> Show 90-day score history (sparkline + samples)
49
+ npx pqcheck changes <domain> Summarize public attack-surface changes in last 14 days
45
50
  npx pqcheck cert <file.pem> Analyze a local PEM/CRT cert file (offline, no network)
46
51
  ```
47
52
 
@@ -77,7 +82,7 @@ npx pqcheck --file domains.txt Bulk scan from a newline-separated
77
82
  ### Subcommand-specific flags
78
83
 
79
84
  **`pqcheck deps`:**
80
- - `--lock` — Also write `quantapact-deps.lock` + `.md`
85
+ - `--lock` — Also write `cipherwake-deps.lock` + `.md`
81
86
  - `-o <dir>` — Output directory for `--lock` files
82
87
  - `--max=<N>` — Max third parties to scan (default 20)
83
88
  - `--allowlist <file>` — Exit **3** if any third-party not in allowlist (CI vendor-risk gate)
@@ -156,13 +161,15 @@ Like SBOM, `package-lock.json`, or `cargo audit` outputs — track quantum expos
156
161
  ```bash
157
162
  npx pqcheck lock yourcompany.com
158
163
  # Writes:
159
- # quantapact.lock — stable JSON manifest
160
- # quantapact-report.md — human-readable summary (renders on GitHub)
164
+ # cipherwake.lock — stable JSON manifest
165
+ # cipherwake-report.md — human-readable summary (renders on GitHub)
161
166
  ```
162
167
 
163
168
  Commit both files. Use `npx pqcheck diff old.lock new.lock` in CI to surface regressions in PR comments.
164
169
 
165
- Schema documented at [quantapact.com/schemas/qxm/v1](https://quantapact.com/methodology/qxm).
170
+ > **Filename history.** This tool was previously named Quantapact and earlier versions wrote `quantapact.lock` + `quantapact-report.md`. Both names work forever — `pqcheck lock` auto-detects an existing legacy lockfile and overwrites it in place rather than silently creating a second file in your repo. New repos get the new `cipherwake.lock` default. No migration required.
171
+
172
+ Schema documented at [cipherwake.io/schemas/qxm/v1](https://cipherwake.io/methodology/qxm).
166
173
 
167
174
  ### Supply-chain dependency scanning
168
175
 
@@ -171,41 +178,41 @@ npx pqcheck deps stripe.com
171
178
  # Output: every third-party origin on stripe.com (analytics, CDN, fonts, etc.) graded for quantum risk
172
179
  ```
173
180
 
174
- Add `--lock` to write `quantapact-deps.lock` + `.md` for committing or PR comparison. Add `--allowlist file.txt` to gate CI on vendor approval.
181
+ Add `--lock` to write `cipherwake-deps.lock` + `.md` for committing or PR comparison. Add `--allowlist file.txt` to gate CI on vendor approval.
175
182
 
176
183
  ## Companion surfaces
177
184
 
178
- This CLI is one of four ways to consume the [Decryption Blast Radius API](https://quantapact.com/api):
185
+ This CLI is one of four ways to consume the [Decryption Blast Radius API](https://cipherwake.io/api):
179
186
 
180
187
  | Surface | Where |
181
188
  |---|---|
182
189
  | **CLI** (this package) | `npx pqcheck` |
183
190
  | **Browser extension** | Chrome Web Store / Firefox AMO / Edge — toolbar badge per tab + dependency analysis |
184
- | **GitHub Action** | [`quantapact/pqcheck/action@main`](https://github.com/quantapact/pqcheck/tree/main/action) — PR comments, SARIF upload, lockfile generation |
185
- | **Slack `/pqcheck`** | [Install on workspace](https://quantapact.com/install-slack) |
186
- | **Web** | [quantapact.com](https://quantapact.com) — share-friendly URLs at `/r/<domain>` |
191
+ | **GitHub Action** | [`cipherwake-io/pqcheck/action@main`](https://github.com/cipherwake-io/pqcheck/tree/main/action) — PR comments, SARIF upload, lockfile generation |
192
+ | **Slack `/pqcheck`** | [Install on workspace](https://cipherwake.io/install-slack) |
193
+ | **Web** | [cipherwake.io](https://cipherwake.io) — share-friendly URLs at `/r/<domain>` |
187
194
 
188
195
  ## Public API
189
196
 
190
- `pqcheck` is a wrapper around the public Quantapact API. You can also call the API directly:
197
+ `pqcheck` is a wrapper around the public Cipherwake API. You can also call the API directly:
191
198
 
192
199
  ```bash
193
- curl -s "https://www.quantapact.com/api/scan?domain=stripe.com" | jq '.grade, .score'
200
+ curl -s "https://www.cipherwake.io/api/scan?domain=stripe.com" | jq '.grade, .score'
194
201
  ```
195
202
 
196
- Full API reference at [quantapact.com/api](https://quantapact.com/api).
203
+ Full API reference at [cipherwake.io/api](https://cipherwake.io/api).
197
204
 
198
- **Rate limits:** 300 scans per hour per IP, 20 `--fresh` (force-refresh) scans per hour per IP. No API key required. Returns HTTP 429 if exceeded — back off and retry, or [let us know via the feedback form](https://quantapact.com/feedback) if you need higher limits (we're prioritizing the API tier based on real demand).
205
+ **Rate limits:** 300 scans per hour per IP, 20 `--fresh` (force-refresh) scans per hour per IP. No API key required. Returns HTTP 429 if exceeded — back off and retry, or [let us know via the feedback form](https://cipherwake.io/feedback) if you need higher limits (we're prioritizing the API tier based on real demand).
199
206
 
200
207
  ## Methodology
201
208
 
202
209
  Decryption Blast Radius scoring methodology is fully open. Component weights, PQC discount math, the "what we DON'T claim" sections, edge cases — all documented:
203
210
 
204
- - [Decryption Blast Radius](https://quantapact.com/methodology/decryption-blast-radius) — core methodology
205
- - [Score components](https://quantapact.com/methodology/score-components) — the 4-bar weighted breakdown + PQC discount
206
- - [QXM lockfile schema](https://quantapact.com/methodology/qxm) — committable manifest format
207
- - [Browser extension methodology](https://quantapact.com/methodology/browser-extension) — supply-chain HNDL detection logic
208
- - [Methodology library](https://quantapact.com/methodology) — full index
211
+ - [Decryption Blast Radius](https://cipherwake.io/methodology/decryption-blast-radius) — core methodology
212
+ - [Score components](https://cipherwake.io/methodology/score-components) — the 4-bar weighted breakdown + PQC discount
213
+ - [QXM lockfile schema](https://cipherwake.io/methodology/qxm) — committable manifest format
214
+ - [Browser extension methodology](https://cipherwake.io/methodology/browser-extension) — supply-chain HNDL detection logic
215
+ - [Methodology library](https://cipherwake.io/methodology) — full index
209
216
 
210
217
  ## Versioning + stability
211
218
 
@@ -215,20 +222,20 @@ The CLI follows the same policy — output formats are stable across minor versi
215
222
 
216
223
  ## Privacy
217
224
 
218
- `pqcheck` sends the domain you scan to `quantapact.com/api/scan` (so the TLS handshake can be performed from the public internet). No other data is sent — no email, no client-side identifier. The server logs anonymized analytics: domain, hashed IP (for rate limiting), user-agent. We don't track individual users across scans. See [quantapact.com/privacy](https://quantapact.com/privacy).
225
+ `pqcheck` sends the domain you scan to `cipherwake.io/api/scan` (so the TLS handshake can be performed from the public internet). No other data is sent — no email, no client-side identifier. The server logs anonymized analytics: domain, hashed IP (for rate limiting), user-agent. We don't track individual users across scans. See [cipherwake.io/privacy](https://cipherwake.io/privacy).
219
226
 
220
227
  ## CI integration
221
228
 
222
229
  ```yaml
223
230
  # .github/workflows/quantum-risk-gate.yml
224
- - name: Quantapact public-surface gate
231
+ - name: Cipherwake public-surface gate
225
232
  run: npx pqcheck@latest mycompany.com --threshold 7
226
233
  ```
227
234
 
228
- For richer integration (sticky PR comments, SARIF upload to Code Scanning, lockfile diff on regression), use the [GitHub Action](https://github.com/quantapact/pqcheck/tree/main/action):
235
+ For richer integration (sticky PR comments, SARIF upload to Code Scanning, lockfile diff on regression), use the [GitHub Action](https://github.com/cipherwake-io/pqcheck/tree/main/action):
229
236
 
230
237
  ```yaml
231
- - uses: quantapact/pqcheck/action@main
238
+ - uses: cipherwake-io/pqcheck/action@main
232
239
  with:
233
240
  domain: mycompany.com
234
241
  threshold: '7'
@@ -245,12 +252,12 @@ For richer integration (sticky PR comments, SARIF upload to Code Scanning, lockf
245
252
 
246
253
  ## License
247
254
 
248
- MIT. © 2026 Quantapact.
255
+ MIT. © 2026 Cipherwake.
249
256
 
250
257
  ---
251
258
 
252
- **Source:** [github.com/quantapact/pqcheck](https://github.com/quantapact/pqcheck)
259
+ **Source:** [github.com/cipherwake-io/pqcheck](https://github.com/cipherwake-io/pqcheck)
253
260
 
254
261
  **Changelog:** [CHANGELOG.md](./CHANGELOG.md) for version-by-version release notes.
255
262
 
256
- **Issues / feedback:** [quantapact.com/feedback](https://quantapact.com/feedback) or open an issue on the repo.
263
+ **Issues / feedback:** [cipherwake.io/feedback](https://cipherwake.io/feedback) or open an issue on the repo.
package/bin/pqcheck.js CHANGED
@@ -2,12 +2,48 @@
2
2
  // =============================================================================
3
3
  // pqcheck CLI — npx pqcheck <domain>
4
4
  // =============================================================================
5
- // Tiny wrapper around the public scan API at quantapact.com.
5
+ // Tiny wrapper around the public scan API at cipherwake.io.
6
6
  // Zero deps (uses node:fetch). Works under `npx pqcheck` without installation.
7
7
  // =============================================================================
8
8
 
9
- const API_BASE = process.env.PQCHECK_API_BASE || "https://quantapact.com";
10
- const VERSION = "0.7.8";
9
+ const API_BASE = process.env.PQCHECK_API_BASE || "https://cipherwake.io";
10
+ const VERSION = "0.10.0";
11
+
12
+ // API-key support — paid tiers (Starter $29 / Growth $79 / Scale $199) get
13
+ // per-account monthly quotas instead of the per-IP rate limit. Set via:
14
+ // export CIPHERWAKE_API_KEY=qpk_<32-hex>
15
+ // Anonymous CLI use still works (no env var → falls back to IP rate limit).
16
+ //
17
+ // QUANTAPACT_API_KEY is honored as a deprecated fallback for existing users
18
+ // (rebrand 2026-05-15). Will be removed in v1.0; in the meantime no break.
19
+ const QP_API_KEY = (process.env.CIPHERWAKE_API_KEY || process.env.QUANTAPACT_API_KEY || "").trim();
20
+
21
+ // Builds headers with optional Authorization. Use for every CLI → API call
22
+ // so a single env-var toggle authenticates every endpoint at once.
23
+ function apiHeaders(extra = {}) {
24
+ const h = { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION}`, ...extra };
25
+ if (QP_API_KEY) h.authorization = `Bearer ${QP_API_KEY}`;
26
+ return h;
27
+ }
28
+
29
+ // Helpful messaging when the server tells us auth/quota failed.
30
+ async function handleAuthError(resp) {
31
+ if (resp.status === 401) {
32
+ const body = await safeJSON(resp);
33
+ if (body?.error === "invalid_api_key") {
34
+ console.error(color("red", "CIPHERWAKE_API_KEY is invalid or revoked. Check https://cipherwake.io/account to rotate."));
35
+ return true;
36
+ }
37
+ }
38
+ if (resp.status === 429) {
39
+ const body = await safeJSON(resp);
40
+ if (body?.error === "monthly_quota_exceeded") {
41
+ console.error(color("red", `Monthly quota exceeded${body.detail ? ` — ${body.detail}` : ""}. Upgrade tier at https://cipherwake.io/pricing or wait until the 1st.`));
42
+ return true;
43
+ }
44
+ }
45
+ return false;
46
+ }
11
47
 
12
48
  const ANSI = {
13
49
  reset: "\x1b[0m",
@@ -25,9 +61,17 @@ const color = (c, s) => (supportsColor ? `${ANSI[c]}${s}${ANSI.reset}` : s);
25
61
  async function main() {
26
62
  const args = process.argv.slice(2);
27
63
 
28
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
64
+ // No-args entry: show a layman's quick-start before the full usage block.
65
+ // Behavior: argv.length === 0 prints the friendly intro then usage and exits 1.
66
+ // --help / -h prints only usage (no intro) and exits 0.
67
+ if (args.length === 0) {
68
+ printQuickStart();
69
+ printUsage();
70
+ process.exit(1);
71
+ }
72
+ if (args.includes("--help") || args.includes("-h")) {
29
73
  printUsage();
30
- process.exit(args.length === 0 ? 1 : 0);
74
+ process.exit(0);
31
75
  }
32
76
  if (args.includes("--version") || args.includes("-v")) {
33
77
  console.log(`pqcheck ${VERSION}`);
@@ -47,9 +91,15 @@ async function main() {
47
91
  if (args[0] === "history") {
48
92
  return runHistoryCommand(args.slice(1));
49
93
  }
94
+ if (args[0] === "changes") {
95
+ return runChangesCommand(args.slice(1));
96
+ }
50
97
  if (args[0] === "cert") {
51
98
  return runCertCommand(args.slice(1));
52
99
  }
100
+ if (args[0] === "watch") {
101
+ return runWatchCommand(args.slice(1));
102
+ }
53
103
 
54
104
  // Multi-domain support: positional args are domains.
55
105
  // --file reads additional domains from a newline-delimited file.
@@ -133,7 +183,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
133
183
  const qs = fresh ? `?domain=${encodeURIComponent(domain)}&force=1` : `?domain=${encodeURIComponent(domain)}`;
134
184
  const resp = await fetch(`${API_BASE}/api/scan${qs}`, {
135
185
  method: "GET",
136
- headers: { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION} (scan)` },
186
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (scan)` }),
137
187
  });
138
188
  if (!quiet && format === "text") process.stderr.write("\r\x1b[K");
139
189
  if (!resp.ok) {
@@ -232,7 +282,7 @@ async function runWatch({ domains, format, quiet, threshold, webhookUrl, interva
232
282
  try {
233
283
  const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}`, {
234
284
  method: "GET",
235
- headers: { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION} (watch)` },
285
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (watch)` }),
236
286
  });
237
287
  if (!resp.ok) continue;
238
288
  const report = await resp.json();
@@ -357,7 +407,14 @@ function printCsvRow(r) {
357
407
  console.log(cells.join(","));
358
408
  }
359
409
  function csvEscape(s) {
360
- const v = String(s ?? "");
410
+ // Spreadsheet-formula-injection defense (R2 finding): cells starting
411
+ // with =, +, -, @, TAB, or CR get treated as formulas by Excel/Sheets.
412
+ // Prefix with a literal-string ' to neutralize. Defense-in-depth even
413
+ // though domains are shape-validated upstream.
414
+ let v = String(s ?? "");
415
+ if (/^[=+\-@\t\r\n]/.test(v)) {
416
+ v = "'" + v;
417
+ }
361
418
  if (v.includes(",") || v.includes('"') || v.includes("\n")) {
362
419
  return '"' + v.replace(/"/g, '""') + '"';
363
420
  }
@@ -393,6 +450,13 @@ function printMarkdown(r, multi) {
393
450
  lines.push(`> ⚠ Public surface only. Internal Blast Radius is typically 12–40× this score.`);
394
451
  lines.push("");
395
452
  lines.push(`Full report: ${API_BASE}/?check=${encodeURIComponent(r.domain)} · Share: ${API_BASE}/r/${encodeURIComponent(r.domain)}`);
453
+ // Conversion CTA (generalbusiness.md principle #15): one-line activation
454
+ // path on every value-delivery moment. Pointed at /watch/<domain>.
455
+ // Audit-required 2026-05-14: copy matches current locked plan ($29 Starter
456
+ // not the old "watch free / weekly digest"). PR-comment context plants
457
+ // team-invitation idea for the eventual Growth tier upgrade.
458
+ lines.push(`📌 **Monitor ${r.domain} continuously?** ${API_BASE}/watch/${encodeURIComponent(r.domain)}`);
459
+ lines.push(`Cipherwake Starter $29/mo · 5 domains · daily scans + email alerts on cert/script/posture changes. Invite your team on Growth ($79/mo) when ready.`);
396
460
  if (multi) lines.push("\n---\n");
397
461
  console.log(lines.join("\n"));
398
462
  }
@@ -506,7 +570,25 @@ function printReport(r) {
506
570
  console.log("");
507
571
  }
508
572
 
509
- console.log(color("violet", ` → Full report: ${API_BASE}/?check=${encodeURIComponent(r.domain)}`));
573
+ // Provenance pill — "Tracked by Cipherwake since X · N observations". Trust
574
+ // signal that this isn't a one-shot probe but a historical record. Only
575
+ // renders if we actually have prior observations for the domain.
576
+ if (r.trackedSince) {
577
+ const trackedDate = String(r.trackedSince).slice(0, 10);
578
+ const obs = typeof r.observations === "number" && r.observations > 0 ? r.observations : null;
579
+ const obsLine = obs ? ` · ${obs} observation${obs === 1 ? "" : "s"}` : "";
580
+ console.log(color("dim", ` Tracked by Cipherwake since ${trackedDate}${obsLine}`));
581
+ console.log("");
582
+ }
583
+
584
+ // Conversion CTA (generalbusiness.md principle #15): every value-delivery
585
+ // moment surfaces the same /watch/<domain> activation path. Audit-required
586
+ // 2026-05-14: copy must match current locked revenue plan ($29 Starter,
587
+ // not the old "free 1-domain weekly digest").
588
+ console.log(color("violet", ` 📌 Monitor ${r.domain} daily: ${API_BASE}/watch/${encodeURIComponent(r.domain)}`));
589
+ console.log(color("dim", ` Cipherwake Starter $29/mo · 5 watched domains · email alerts · cancel anytime`));
590
+ console.log("");
591
+ console.log(color("dim", ` → Full report: ${API_BASE}/?check=${encodeURIComponent(r.domain)}`));
510
592
  console.log(color("dim", ` → Share this: ${API_BASE}/r/${encodeURIComponent(r.domain)}`));
511
593
  console.log(color("dim", ` → Compare two: ${API_BASE}/compare?a=${encodeURIComponent(r.domain)}&b=`));
512
594
  console.log("");
@@ -531,6 +613,31 @@ async function safeJSON(resp) {
531
613
  try { return await resp.json(); } catch { return null; }
532
614
  }
533
615
 
616
+ // Friendly first-time intro shown when the CLI is invoked with no args.
617
+ // Goal: tell a brand-new user what this tool does + give them ONE
618
+ // command to copy-paste, before the full usage block. Modeled on
619
+ // `curl`/`gh`/`vercel` first-run output (concise, action-oriented).
620
+ function printQuickStart() {
621
+ console.log(`
622
+ ${color("bold", "👋 Welcome to pqcheck")} ${color("dim", `(v${VERSION})`)}
623
+
624
+ Cipherwake's CLI grades any website's quantum-decryption risk —
625
+ the chance that a harvest-now-decrypt-later attack could read its
626
+ TLS traffic once quantum decryption arrives.
627
+
628
+ ${color("bold", "Try it:")}
629
+ ${color("dim", "$")} npx pqcheck chase.com
630
+
631
+ ${color("bold", "What you'll see:")} a single letter grade (A–F), the score components
632
+ (cipher class, cert lifetime, key rotation history, subdomain exposure),
633
+ top findings, and a link to the full interactive report.
634
+
635
+ ${color("bold", "Free + open methodology.")} No account needed for single-domain scans.
636
+ Add ${color("dim", "QUANTAPACT_API_KEY")} env var for higher rate limits + private results
637
+ (create one at ${color("dim", "https://cipherwake.io/signin")}).
638
+ `);
639
+ }
640
+
534
641
  function printUsage() {
535
642
  console.log(`
536
643
  ${color("bold", "pqcheck")} ${color("dim", `v${VERSION}`)}
@@ -539,11 +646,13 @@ Public Surface Blast Radius — quantum-decryption risk for any domain.
539
646
 
540
647
  ${color("bold", "Commands:")}
541
648
  npx pqcheck <domain> Scan + print human-readable report
542
- npx pqcheck lock <domain> Generate quantapact.lock (QXM) committable manifest
649
+ npx pqcheck lock <domain> Generate cipherwake.lock (QXM) committable manifest
543
650
  npx pqcheck deps <domain> Scan all third-party origins on the page (supply-chain HNDL)
544
651
  npx pqcheck diff <old.lock> <new.lock> Compare two QXM lockfiles; exit 2 on regression
545
652
  npx pqcheck history <domain> Show 90-day score history (sparkline + samples)
653
+ npx pqcheck changes <domain> Summarize public attack-surface changes in last 14 days
546
654
  npx pqcheck cert <file.pem> Analyze a local PEM/CRT cert file (offline, no network)
655
+ npx pqcheck watch <domain> Add a domain to your watched-domain list (requires CIPHERWAKE_API_KEY)
547
656
 
548
657
  ${color("bold", "Multi-domain:")}
549
658
  npx pqcheck a.com b.com c.com Multi-domain scan (positional)
@@ -568,7 +677,7 @@ ${color("bold", "Common flags:")}
568
677
 
569
678
  ${color("bold", "Subcommand-specific:")}
570
679
  pqcheck deps:
571
- --lock Also write quantapact-deps.lock + .md
680
+ --lock Also write cipherwake-deps.lock + .md
572
681
  -o <dir> Output directory for --lock files
573
682
  --max=<N> Max third parties to scan (default 20)
574
683
  --allowlist <file> Exit 3 if any third-party not in allowlist (CI gate)
@@ -597,14 +706,14 @@ ${color("bold", "Examples:")}
597
706
  npx pqcheck deps acme.com --baseline .pqcheck-baseline.json --write-baseline ${color("dim", "# capture initial state")}
598
707
  npx pqcheck deps acme.com --baseline .pqcheck-baseline.json --fail-on-new ${color("dim", "# fail PR on new third party")}
599
708
  npx pqcheck diff main.lock pr.lock ${color("dim", "# regression detection in PR")}
600
- npx pqcheck history quantapact.com
709
+ npx pqcheck history cipherwake.io
601
710
  npx pqcheck cert ./mycert.pem ${color("dim", "# offline cert analysis")}
602
711
  npx pqcheck --file domains.txt --format json > scans.ndjson
603
712
  npx pqcheck mybank.com --format sarif > pqcheck.sarif ${color("dim", "# upload to Code Scanning")}
604
713
  npx pqcheck mybank.com --gh-action ${color("dim", "# inline PR annotations")}
605
714
 
606
715
  Backed by the patented Decryption Blast Radius methodology.
607
- ${color("violet", "https://quantapact.com")}
716
+ ${color("violet", "https://cipherwake.io")}
608
717
  `);
609
718
  }
610
719
 
@@ -612,20 +721,46 @@ ${color("violet", "https://quantapact.com")}
612
721
  // `pqcheck lock` — QXM (Quantum Exposure Manifest) generator
613
722
  // =============================================================================
614
723
  // Generates two files committable to a git repo:
615
- // quantapact.lock — stable JSON manifest (machine-readable)
616
- // quantapact-report.md — human-readable summary (renders on GitHub)
724
+ // cipherwake.lock — stable JSON manifest (machine-readable)
725
+ // cipherwake-report.md — human-readable summary (renders on GitHub)
617
726
  //
618
727
  // Like SBOM / package-lock.json / cargo audit / snyk test outputs — devs commit
619
728
  // these to track quantum exposure as a first-class technical concern.
620
729
  //
730
+ // Filename history: this tool was previously named Quantapact, and earlier
731
+ // versions wrote `quantapact.lock` / `quantapact-report.md`. We permanently
732
+ // support reading EITHER filename; existing repos with the old name keep
733
+ // working forever. When re-locking in a directory that has the legacy file,
734
+ // we overwrite it in place rather than silently creating a new file alongside.
735
+ // New repos (no existing lockfile) get the new default `cipherwake.lock`.
736
+ //
621
737
  // Usage:
622
- // npx pqcheck lock <domain> Write to ./quantapact.lock + .md
738
+ // npx pqcheck lock <domain> Write to ./cipherwake.lock + .md
739
+ // (or preserves ./quantapact.lock if present)
623
740
  // npx pqcheck lock <domain> -o dir/ Write into a specific directory
624
741
  // npx pqcheck lock <domain> --stdout Print JSON to stdout (no files)
625
742
  // npx pqcheck lock Read domain from existing
626
- // quantapact.lock if present, else error
743
+ // cipherwake.lock OR quantapact.lock, else error
627
744
  // =============================================================================
628
745
 
746
+ // Discover an existing lockfile in `dir`, preferring the new name but
747
+ // accepting the legacy name. Returns { lockPath, mdPath, isLegacy } if found,
748
+ // or null if neither exists. Read-anywhere, write-back-to-same-name policy.
749
+ async function discoverExistingLockfile(fs, path, dir) {
750
+ const candidates = [
751
+ { lockName: "cipherwake.lock", mdName: "cipherwake-report.md", isLegacy: false },
752
+ { lockName: "quantapact.lock", mdName: "quantapact-report.md", isLegacy: true },
753
+ ];
754
+ for (const c of candidates) {
755
+ const lockPath = path.join(dir, c.lockName);
756
+ try {
757
+ await fs.access(lockPath);
758
+ return { lockPath, mdPath: path.join(dir, c.mdName), isLegacy: c.isLegacy };
759
+ } catch { /* try next */ }
760
+ }
761
+ return null;
762
+ }
763
+
629
764
  async function runLockCommand(args) {
630
765
  const fs = await import("node:fs/promises");
631
766
  const path = await import("node:path");
@@ -639,16 +774,26 @@ async function runLockCommand(args) {
639
774
  const positional = args.filter((a) => !a.startsWith("-") && a !== outDir);
640
775
  let domain = positional.length > 0 ? normalizeDomain(positional[0]) : null;
641
776
 
777
+ // Discover any existing lockfile (new or legacy). Used both for re-lock
778
+ // domain auto-detection AND to preserve the filename on write.
779
+ const existing = await discoverExistingLockfile(fs, path, outDir);
780
+
642
781
  if (!domain) {
643
- try {
644
- const existing = await fs.readFile(path.join(outDir, "quantapact.lock"), "utf8");
645
- const parsed = JSON.parse(existing);
646
- domain = parsed.domain;
647
- if (!stdout) {
648
- console.error(color("dim", `Re-locking from existing quantapact.lock (domain: ${domain})`));
782
+ if (existing) {
783
+ try {
784
+ const content = await fs.readFile(existing.lockPath, "utf8");
785
+ const parsed = JSON.parse(content);
786
+ domain = parsed.domain;
787
+ if (!stdout) {
788
+ const baseName = path.basename(existing.lockPath);
789
+ console.error(color("dim", `Re-locking from existing ${baseName} (domain: ${domain})`));
790
+ }
791
+ } catch {
792
+ console.error(color("red", `error: could not parse existing ${path.basename(existing.lockPath)}`));
793
+ process.exit(1);
649
794
  }
650
- } catch {
651
- console.error(color("red", "error: no domain provided and no existing quantapact.lock found"));
795
+ } else {
796
+ console.error(color("red", "error: no domain provided and no existing cipherwake.lock found"));
652
797
  console.error(color("dim", "Usage: npx pqcheck lock <domain>"));
653
798
  process.exit(1);
654
799
  }
@@ -665,7 +810,7 @@ async function runLockCommand(args) {
665
810
  try {
666
811
  const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}`, {
667
812
  method: "GET",
668
- headers: { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION} (lock)` },
813
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (lock)` }),
669
814
  });
670
815
  if (!stdout) process.stderr.write("\r\x1b[K");
671
816
  if (!resp.ok) {
@@ -688,9 +833,16 @@ async function runLockCommand(args) {
688
833
  return;
689
834
  }
690
835
 
691
- // Write both files
692
- const lockPath = path.join(outDir, "quantapact.lock");
693
- const mdPath = path.join(outDir, "quantapact-report.md");
836
+ // Write both files. Filename policy: if a legacy quantapact.lock already
837
+ // exists in this directory, overwrite it in place (preserve user's
838
+ // committed filename — no surprise renames in their repo). Otherwise
839
+ // default to the new cipherwake.lock.
840
+ const lockPath = existing
841
+ ? existing.lockPath
842
+ : path.join(outDir, "cipherwake.lock");
843
+ const mdPath = existing
844
+ ? existing.mdPath
845
+ : path.join(outDir, "cipherwake-report.md");
694
846
  const md = renderQxmMarkdown(manifest);
695
847
 
696
848
  try {
@@ -736,7 +888,7 @@ function buildQxmManifest(report, crypto) {
736
888
  );
737
889
 
738
890
  return {
739
- schema: "https://quantapact.com/schemas/qxm/v1",
891
+ schema: "https://cipherwake.io/schemas/qxm/v1",
740
892
  schemaVersion: 1,
741
893
  generator: `pqcheck-cli/${VERSION}`,
742
894
  generatedAt: report.generatedAt || new Date().toISOString(),
@@ -756,13 +908,13 @@ function buildQxmManifest(report, crypto) {
756
908
  components: report.components || null,
757
909
  evidence: {
758
910
  evidenceHash,
759
- methodology: "https://quantapact.com/methodology",
760
- shareableReport: `https://quantapact.com/r/${encodeURIComponent(report.domain)}`,
761
- badge: `https://quantapact.com/badge/${encodeURIComponent(report.domain)}.svg`,
911
+ methodology: "https://cipherwake.io/methodology",
912
+ shareableReport: `https://cipherwake.io/r/${encodeURIComponent(report.domain)}`,
913
+ badge: `https://cipherwake.io/badge/${encodeURIComponent(report.domain)}.svg`,
762
914
  },
763
915
  remediation: {
764
916
  tessera: tesseraNeeded ? "join-waitlist" : "not-needed",
765
- tesseraWaitlist: "https://quantapact.com/feedback?source=qxm-tessera-interest",
917
+ tesseraWaitlist: "https://cipherwake.io/feedback?source=qxm-tessera-interest",
766
918
  notes: tesseraNeeded
767
919
  ? "Findings include cryptographic exposure that Tessera SDK is being designed to remediate. Tessera is in development; join the waitlist to be notified when ready."
768
920
  : "No quantum-decryption-relevant findings requiring Tessera remediation at this time.",
@@ -775,7 +927,7 @@ function renderQxmMarkdown(m) {
775
927
  lines.push(`# Quantum Exposure Manifest — \`${m.domain}\``);
776
928
  lines.push("");
777
929
  lines.push(`> **Decryption Blast Radius:** ${m.score} / 10 (Grade ${m.grade}, ${m.scoreLabel})`);
778
- lines.push(`> Generated by [pqcheck](https://quantapact.com) at ${m.generatedAt}`);
930
+ lines.push(`> Generated by [pqcheck](https://cipherwake.io) at ${m.generatedAt}`);
779
931
  lines.push("");
780
932
  if (!m.reachable) {
781
933
  lines.push(`*${m.domain} was not reachable at scan time.*`);
@@ -852,7 +1004,8 @@ function renderQxmMarkdown(m) {
852
1004
  // Fetches the public HTML of the target domain, extracts third-party origins
853
1005
  // referenced via <script src>, <iframe src>, <link href>, <img src>, then runs
854
1006
  // /api/scan against each unique third party. Outputs a sorted summary + an
855
- // optional committable lockfile (quantapact-deps.lock).
1007
+ // optional committable lockfile (cipherwake-deps.lock; legacy quantapact-deps.lock
1008
+ // is overwritten in place if present, see write-path comments below).
856
1009
  //
857
1010
  // Parallel to the browser extension's Dependencies tab, exposed as a CLI for
858
1011
  // CI integration: gate PR builds on third-party crypto posture.
@@ -860,7 +1013,7 @@ function renderQxmMarkdown(m) {
860
1013
  // Usage:
861
1014
  // npx pqcheck deps <domain> Scan + print summary table
862
1015
  // npx pqcheck deps <domain> --json JSON output (pipe to jq, etc.)
863
- // npx pqcheck deps <domain> --lock Also write quantapact-deps.lock + .md
1016
+ // npx pqcheck deps <domain> --lock Also write cipherwake-deps.lock + .md
864
1017
  // npx pqcheck deps <domain> -o dir/ Output directory for --lock files
865
1018
  // npx pqcheck deps <domain> --max=20 Cap on third parties scanned (default 20)
866
1019
  // =============================================================================
@@ -932,12 +1085,15 @@ async function runDepsCommand(args) {
932
1085
  }
933
1086
 
934
1087
  if (!json) process.stderr.write(color("dim", `Fetching ${domain} HTML...`));
935
- const html = await fetchPageHTML(domain);
1088
+ const fetched = await fetchPageHTML(domain);
936
1089
  if (!json) process.stderr.write("\r\x1b[K");
937
- if (!html) {
1090
+ if (!fetched) {
938
1091
  console.error(color("red", `error: could not fetch https://${domain}/`));
939
1092
  process.exit(1);
940
1093
  }
1094
+ const { html, headerCsp } = fetched;
1095
+ const metaCsp = extractMetaCsp(html);
1096
+ const cspVerdict = classifyCsp(headerCsp, metaCsp);
941
1097
 
942
1098
  const refs = extractThirdPartyRefs(html, domain);
943
1099
  if (refs.length === 0) {
@@ -977,7 +1133,7 @@ async function runDepsCommand(args) {
977
1133
  batch.map(async (h) => {
978
1134
  try {
979
1135
  const r = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(h.host)}&source=cli-deps`, {
980
- headers: { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION} (deps)` },
1136
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (deps)` }),
981
1137
  });
982
1138
  if (!r.ok) return { ...h, types: Array.from(h.types), scan: null, error: `${r.status}` };
983
1139
  const body = await r.json();
@@ -1031,13 +1187,17 @@ async function runDepsCommand(args) {
1031
1187
 
1032
1188
  // Build manifest
1033
1189
  const manifest = {
1034
- $schema: "https://quantapact.com/schemas/deps/v1",
1035
- schemaVersion: "1.1", // bumped for SRI + baseline-diff fields
1190
+ $schema: "https://cipherwake.io/schemas/deps/v1",
1191
+ schemaVersion: "1.2", // bumped for CSP + vendor classification fields
1036
1192
  domain,
1037
1193
  scannedAt: new Date().toISOString(),
1038
1194
  tool: "pqcheck-cli",
1039
1195
  toolVersion: VERSION,
1040
1196
  summary,
1197
+ csp: {
1198
+ quality: cspVerdict.quality, // "absent" | "weak" | "strict"
1199
+ source: cspVerdict.source, // "header" | "meta" | null
1200
+ },
1041
1201
  baseline: baselineHosts ? {
1042
1202
  file: baselinePath,
1043
1203
  newHosts,
@@ -1049,6 +1209,7 @@ async function runDepsCommand(args) {
1049
1209
  types: r.types,
1050
1210
  occurrences: r.occurrences,
1051
1211
  sri: { allScriptsHaveSri: !r.anyMissingSri, allHttps: r.allHttps },
1212
+ vendor: classifyVendor(r.host), // { name, category } or null
1052
1213
  isNew: r.isNew || false,
1053
1214
  scan: r.scan,
1054
1215
  error: r.error,
@@ -1066,7 +1227,7 @@ async function runDepsCommand(args) {
1066
1227
  try {
1067
1228
  const fs2 = await import("node:fs/promises");
1068
1229
  const baselinePayload = {
1069
- $schema: "https://quantapact.com/schemas/deps-baseline/v1",
1230
+ $schema: "https://cipherwake.io/schemas/deps-baseline/v1",
1070
1231
  domain,
1071
1232
  capturedAt: new Date().toISOString(),
1072
1233
  toolVersion: VERSION,
@@ -1101,6 +1262,15 @@ async function runDepsCommand(args) {
1101
1262
  console.log("");
1102
1263
  console.log(` ${color("bold", "Supply-chain HNDL exposure")} for ${color("violet", domain)}`);
1103
1264
  console.log(` ${color("dim", `${summary.uniqueOrigins} unique third-party origins · ${summary.totalReferences} references · weakest: ${summary.weakestLink?.host ?? "—"} (${summary.weakestLink?.grade ?? "—"})`)}`);
1265
+ // CSP-quality summary line — single line, site-wide. Mirrors the banner the
1266
+ // extension shows on the Supply Chain tab.
1267
+ if (cspVerdict.quality === "absent") {
1268
+ console.log(` ${color("red", "✗ No CSP enforcement")} ${color("dim", "— vendor swaps go undetected by the browser")}`);
1269
+ } else if (cspVerdict.quality === "weak") {
1270
+ console.log(` ${color("yellow", "⚠ CSP is permissive")} ${color("dim", `— uses unsafe-inline / wildcards / data: (${cspVerdict.source})`)}`);
1271
+ } else if (cspVerdict.quality === "strict") {
1272
+ console.log(` ${color("green", "✓ Strict CSP enforced")} ${color("dim", `(${cspVerdict.source})`)}`);
1273
+ }
1104
1274
  if (baselineHosts) {
1105
1275
  const baselineSize = baselineHosts.size;
1106
1276
  if (baselineSize === 0) {
@@ -1113,19 +1283,33 @@ async function runDepsCommand(args) {
1113
1283
  }
1114
1284
  }
1115
1285
  console.log("");
1116
- console.log(` ${color("dim", "GRADE HOST PQC SRI TYPES")}`);
1117
- console.log(` ${color("dim", "───── ───────────────────────────────────────── ─── ─── ─────")}`);
1286
+ console.log(` ${color("dim", "GRADE HOST VENDOR (CATEGORY) PQC SRI TYPES")}`);
1287
+ console.log(` ${color("dim", "───── ───────────────────────────────────────── ────────────────────── ─── ─── ─────")}`);
1118
1288
  for (const r of results) {
1119
1289
  const gradeStr = r.scan?.grade ?? "?";
1120
1290
  const gradeColored = gradeStr === "A" ? color("green", gradeStr) : gradeStr === "F" || gradeStr === "D" ? color("red", gradeStr) : color("yellow", gradeStr);
1121
- const isNewMark = r.isNew ? color("yellow", " *NEW*") : "";
1122
1291
  const hostRaw = r.host + (r.isNew ? " *NEW*" : "");
1123
1292
  const host = hostRaw.length > 41 ? hostRaw.slice(0, 40) + "…" : (r.host.padEnd(r.isNew ? 35 : 41, " ") + (r.isNew ? color("yellow", " *NEW*") : ""));
1124
1293
  const pqc = r.scan?.hybridPQC ? color("green", "yes") : color("dim", "no ");
1125
1294
  const hasScript = (r.types || []).includes("script");
1126
1295
  const sriCell = !hasScript ? color("dim", "n/a") : r.anyMissingSri ? color("yellow", "off") : color("green", "on ");
1127
1296
  const types = r.types.join(",");
1128
- console.log(` ${gradeColored.padEnd(8, " ")} ${host} ${pqc} ${sriCell} ${color("dim", types)}`);
1297
+ // Vendor classification — "(New Relic · errors)" beats "bam.nr-data.net"
1298
+ // for comprehension. Padded to 22 chars so the table columns stay aligned.
1299
+ // Heuristic matches return `name: null` and just a category — render as
1300
+ // "(cdn — inferred)" to signal we're guessing without claiming certainty.
1301
+ const vendor = classifyVendor(r.host);
1302
+ let vendorStrRaw;
1303
+ if (vendor?.name) {
1304
+ vendorStrRaw = `${vendor.name} (${vendor.category})`;
1305
+ } else if (vendor?.category) {
1306
+ vendorStrRaw = `(${vendor.category} — inferred)`;
1307
+ } else {
1308
+ vendorStrRaw = "—";
1309
+ }
1310
+ const vendorTruncated = vendorStrRaw.length > 22 ? vendorStrRaw.slice(0, 21) + "…" : vendorStrRaw.padEnd(22, " ");
1311
+ const vendorColored = color("dim", vendorTruncated);
1312
+ console.log(` ${gradeColored.padEnd(8, " ")} ${host} ${vendorColored} ${pqc} ${sriCell} ${color("dim", types)}`);
1129
1313
  }
1130
1314
  console.log("");
1131
1315
  if (baselineHosts && baselineHosts.size > 0 && newHosts.length > 0) {
@@ -1136,8 +1320,19 @@ async function runDepsCommand(args) {
1136
1320
  console.log("");
1137
1321
 
1138
1322
  if (lock) {
1139
- const lockPath = path.join(outDir, "quantapact-deps.lock");
1140
- const mdPath = path.join(outDir, "quantapact-deps-report.md");
1323
+ // Filename policy mirrors `pqcheck lock`: prefer the new cipherwake-deps.lock,
1324
+ // but if a legacy quantapact-deps.lock exists in this dir, overwrite that one
1325
+ // in place so the user's repo doesn't suddenly grow a second lockfile.
1326
+ const fsSync = await import("node:fs");
1327
+ const legacyDepsLock = path.join(outDir, "quantapact-deps.lock");
1328
+ const legacyDepsMd = path.join(outDir, "quantapact-deps-report.md");
1329
+ const hasLegacy = fsSync.existsSync(legacyDepsLock);
1330
+ const lockPath = hasLegacy
1331
+ ? legacyDepsLock
1332
+ : path.join(outDir, "cipherwake-deps.lock");
1333
+ const mdPath = hasLegacy
1334
+ ? legacyDepsMd
1335
+ : path.join(outDir, "cipherwake-deps-report.md");
1141
1336
  try {
1142
1337
  await fs.mkdir(outDir, { recursive: true });
1143
1338
  await fs.writeFile(lockPath, JSON.stringify(manifest, null, 2));
@@ -1177,16 +1372,163 @@ async function fetchPageHTML(domain) {
1177
1372
  method: "GET",
1178
1373
  redirect: "follow",
1179
1374
  signal: ctrl.signal,
1180
- headers: { "User-Agent": `pqcheck-cli/${VERSION} (deps; +https://quantapact.com)` },
1375
+ headers: { "User-Agent": `pqcheck-cli/${VERSION} (deps; +https://cipherwake.io)` },
1181
1376
  });
1182
1377
  clearTimeout(t);
1183
1378
  if (!resp.ok) return null;
1184
- return await resp.text();
1379
+ const html = await resp.text();
1380
+ // Also capture the CSP header so the deps view can show the site-wide
1381
+ // enforcement level. Report-only header is ignored (not enforced).
1382
+ const headerCsp = resp.headers.get("content-security-policy") || "";
1383
+ return { html, headerCsp };
1185
1384
  } catch {
1186
1385
  return null;
1187
1386
  }
1188
1387
  }
1189
1388
 
1389
+ // Scrape <meta http-equiv="Content-Security-Policy" content="..."> from HTML
1390
+ // as a fallback when the response header is absent.
1391
+ function extractMetaCsp(html) {
1392
+ const re = /<meta[^>]*\bhttp-equiv\s*=\s*["']?content-security-policy["']?[^>]*\bcontent\s*=\s*["']([^"']+)["']/i;
1393
+ const m = html.match(re);
1394
+ return m ? m[1] : "";
1395
+ }
1396
+
1397
+ // Site-level CSP-quality classifier. Mirrors lib/serviceCatalog.ts and the
1398
+ // extension's classifyCsp(). Three buckets: absent / weak / strict.
1399
+ function classifyCsp(headerCsp, metaCsp) {
1400
+ const policy = ((headerCsp || "").trim() || (metaCsp || "").trim()).toLowerCase();
1401
+ if (!policy) return { quality: "absent", source: null, raw: "" };
1402
+ const weakSignals = ["'unsafe-inline'", "'unsafe-eval'", "'unsafe-hashes'", " * ", " *;", "data:", "blob:"];
1403
+ const weak = weakSignals.some((sig) => policy.includes(sig));
1404
+ return {
1405
+ quality: weak ? "weak" : "strict",
1406
+ source: (headerCsp || "").trim() ? "header" : "meta",
1407
+ raw: policy,
1408
+ };
1409
+ }
1410
+
1411
+ // Compact vendor catalog — covers ~50 most common third-party origins so the
1412
+ // pretty table can show "(New Relic · errors)" instead of just "bam.nr-data.net".
1413
+ // Mirror of lib/serviceCatalog.ts + extension/popup.js SERVICE_CATALOG; keep
1414
+ // the three in sync when adding vendors.
1415
+ const SERVICE_CATALOG = {
1416
+ "googletagmanager.com": { name: "Google Tag Manager", category: "analytics" },
1417
+ "google-analytics.com": { name: "Google Analytics", category: "analytics" },
1418
+ "googleadservices.com": { name: "Google Ads", category: "ads" },
1419
+ "doubleclick.net": { name: "Google DoubleClick", category: "ads" },
1420
+ "googleapis.com": { name: "Google APIs", category: "cdn" },
1421
+ "gstatic.com": { name: "Google Static", category: "cdn" },
1422
+ "recaptcha.net": { name: "Google reCAPTCHA", category: "captcha" },
1423
+ "youtube.com": { name: "YouTube", category: "social" },
1424
+ "cloudflare.com": { name: "Cloudflare", category: "cdn" },
1425
+ "cdnjs.cloudflare.com": { name: "Cloudflare cdnjs", category: "cdn" },
1426
+ "cloudflareinsights.com": { name: "Cloudflare Web Analytics", category: "analytics" },
1427
+ "challenges.cloudflare.com": { name: "Cloudflare Turnstile", category: "captcha" },
1428
+ "stripe.com": { name: "Stripe", category: "payments" },
1429
+ "js.stripe.com": { name: "Stripe Checkout", category: "payments" },
1430
+ "amazonaws.com": { name: "AWS S3", category: "cdn" },
1431
+ "cloudfront.net": { name: "AWS CloudFront", category: "cdn" },
1432
+ "clarity.ms": { name: "Microsoft Clarity", category: "analytics" },
1433
+ "azureedge.net": { name: "Azure CDN", category: "cdn" },
1434
+ "facebook.com": { name: "Facebook", category: "social" },
1435
+ "facebook.net": { name: "Facebook Pixel", category: "ads" },
1436
+ "fbcdn.net": { name: "Facebook CDN", category: "cdn" },
1437
+ "connect.facebook.net": { name: "Facebook Pixel", category: "ads" },
1438
+ "twitter.com": { name: "Twitter (X)", category: "social" },
1439
+ "linkedin.com": { name: "LinkedIn", category: "social" },
1440
+ "snap.licdn.com": { name: "LinkedIn Tracking", category: "ads" },
1441
+ "use.typekit.net": { name: "Adobe Fonts", category: "fonts" },
1442
+ "typekit.net": { name: "Adobe Fonts", category: "fonts" },
1443
+ "paypal.com": { name: "PayPal", category: "payments" },
1444
+ "auth0.com": { name: "Auth0", category: "auth" },
1445
+ "okta.com": { name: "Okta", category: "auth" },
1446
+ "intercomcdn.com": { name: "Intercom", category: "support" },
1447
+ "zendesk.com": { name: "Zendesk", category: "support" },
1448
+ "sentry.io": { name: "Sentry", category: "errors" },
1449
+ "browser.sentry-cdn.com": { name: "Sentry", category: "errors" },
1450
+ "js-agent.newrelic.com": { name: "New Relic", category: "errors" },
1451
+ "bam.nr-data.net": { name: "New Relic", category: "errors" },
1452
+ "datadoghq.com": { name: "Datadog", category: "errors" },
1453
+ "browser-intake-datadoghq.com": { name: "Datadog RUM", category: "errors" },
1454
+ "mailchimp.com": { name: "Mailchimp", category: "analytics" },
1455
+ "hubspot.com": { name: "HubSpot", category: "analytics" },
1456
+ "js.hubspot.com": { name: "HubSpot Tracking", category: "analytics" },
1457
+ "segment.com": { name: "Segment", category: "analytics" },
1458
+ "amplitude.com": { name: "Amplitude", category: "analytics" },
1459
+ "mxpnl.com": { name: "Mixpanel", category: "analytics" },
1460
+ "hotjar.com": { name: "Hotjar", category: "analytics" },
1461
+ "plausible.io": { name: "Plausible Analytics", category: "analytics" },
1462
+ "js.hcaptcha.com": { name: "hCaptcha", category: "captcha" },
1463
+ "cdn.jsdelivr.net": { name: "jsDelivr CDN", category: "cdn" },
1464
+ "unpkg.com": { name: "unpkg CDN", category: "cdn" },
1465
+ "code.jquery.com": { name: "jQuery CDN", category: "cdn" },
1466
+ "akamaihd.net": { name: "Akamai CDN", category: "cdn" },
1467
+ "akamaized.net": { name: "Akamai CDN", category: "cdn" },
1468
+ "fastly.net": { name: "Fastly CDN", category: "cdn" },
1469
+ "bootstrapcdn.com": { name: "Bootstrap CDN", category: "cdn" },
1470
+ "maxcdn.bootstrapcdn.com": { name: "Bootstrap CDN", category: "cdn" },
1471
+ "cookielaw.org": { name: "OneTrust Cookie Consent", category: "consent" },
1472
+ "cookiebot.com": { name: "Cookiebot", category: "consent" },
1473
+ "vimeo.com": { name: "Vimeo", category: "social" },
1474
+ "shopify.com": { name: "Shopify", category: "ecommerce" },
1475
+ "cdn.shopify.com": { name: "Shopify CDN", category: "ecommerce" },
1476
+ "tiktok.com": { name: "TikTok", category: "social" },
1477
+ };
1478
+
1479
+ // Conservative heuristic patterns — same as lib/serviceCatalog.ts + popup.js.
1480
+ // Used when a host isn't in the explicit catalog. Doesn't name the vendor
1481
+ // (we don't know), but assigns a high-confidence category like "cdn" or
1482
+ // "ads" so unknown hosts aren't blank.
1483
+ const HEURISTIC_PATTERNS = [
1484
+ // CDN
1485
+ { re: /^cdn[.-]/, category: "cdn" },
1486
+ { re: /^static\./, category: "cdn" },
1487
+ { re: /^assets\./, category: "cdn" },
1488
+ { re: /\.cloudfront\.net$/, category: "cdn" },
1489
+ { re: /\.akamai(?:edge|hd|ized)?\.net$/, category: "cdn" },
1490
+ { re: /\.fastly\.net$/, category: "cdn" },
1491
+ { re: /\.azureedge\.net$/, category: "cdn" },
1492
+ // Analytics / RUM
1493
+ { re: /^analytics?\./, category: "analytics" },
1494
+ { re: /^metrics?\./, category: "analytics" },
1495
+ { re: /^telemetry\./, category: "analytics" },
1496
+ { re: /^rum\./, category: "analytics" },
1497
+ // Ads
1498
+ { re: /^ads?[.-]/, category: "ads" },
1499
+ { re: /^adserver\./, category: "ads" },
1500
+ { re: /^pubads\./, category: "ads" },
1501
+ { re: /\.advertising\./, category: "ads" },
1502
+ // Consent / cookies
1503
+ { re: /^consent\./, category: "consent" },
1504
+ { re: /^cookies?\./, category: "consent" },
1505
+ { re: /^gdpr\./, category: "consent" },
1506
+ // Fonts
1507
+ { re: /^fonts?\./, category: "fonts" },
1508
+ // Errors / monitoring
1509
+ { re: /^sentry[.-]/, category: "errors" },
1510
+ ];
1511
+
1512
+ function classifyVendor(host) {
1513
+ if (!host) return null;
1514
+ const lower = host.toLowerCase();
1515
+ if (SERVICE_CATALOG[lower]) return SERVICE_CATALOG[lower];
1516
+ for (const pattern of Object.keys(SERVICE_CATALOG)) {
1517
+ if (lower === pattern || lower.endsWith("." + pattern)) {
1518
+ return SERVICE_CATALOG[pattern];
1519
+ }
1520
+ }
1521
+ for (const { re, category } of HEURISTIC_PATTERNS) {
1522
+ if (re.test(lower)) {
1523
+ // Inferred — no vendor name, just the category. CLI consumers (the
1524
+ // pretty table + JSON output) check `name === null` to distinguish
1525
+ // from explicit catalog matches.
1526
+ return { name: null, category };
1527
+ }
1528
+ }
1529
+ return null;
1530
+ }
1531
+
1190
1532
  function extractThirdPartyRefs(html, targetDomain) {
1191
1533
  const out = [];
1192
1534
  const targetRoot = registeredDomain(targetDomain);
@@ -1323,6 +1665,67 @@ function depsManifestToMarkdown(m) {
1323
1665
  // SARIF + GitHub Action output formats
1324
1666
  // =============================================================================
1325
1667
 
1668
+ // Derive a SARIF-stable rule ID from a finding. Prefer the registry's stable
1669
+ // `id` (e.g., "tls.rsa_kex_fallback") so the same finding gets the same
1670
+ // ruleId every run — without it, GitHub Code Scanning treats a reordered
1671
+ // finding list as a fresh batch of new findings, blowing up the triage UX.
1672
+ // Short non-crypto hash for SARIF rule-ID disambiguation. djb2-style.
1673
+ // Pure JS so it works in both Node and the CLI bundle. 8-char hex output
1674
+ // is enough entropy to disambiguate distinct legacy findings without
1675
+ // becoming churn-prone — same input string always produces the same hash.
1676
+ function shortHash(input) {
1677
+ let h = 5381;
1678
+ for (let i = 0; i < input.length; i++) {
1679
+ h = ((h << 5) + h + input.charCodeAt(i)) >>> 0;
1680
+ }
1681
+ return h.toString(16).padStart(8, "0");
1682
+ }
1683
+
1684
+ function stableRuleId(f) {
1685
+ if (f && typeof f.id === "string" && f.id.length > 0) {
1686
+ // Normalize: if a registry ID already carries the `pqcheck.` prefix
1687
+ // (which happened before this helper existed), don't double-prefix
1688
+ // it into `pqcheck.pqcheck.tls...`. Strip and reattach.
1689
+ const id = f.id.replace(/^pqcheck\./i, "");
1690
+ return `pqcheck.${id}`;
1691
+ }
1692
+ // Fallback for legacy findings emitted without the registry — slug the
1693
+ // title. Still stable across runs (same input → same output).
1694
+ const title = f?.title || "finding";
1695
+ const detail = f?.detail || "";
1696
+ const slug = title
1697
+ .toLowerCase()
1698
+ .replace(/[^a-z0-9]+/g, "_")
1699
+ .replace(/^_+|_+$/g, "")
1700
+ .slice(0, 48);
1701
+ // GPT adversarial review 2026-05-12: legacy findings with similar
1702
+ // titles ("No HSTS header" vs "No HSTS Header!") both slug to
1703
+ // "no_hsts_header" and would collide in SARIF, causing GitHub Code
1704
+ // Scanning to merge unrelated findings. Append a short hash of the
1705
+ // full (title + detail) so semantically-different findings get
1706
+ // distinct rule IDs even when their slugged titles match.
1707
+ const disambiguator = shortHash(`${title}|${detail}`);
1708
+ if (slug.length === 0) {
1709
+ return `pqcheck.legacy.unnamed.${disambiguator}`;
1710
+ }
1711
+ return `pqcheck.legacy.${slug}.${disambiguator}`;
1712
+ }
1713
+
1714
+ // Deduplicate the `rules` array — multiple findings can share the same
1715
+ // underlying rule (e.g., two cert findings both pinned to `cert.expired`).
1716
+ // SARIF's rule list must contain each rule once.
1717
+ function dedupeRules(findings) {
1718
+ const seen = new Set();
1719
+ const out = [];
1720
+ for (const f of findings) {
1721
+ const id = stableRuleId(f);
1722
+ if (seen.has(id)) continue;
1723
+ seen.add(id);
1724
+ out.push(f);
1725
+ }
1726
+ return out;
1727
+ }
1728
+
1326
1729
  function reportToSarif(report) {
1327
1730
  // SARIF 2.1.0 minimal schema for security findings.
1328
1731
  // Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
@@ -1336,9 +1739,15 @@ function reportToSarif(report) {
1336
1739
  driver: {
1337
1740
  name: "pqcheck",
1338
1741
  version: VERSION,
1339
- informationUri: "https://quantapact.com",
1340
- rules: findings.map((f, i) => ({
1341
- id: `pqcheck-${i + 1}`,
1742
+ informationUri: "https://cipherwake.io",
1743
+ // Stable rule IDs — anchored to the finding's registry id (e.g.
1744
+ // "tls.rsa_kex_fallback") when present, otherwise a slug derived
1745
+ // from the title. Previously used positional pqcheck-${i+1} which
1746
+ // made GitHub Code Scanning see every reorder as a "new" finding,
1747
+ // poisoning the dedup/triage UX. Stable IDs let Code Scanning
1748
+ // recognize the same rule across runs.
1749
+ rules: dedupeRules(findings).map((f) => ({
1750
+ id: stableRuleId(f),
1342
1751
  name: (f.title || "finding").replace(/[^A-Za-z0-9]/g, "_"),
1343
1752
  shortDescription: { text: f.title || "finding" },
1344
1753
  fullDescription: { text: f.detail || f.title || "finding" },
@@ -1346,8 +1755,8 @@ function reportToSarif(report) {
1346
1755
  })),
1347
1756
  },
1348
1757
  },
1349
- results: findings.map((f, i) => ({
1350
- ruleId: `pqcheck-${i + 1}`,
1758
+ results: findings.map((f) => ({
1759
+ ruleId: stableRuleId(f),
1351
1760
  level: sevMap[f.severity] || "note",
1352
1761
  message: { text: `${f.title || "finding"}${f.detail ? ` — ${f.detail}` : ""}` },
1353
1762
  // GitHub Code Scanning requires file: scheme (or relative path) for
@@ -1355,7 +1764,7 @@ function reportToSarif(report) {
1355
1764
  // relative path so findings show up cleanly in the Security tab.
1356
1765
  locations: [{
1357
1766
  physicalLocation: {
1358
- artifactLocation: { uri: `quantapact-scan/${report.domain || "unknown"}.txt` },
1767
+ artifactLocation: { uri: `cipherwake-scan/${report.domain || "unknown"}.txt` },
1359
1768
  region: { startLine: 1, startColumn: 1 },
1360
1769
  },
1361
1770
  }],
@@ -1364,7 +1773,7 @@ function reportToSarif(report) {
1364
1773
  score: report.score,
1365
1774
  grade: report.grade,
1366
1775
  severity: f.severity,
1367
- reportUrl: `https://www.quantapact.com/r/${report.domain || ""}`,
1776
+ reportUrl: `https://www.cipherwake.io/r/${report.domain || ""}`,
1368
1777
  },
1369
1778
  })),
1370
1779
  properties: {
@@ -1385,10 +1794,10 @@ function printGitHubActionAnnotations(report) {
1385
1794
  if (report._meta?.degraded) {
1386
1795
  const reason = String(report._meta.degradedReason || "live probe failed").replace(/[\r\n]/g, " ").replace(/::/g, ":");
1387
1796
  const since = String(report._meta.lastUpdated || "unknown");
1388
- console.log(`::warning title=Quantapact: cached score (live probe failed)::Showing last known-good score from ${since}. Reason: ${reason}. Re-run shortly for a fresh probe.`);
1797
+ console.log(`::warning title=Cipherwake: cached score (live probe failed)::Showing last known-good score from ${since}. Reason: ${reason}. Re-run shortly for a fresh probe.`);
1389
1798
  }
1390
1799
  // Top-line score/grade as a notice
1391
- console.log(`::notice title=Quantapact: ${report.domain}::Grade ${report.grade || "?"} · score ${report.score ?? "?"} / 10`);
1800
+ console.log(`::notice title=Cipherwake: ${report.domain}::Grade ${report.grade || "?"} · score ${report.score ?? "?"} / 10`);
1392
1801
  for (const f of findings) {
1393
1802
  const cmd = sevMap[f.severity] || "notice";
1394
1803
  const title = (f.title || "finding").replace(/[\r\n]/g, " ");
@@ -1420,7 +1829,7 @@ async function runHistoryCommand(args) {
1420
1829
  let h;
1421
1830
  try {
1422
1831
  const r = await fetch(`${API_BASE}/api/history?domain=${encodeURIComponent(domain)}&days=${days}`, {
1423
- headers: { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION} (history)` },
1832
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (history)` }),
1424
1833
  });
1425
1834
  if (!r.ok) {
1426
1835
  console.error(color("red", `error: ${r.status} ${r.statusText}`));
@@ -1480,6 +1889,84 @@ async function runHistoryCommand(args) {
1480
1889
  console.log("");
1481
1890
  }
1482
1891
 
1892
+ // =============================================================================
1893
+ // `pqcheck changes <domain>` — surface observation-table deltas for a domain
1894
+ // =============================================================================
1895
+ // Hits /api/changes-summary which aggregates the new observation tables
1896
+ // shipped 2026-05-13 (subdomain_observations, script_observations,
1897
+ // posture_snapshots, cert_observations). Returns "N attack-surface changes
1898
+ // in last 14d" + breakdown. Devs use this in CI ("did anything change since
1899
+ // yesterday?") and in PR descriptions ("attached: 3 changes detected since
1900
+ // last week").
1901
+
1902
+ async function runChangesCommand(args) {
1903
+ const json = args.includes("--json");
1904
+ const positional = args.filter((a) => !a.startsWith("-"));
1905
+ const domain = positional.length > 0 ? normalizeDomain(positional[0]) : null;
1906
+ if (!domain || !isValidDomain(domain)) {
1907
+ console.error(color("red", "error: pqcheck changes requires a valid domain"));
1908
+ console.error(color("dim", "Usage: npx pqcheck changes <domain> [--json]"));
1909
+ process.exit(1);
1910
+ }
1911
+
1912
+ let summary;
1913
+ try {
1914
+ const r = await fetch(`${API_BASE}/api/changes-summary?domain=${encodeURIComponent(domain)}`, {
1915
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (changes)` }),
1916
+ });
1917
+ if (!r.ok) {
1918
+ console.error(color("red", `error: ${r.status} ${r.statusText}`));
1919
+ process.exit(1);
1920
+ }
1921
+ summary = await r.json();
1922
+ } catch (err) {
1923
+ console.error(color("red", `error: ${err.message}`));
1924
+ process.exit(1);
1925
+ }
1926
+
1927
+ if (json) {
1928
+ console.log(JSON.stringify(summary, null, 2));
1929
+ return;
1930
+ }
1931
+
1932
+ const tracked = summary.trackedSince;
1933
+ const total = summary.changes?.last14d ?? 0;
1934
+ const b = summary.breakdown ?? {};
1935
+ console.log("");
1936
+ console.log(` ${color("bold", domain)} ${color("dim", "·")} attack-surface change summary`);
1937
+
1938
+ if (!tracked) {
1939
+ console.log(color("dim", " No observation history yet. Run `npx pqcheck " + domain + "` to start accumulating."));
1940
+ console.log("");
1941
+ return;
1942
+ }
1943
+
1944
+ const trackedDate = String(tracked).slice(0, 10);
1945
+ console.log(color("dim", ` Tracking since ${trackedDate}`));
1946
+ console.log("");
1947
+
1948
+ if (total === 0) {
1949
+ console.log(` ${color("green", "✓")} No public attack-surface changes detected in the last 14 days.`);
1950
+ console.log("");
1951
+ return;
1952
+ }
1953
+
1954
+ console.log(` ${color("yellow", "⚠")} ${color("bold", total)} change${total === 1 ? "" : "s"} detected in the last 14 days:`);
1955
+ console.log("");
1956
+ if (b.newSubdomains14d) {
1957
+ console.log(` ${color("violet", "•")} ${color("bold", b.newSubdomains14d)} new subdomain${b.newSubdomains14d === 1 ? "" : "s"} observed in CT logs or live scans`);
1958
+ }
1959
+ if (b.newScripts14d) {
1960
+ console.log(` ${color("violet", "•")} ${color("bold", b.newScripts14d)} new third-party script host${b.newScripts14d === 1 ? "" : "s"} loaded`);
1961
+ }
1962
+ if (b.newCertKeys14d) {
1963
+ console.log(` ${color("violet", "•")} ${color("bold", b.newCertKeys14d)} new SPKI fingerprint${b.newCertKeys14d === 1 ? "" : "s"} (cert rotation with new key)`);
1964
+ }
1965
+ console.log("");
1966
+ console.log(color("dim", ` Full changelog: ${API_BASE}/domain/${domain}/security-changelog`));
1967
+ console.log("");
1968
+ }
1969
+
1483
1970
  // =============================================================================
1484
1971
  // `pqcheck diff` — diff two QXM lockfiles
1485
1972
  // =============================================================================
@@ -1509,7 +1996,7 @@ async function runDiffCommand(args) {
1509
1996
  }
1510
1997
 
1511
1998
  console.log("");
1512
- console.log(` ${color("bold", "Quantapact lockfile diff")}`);
1999
+ console.log(` ${color("bold", "Cipherwake lockfile diff")}`);
1513
2000
  console.log(` ${color("dim", `${positional[0]} → ${positional[1]}`)}`);
1514
2001
  console.log("");
1515
2002
  if (diff.scoreChange !== null) {
@@ -1580,6 +2067,114 @@ function computeLockDiff(oldLock, newLock) {
1580
2067
  // `pqcheck cert <pem-file>` — analyze a local cert file (offline)
1581
2068
  // =============================================================================
1582
2069
 
2070
+ // `pqcheck watch <domain>` — adds the given domain to the user's watched-
2071
+ // domain list via the authenticated /api/watched-domains POST. Requires
2072
+ // QUANTAPACT_API_KEY env var. Closes the CLI ↔ account loop: developers
2073
+ // who use the CLI can now opt into persistent monitoring from the same
2074
+ // surface without leaving the terminal.
2075
+ async function runWatchCommand(args) {
2076
+ const positional = args.filter((a) => !a.startsWith("-"));
2077
+ if (positional.length === 0) {
2078
+ console.error(color("red", "error: pqcheck watch requires a domain"));
2079
+ console.error(color("dim", "Usage: npx pqcheck watch <domain>"));
2080
+ console.error("");
2081
+ console.error(color("dim", "Example: npx pqcheck watch chase.com"));
2082
+ process.exit(1);
2083
+ }
2084
+ if (!QP_API_KEY) {
2085
+ const rawDomain = positional[0] ? String(positional[0]).trim().toLowerCase() : "";
2086
+ const looksLikeDomain = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(rawDomain);
2087
+ console.error(color("red", "error: pqcheck watch requires CIPHERWAKE_API_KEY (paid tier)"));
2088
+ console.error("");
2089
+ console.error(color("dim", "Two ways to add this domain:"));
2090
+ if (looksLikeDomain) {
2091
+ console.error(color("dim", ` 1. Sign up + click "Watch" in your browser:`));
2092
+ console.error(color("dim", ` ${API_BASE}/watch/${rawDomain}`));
2093
+ } else {
2094
+ console.error(color("dim", ` 1. Sign up + click "Watch" in your browser:`));
2095
+ console.error(color("dim", ` ${API_BASE}/watch/<your-domain>`));
2096
+ }
2097
+ console.error(color("dim", ` 2. Stay on the CLI — sign up + rotate an API key:`));
2098
+ console.error(color("dim", ` ${API_BASE}/signin`));
2099
+ console.error(color("dim", ` ${API_BASE}/account (rotate key)`));
2100
+ console.error(color("dim", ` export CIPHERWAKE_API_KEY=qpk_...`));
2101
+ process.exit(1);
2102
+ }
2103
+
2104
+ const raw = positional[0];
2105
+ const domain = normalizeDomain(raw);
2106
+ if (!isValidDomain(domain)) {
2107
+ console.error(color("red", `error: '${raw}' is not a valid domain`));
2108
+ process.exit(1);
2109
+ }
2110
+
2111
+ console.log("");
2112
+ console.log(color("violet", ` 📌 Adding ${domain} to your watched-domain list…`));
2113
+
2114
+ let resp;
2115
+ try {
2116
+ resp = await fetch(`${API_BASE}/api/watched-domains`, {
2117
+ method: "POST",
2118
+ headers: {
2119
+ "content-type": "application/json",
2120
+ "authorization": "Bearer " + QP_API_KEY,
2121
+ },
2122
+ body: JSON.stringify({ domain }),
2123
+ });
2124
+ } catch (err) {
2125
+ console.error(color("red", ` network error: ${err.message ?? err}`));
2126
+ process.exit(1);
2127
+ }
2128
+
2129
+ if (resp.status === 401 || resp.status === 403) {
2130
+ console.error(color("red", ` authentication failed (HTTP ${resp.status})`));
2131
+ console.error(color("dim", ` Your API key may be invalid or revoked. Regenerate at ${API_BASE}/account`));
2132
+ process.exit(1);
2133
+ }
2134
+
2135
+ let out = {};
2136
+ try { out = await resp.json(); } catch { /* ignore */ }
2137
+
2138
+ if (!resp.ok) {
2139
+ if (out.error === "domain_already_watched") {
2140
+ console.log(color("dim", ` ${domain} is already on your watched-domain list.`));
2141
+ console.log(color("dim", ` Manage at: ${API_BASE}/account`));
2142
+ process.exit(0);
2143
+ }
2144
+ if (out.error === "tier_cap_exceeded") {
2145
+ console.error(color("red", ` ${out.message || "Tier cap reached."}`));
2146
+ console.error(color("dim", ` See pricing: ${API_BASE}/pricing`));
2147
+ process.exit(1);
2148
+ }
2149
+ if (out.error === "invalid_domain") {
2150
+ console.error(color("red", ` ${domain} is not a valid hostname.`));
2151
+ process.exit(1);
2152
+ }
2153
+ console.error(color("red", ` add failed: ${out.message || out.error || `HTTP ${resp.status}`}`));
2154
+ process.exit(1);
2155
+ }
2156
+
2157
+ console.log("");
2158
+ console.log(color("green", ` ✓ Now watching ${domain}.`));
2159
+ console.log("");
2160
+ if (out.verificationInstructions) {
2161
+ const v = out.verificationInstructions;
2162
+ console.log(color("bold", " Next: verify ownership"));
2163
+ console.log(color("dim", ` Pick ONE method:`));
2164
+ console.log("");
2165
+ console.log(color("dim", ` DNS TXT — name: ${v.dnsTxt?.recordName}`));
2166
+ console.log(color("dim", ` value: ${v.dnsTxt?.recordValue}`));
2167
+ console.log("");
2168
+ console.log(color("dim", ` HTTP file — url: ${v.wellKnown?.url}`));
2169
+ console.log(color("dim", ` body: ${v.wellKnown?.body}`));
2170
+ console.log("");
2171
+ console.log(color("dim", ` After adding the record, click 'Verify now' at ${API_BASE}/account`));
2172
+ console.log(color("dim", ` or run: npx pqcheck watch ${domain} --verify (coming soon)`));
2173
+ }
2174
+ console.log("");
2175
+ process.exit(0);
2176
+ }
2177
+
1583
2178
  async function runCertCommand(args) {
1584
2179
  const fs = await import("node:fs/promises");
1585
2180
  const crypto = await import("node:crypto");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.7.8",
3
+ "version": "0.10.0",
4
4
  "description": "Decryption Blast Radius scanner — find out how much of your data unlocks when quantum decryption arrives.",
5
5
  "keywords": [
6
6
  "post-quantum",
@@ -17,15 +17,15 @@
17
17
  "crypto-audit",
18
18
  "crypto-inventory"
19
19
  ],
20
- "homepage": "https://quantapact.com",
21
- "bugs": "https://quantapact.com",
20
+ "homepage": "https://cipherwake.io",
21
+ "bugs": "https://cipherwake.io",
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "https://github.com/quantapact/pqcheck.git",
24
+ "url": "https://github.com/cipherwake-io/pqcheck.git",
25
25
  "directory": "cli"
26
26
  },
27
27
  "license": "MIT",
28
- "author": "Quantapact",
28
+ "author": "Cipherwake",
29
29
  "type": "module",
30
30
  "engines": {
31
31
  "node": ">=18"