Close 7 of 9 pre-launch teardown findings: hash-chain the Mode B audit log, make the licence enforceable, restore the readiness gate - #326
Conversation
`BUILTIN_PATTERNS` is the complete 53-pattern set — `lib/classifier/patterns.ts` spreads `...CMMC_PATTERNS` and `...HIPAA_PATTERNS` into it. Both browser scan consumers spread them alongside it a second time: lib/reports/snapshot-from-scan.ts LOCAL_ENGINES lib/scan/local-engine.ts ALL_PATTERNS Ninety pattern evaluations for fifty-three patterns. `scanForSnapshot` keys findings by pattern name and SUMS collisions (`existing.count += count`), so nothing deduped them: every CUI and PHI finding on the public /demo was reported at exactly 2x, and those doubled counts were POSTed to `/api/report/snapshot-lead` as the lead's risk profile — inflating precisely the categories the $499 report is sold on. `local-engine.ts` also published the array length to the UI as `patternsChecked`, so /demo printed "90" a few hundred pixels below its own header printing "53" from `PATTERN_COUNT`. CLAUDE.md's NEVER-DO list names that failure exactly: the pattern files are in the public repo and a buyer who counts them arrives at 53. `lib/detection/engines.ts` was written to delete this exact 90 and its comment names the number. It fixed the CONSTANT. `engines.test.ts` locks the CONSTANT. Nothing locked the CONSUMERS, and they were green the whole time. New guard `lib/detection/__tests__/engine-registry-single-source.test.ts` holds the line three ways — on the shipped arrays, on observed scan output, and on the source text so the next re-concatenation fails the build before anyone measures it. Both sites were self-tested by reintroducing them individually: the guard reports the offending file by name and the CAGE and ZIP counts go 1 -> 2. Also in this change: - docs/audit/PRE-LAUNCH-TEARDOWN-2026-09-02.html — the full adversarial teardown this fix came out of: five interaction surfaces, a GSec LLC revenue teardown, a monetization rebuild, a 180-day operations calendar, and a verification matrix anchoring every assertion to a file, a line, or a stated assumption. Live HTTP probing of houndshield.com was refused by the audit environment's egress policy; nothing in the document claims a live response. - .claude/rules/stack.md — two lines the teardown proved wrong. Migrations read "001-034" against 037 on disk. `proxy/` was described as an "HTTPS proxy" two lines above "HTTP proxy server"; server.ts is an Express app on plain HTTP with no CONNECT handler and no TLS listener. The entry now also records that proxy_events carries no hash chain and that licence enforcement is not wired, so neither is rediscovered as a surprise mid-sale. - tasks/lessons.md, tasks/todo.md — the generalisable lessons and the nine findings left open, seven of which are founder dashboard actions. Gates: tsc --noEmit clean, eslint 0 errors, 3099/3099 tests across 223 files, coverage gate passed, `npm run build` succeeds, verify-no-leaks self-test and scan both pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5
The teardown inferred from the repository that Vercel framework detection was failing at the repo root and that nothing had deployed since #288. Checking the Vercel API disproved it: `dpl_5aN5WbocU36ow…` carries `target: production`, `state: READY` and commit `b88b7ee` — the exact HEAD the teardown was written against. The project's Root Directory is already set correctly. Retracted in place rather than softened, and the finding it became is stronger. What the API surfaced instead is invisible from the repository: three deployments with `target: production` and `state: ERROR` inside one 25-minute window — `bfcbe54` (dpl_8c5WRb1rjLVQ…), `9c9f2b9` (dpl_EXWymKuPU6fo…) and `ba8bf29` (dpl_8gJMNKSPXecH…). The build log for the last ends `Failed to type check` on `TwoFactorSettings.tsx(110,29) TS2339 backupCodes` and two pdfjs v6 errors in `local-intake.ts`. `main` took three merges whose build had not passed, and production served a stale bundle throughout while the branch read as merged. A pipeline that works but accepts un-built merges is a repeatable failure; a wrong project setting is a one-time fix. The repo-root scaffold stays in the report as a trap rather than a live failure: if the Root Directory setting is ever lost, detection reads `next.config.ts`, finds no `next` dependency, and the deploy fails with nothing in the repository to explain why. Also corrected: `/api/reports/sample` is stale to the last production build (`b88b7ee`), not "months stale"; the 1.4 zero-code fix is now branch protection requiring the CI type check, which is what would have stopped all three failed builds. Verification matrix gains five Confirmed rows from the Vercel API, the executed proxy bench (p99 1.050 ms over 2000 cold scans), and the guard self-test. Methodology banner records the retraction. `tasks/lessons.md` takes the generalisable rule: a claim about what is deployed is checked against the deploy provider, never derived from config files. The repo states intent, the control plane states fact. Gates re-run: detection guards 47/47, verify-no-leaks PASS, HTML well-formed (no unclosed elements, no mismatched tags). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The Security Audit workflow's `npm audit (prod deps) (compliance-firewall-agent)` leg fails on `browserslist <=4.28.6`: GHSA-c83g-rgw3-j3cx unbounded memory growth (no cache eviction) -> OOM GHSA-73wf-gq98-2v4g uncaught crash / prototype write via untrusted browserslist-stats.json custom stats Not caused by this branch. `git diff --stat HEAD~2 -- package.json package-lock.json` is empty: the two commits here touch three source files and documentation, no dependency. The same check failed on unrelated Dependabot PRs on 2026-09-01 and is red on the base branch, so every PR opened since inherits it. #325 hit the identical failure and reached the identical remediation. Ported rather than waited on, because waiting on another PR to merge is still waiting: `npm audit fix --package-lock-only --omit=dev`. Lockfile only, 4.28.6 -> 4.28.8, a patch bump inside the range the tree already resolves. It no-ops once the base branch carries the same change. Reproduced and verified: before npm audit --omit=dev -> 1 high severity vulnerability after npm audit --omit=dev -> found 0 vulnerabilities Re-validated against the new lockfile rather than the old node_modules: `npm ci` (861 packages, browserslist resolves to 4.28.8), `npx tsc --noEmit` clean, `npm run lint` 0 errors, `npm run test:coverage` 223 files and 3099/3099 passed with the coverage gate met, `npm run build` succeeded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5
|
CI record: Not this PR's failure. Ported rather than waited on. #325 hit the identical failure and reached the identical remediation, and waiting for another PR to merge is still waiting. Applied here as Reproduced before, verified after: Re-validated against the new lockfile rather than the stale No re-run was spent — the fix was pushed instead, and the check went green on its own next run. Generated by Claude Code |
`main` moved while this branch was in flight (#326: the pre-launch teardown, the /demo double-counting fix, and the browserslist lockfile bump). Both conflicts are in append-style logs where each side is a pure addition, so both sides are kept rather than either being dropped. The two sessions independently checked the same claim against the Vercel API and reached the same answer from opposite directions, which is worth recording rather than deduplicating: b430ff6 retracted "nothing has deployed since #288" inside docs/audit/PRE-LAUNCH-TEARDOWN-2026-09-02.html, and found something the repository cannot show — three production deployments in state ERROR inside one 25-minute window, so main took three merges whose build had not passed and production served a stale bundle throughout. This branch corrected the same false claim where it does the most damage: CLAUDE.md, where it is one of the two mitigations the shut-down-or-pivot decision rests on. b430ff6 did not touch CLAUDE.md, so the correction is complementary, not duplicated. Also corrected in tasks/todo.md: this branch's own entry claimed "3149 -> 3184 app tests". No pre-change baseline was ever measured, so the delta was unverifiable. Replaced with the figures that were actually run. Gates re-run on the merged tree, not assumed from before the merge: tsc --noEmit clean · 3157 app tests pass (226 files, exit 0) · npm run build exit 0 with /api/cron/reconcile-orders registered · eslint 0 errors · verify-structure PASS · 92 proxy tests pass · npm audit --omit=dev --audit-level=high (the exact CI command) exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GRc3QBQMgVNecjE7TLY1sG
Summary
An adversarial pre-launch teardown found nine defects. Seven are fixed here. Every one still open is a founder dashboard action with no code involved.
The headline: the product sells "a SHA-256 tamper-evident audit trail, generated from a 14-day run in your own environment", and Mode B could not produce one.
proxy/storage.tscreatedproxy_eventswith no hash column, no previous-hash column and no signature. The onlycreateHash("sha256")in the entire proxy tree hashed the licence key. The chain lived exclusively inlib/audit/seed-anchor.tsand Supabase migrations 029/030 — inside the Vercel plane CLAUDE.md itself declares not CUI-safe.So the tamper-evidence sat in the one place a CUI customer is told not to send CUI, and the deployment the claim actually covers produced a flat table anyone with the volume mounted could edit with
sqlite3. An SSP artifact generated from that table is inadmissible — which is what blocks the RPO/MSP co-sell in Phase 5 of the calendar.Closes #
What was fixed
/demoreported every CUI and PHI finding at 2× and published "90" patterns on a page whose header reads 53SHA-256(prev_hash ‖ canonical(event)),GET /v1/audit/verify, 14 tests that tamper for reallicense.validcomputed and discarded; any network failure mintedplan:"pro";/api/license/validatenever existed0.0.0.0:8080— a plaintext CUI endpoint on the customer's LAN127.0.0.1:8080:8080, reverse-proxy path documented/v1/statsand/v1/baselinesunauthenticated while every sibling route was guarded/api/health/ready; public probe untouchedSECURITY.mddisclosure link was a 404The licence bypass could not simply be deleted.
license.tsreturned{valid:true, plan:"pro"}on any network failure — one line in/etc/hostswas an unlimited Pro licence — but that branch was air-gapped Mode C's only licensing path, and its own test file said so while naming the successor it was waiting for. Deleting it would have traded a monetization leak for a broken deployment mode. So the capability is replaced:HOUNDSHIELD_OFFLINE_LICENSEcarries an Ed25519-signed entitlement verified locally with no network, bound to the licence key's hash so it is inert to anyone who does not also hold that key, with a mandatory expiry.scripts/issue-offline-license.mjsissues one; round-tripped against the compiled proxy. Offline operation is now granted rather than achieved by unplugging a cable.The health endpoint was a documentation bug, not a code bug. CLAUDE.md's pre-flight says
/api/healthreports degraded state; the route returns{status:"ok"}unconditionally andhealth-liveness-contract.test.tslocks it that way deliberately. Both positions are right. The public probe is untouched; the capability lives in a new token-gated route that 404s an anonymous caller, a wrong token and an unset token identically, so it is never an oracle. This also re-homeslib/health/service-status.ts— a complete, tested readiness module written for exactly this and orphaned when the route was narrowed, whose only remaining consumer was its own test file.Change type
Impact and operating considerations
Operational impact, threat-model note, and rollback plan:
Detection behaviour is unchanged. Every pattern that fired before still fires; only the duplicate evaluation is gone. Counts fall from 2 to 1 — asserted in both directions.
One behaviour change operators must know about. A proxy with a licence key set that cannot verify it now returns 402 instead of serving. Previously it served with a self-minted Pro licence. Air-gapped installs need
HOUNDSHIELD_OFFLINE_LICENSE+HOUNDSHIELD_LICENSE_PUBLIC_KEY; the 402 body names both. Deliberately unchanged: with no licence key configured the proxy still runs — that is the free demo and the evaluation path, and hard-gating the MIT proxy is a pricing decision, not a security fix. It is now named (source: "evaluation") rather than indistinguishable from paid Pro.Existing databases upgrade in place. Chain columns are added by
ALTER TABLEand left nullable. Pre-chain rows are reported UNVERIFIABLE, never as tampered — retro-hashing records whose integrity was never protected would be manufacturing evidence, which is the precise failure this change exists to prevent.New environment variables, all optional, all fail-closed when unset:
HOUNDSHIELD_ADMIN_TOKEN(falls back to the licence key as before, now warned about and reported by/health),HOUNDSHIELD_OFFLINE_LICENSE,HOUNDSHIELD_LICENSE_PUBLIC_KEY,HEALTH_DIAGNOSTIC_TOKEN(unset ⇒/api/health/ready404s).Rollback: every commit reverts independently. No migration, nothing to sequence.
Validation
npx tsc --noEmitnpm run lintnpm run test:coveragenpm run buildnpm run lintnpm run test:coveragenpm run benchResults and manual verification:
Every guard was proved to discriminate, the way
verify-no-leaks --self-testdoes:expected 2 to be 1. That is the bug reproduced as a number: one CAGE code in the demo script's own prompt, counted twice.UPDATE(turn a BLOCKED CUI event into ALLOWED),DELETE, a forged insert with an invented digest, and a backdated timestamp, asserting each produces the specific verdict —BROKEN_ROWfor an in-place edit,BROKEN_LINKfor a deletion.Offline licence round-tripped end to end against the compiled proxy, not just unit tests:
issue-offline-license.mjs --new-keypair→ issue →validateLicense()returns{valid:true, org_id:"org_rt", plan:"enterprise", source:"offline-token"}, and the same token against a different licence key returnsvalid:false.Two things could not be verified here, and neither is claimed:
403toCONNECT). No claim in the teardown asserts a live response code.Review checklist
Two corrections to the teardown itself
Both retracted in the document, visibly, rather than quietly softened — a finding that changes shape without saying so is one no reader can audit.
"Nothing has deployed since ⛔ BLOCKED ON A DASHBOARD SETTING — delete the repo-root vercel.json so middleware executes #288." The Vercel API disproved it:
dpl_5aN5WbocU36ow…istarget: production,state: READY, commitb88b7ee. What the API surfaced instead is worse and was invisible from the repo — three production deployments failed their type check inside one 25-minute window (bfcbe54,9c9f2b9,ba8bf29), the last ending onTS2339 backupCodesand two pdfjs v6 errors.maintook three merges whose build had not passed."Two contradictory pricing grids ship." They do not.
PRICING_PLANSis dormant Stage-2 data with no non-test importer. The risk is latent, not live — and the dormancy was hand-verified in a comment, which is exactly how the 90-vs-53 double count survived its own deletion. It is an assertion now.What is left — all founder, all dashboard, none of it code
In revenue order, filed in
tasks/todo.md:STRIPE_WEBHOOK_SECRET— still unset, so a completed $499 purchase records no order, sends no receipt and raises no alert. One variable, no dependencies, highest value on the system.buy.stripe.comis egress-blocked from every automated environment, so no test can ever check it, and a dead link voids everything else.035/036/037, then034beforeMARKETING_POSTAL_ADDRESS.HEALTH_DIAGNOSTIC_TOKENin Vercel — until set,/api/health/ready404s by design.ENCRYPTION_KEY=openssl rand -hex 32(revives quarantine) andTURNSTILE_SECRET_KEY(makes CAPTCHA escalation real).Two founder decisions are filed rather than taken: reconciling
plans.tsagainst CLAUDE.md's Stage 2 grid, and whether the MIT proxy should be hard-gated. Deciding either in code would set pricing by side effect — the same reasoning that left the 20%-vs-40% partner ruling alone.proxy/.env.exampleis not updated: it is permission-blocked in this environment. Every new variable is documented indocker-compose.ymlandREADME.md.🤖 Generated with Claude Code
https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5