Hash-chain the Mode B audit log, make the licence enforceable, restore the readiness gate - #327
Conversation
…orceable
Four findings from docs/audit/PRE-LAUNCH-TEARDOWN-2026-09-02.html, all in the
one artifact a CUI-handling contractor actually runs.
## 1. The tamper-evident audit trail did not exist in Mode B
The demo sells "a SHA-256 tamper-evident audit trail, generated from a 14-day
run in your own environment". `proxy/storage.ts` created `proxy_events` with no
hash column, no previous-hash column and no signature, and the only
`createHash("sha256")` in the whole proxy tree hashed the licence key. The chain
lived exclusively in `lib/audit/seed-anchor.ts` and 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 Mode B 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.
hash(n) = SHA-256( prev_hash(n) || canonical(event n) )
`created_at` is now computed in JS rather than left to SQLite's `datetime('now')`
default — a digest cannot cover a value the database invents after the digest is
taken. Tip-read and write run in one IMMEDIATE transaction so a second process
with the same volume mounted cannot fork the chain.
`GET /v1/audit/verify` recomputes the whole chain and returns 409 naming the
first bad record, distinguishing BROKEN_ROW (edited in place) from BROKEN_LINK
(deleted or reordered). `tip_hash` is the value to anchor externally.
Rows written before the chain existed are reported as UNVERIFIABLE, never as
tampered. They cannot be retro-hashed: inventing digests for records whose
integrity was never protected is manufacturing evidence, which is the precise
failure this change exists to prevent.
## 2. The licence was unenforceable by construction
`server.ts` called `validateLicense()`, read `license.org_id`, and never looked
at `license.valid` — the field was computed on every request and discarded.
`license.ts` returned `{valid:true, org_id:"offline", plan:"pro"}` on ANY network
failure with no cache, so one line in /etc/hosts minted an unlimited Pro licence
that never expired. And `/api/license/validate` did not exist, so every deployed
container had been 404ing into that branch since it shipped.
The branch could not simply be deleted: air-gapped is a documented deployment
mode with no other licensing path. So the capability is replaced, not removed —
`HOUNDSHIELD_OFFLINE_LICENSE` carries an Ed25519-signed entitlement verified
locally against a public key the install already holds, bound to the licence
key's hash so it is inert to anyone who does not also hold that key, and
carrying a mandatory expiry. `scripts/issue-offline-license.mjs` generates the
keypair and signs tokens; round-tripped against the compiled proxy.
Offline operation is now GRANTED rather than achieved by unplugging a cable.
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. What changed is that the state is now
NAMED (`source: "evaluation"`) instead of being indistinguishable from paid Pro.
`app/api/license/validate/route.ts` resolves key_hash → api_keys → org_members →
organizations.subscription_tier. It accepts a SHA-256 digest and REJECTS a raw
key rather than helpfully hashing it, fails closed on every error path, and
returns entitlement only — no user id, no traffic data.
## 3. Two unauthenticated reads, and one secret doing two jobs
`GET /v1/stats` and `GET /v1/baselines/:entityId` carried no guard while every
other management route did; they leak AI usage profile and per-entity behavioural
baselines. Both now require the admin token, and the comparison is constant-time
(`timingSafeEqual`) rather than `!==`, which short-circuits on the first
differing byte and leaks the token prefix to anyone who can time a 401.
`HOUNDSHIELD_ADMIN_TOKEN` still falls back to the licence key — removing the
fallback would break every existing install on upgrade — but it is no longer
silent: startup warns, and `/health` reports `admin_credentials_separated`.
`docker-compose.yml` bound `"8080:8080"`, i.e. 0.0.0.0, publishing a PLAIN HTTP
listener carrying CUI-bearing prompts to the whole LAN. Now `127.0.0.1:8080:8080`,
with the reverse-proxy path documented instead.
## 4. The image shipped a compiler, a test harness and the dev tree
Single-stage, so python3/make/g++ never left. `npm ci --omit=dev` was undone by a
later `npm install -D typescript tsx` (npm reconciles the whole tree), and the
following `npm uninstall` removed two packages of many — vitest and
@vitest/coverage-v8 shipped to customers. `tsconfig.json` includes `**/*.ts`, so
`dist/` carried vitest.config.js and a compiled ooda/__tests__.
Now two stages. Measured on this tree: the dependency tree the runtime stage
copies is **47 MB against 139 MB** for the full one — a 66% reduction before
counting the toolchain, which no longer exists in the final image at all. The
image itself could not be built here (no Docker daemon), so no image-size figure
is claimed; CI builds it on every proxy PR.
The builder also drops the `typescript@^5.8.3` pin and uses the version
package.json declares (^7.0.2), so the image and CI stop type-checking on
different majors.
## Validation
proxy lint tsc --noEmit, exit 0
proxy test:coverage 6 files, 120/120 passed (was 92) —
stmts 74.33% branches 62.62% lines 75.63% (was 70.54/59.14/71.81)
proxy bench p99 0.705 ms over 2000 cold scans (budget 10 ms) — PASS
verify-no-leaks PASS
28 new tests. The chain suite proves tamper-evidence by actually tampering —
UPDATE, DELETE, a forged insert and a backdated timestamp, each asserted to
produce the specific verdict — because a suite that only verified data it just
wrote would pass against a function returning {ok:true}. The offline-licence
suite is mostly rejections: wrong signing key, payload edited after signing,
token lifted from another customer, expired, and no public key configured.
`proxy/.env.example` is not updated: it is permission-blocked in this
environment. Every new variable is documented in docker-compose.yml and README.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5
…the build trace
Four more findings from docs/audit/PRE-LAUNCH-TEARDOWN-2026-09-02.html, plus one
correction to the teardown itself.
## 1. The daily pre-flight returned green under every failure it detects
CLAUDE.md's Session Start Protocol step 3 is `curl .../api/health`, and CLAUDE.md
states that endpoint "reports missing control stores and reset-code
configuration as degraded rather than green". It does not: the route returns
`{status:"ok"}` unconditionally.
The narrowing was deliberate and test-locked — `health-liveness-contract.test.ts`
asserts the public probe stays minimal, and a public unauthenticated endpoint
should not publish per-control state. Both positions are right, so the
contradiction was in the DOCUMENTATION, and the cost was real: the one condition
that has actually lost money (`STRIPE_WEBHOOK_SECRET` unset, so a completed $499
purchase records no order and raises no alert) was undetectable from a terminal.
`/api/admin/health` cannot serve it either — it needs a browser session.
So the public probe is untouched and the capability lives in a new token-gated
`/api/health/ready`. It 404s an anonymous caller, a wrong token AND an
unconfigured token, so it is never an oracle confirming the route or the header
name; the comparison is constant-time.
This also re-homes `lib/health/service-status.ts`, a complete tested readiness
module whose header reads "VALUE-FREE, ALWAYS. /api/health is public and
unauthenticated" — written for exactly this and orphaned when the route was
narrowed. Its only remaining consumer was its own test file.
CLAUDE.md now names the real endpoint in both places it was wrong.
## 2. The sample PDF was dated to the last deploy
`app/api/reports/sample/route.ts` declared `dynamic = "force-static"`, so the
handler ran once per build and `buildSampleReportData()`'s `now` froze to the
build clock. A prospect downloading the sample got a "14-day window" ending
whenever we last merged to main. `revalidate = 86400` did nothing: a statically
evaluated handler with no dynamic input produces identical bytes every time.
On an evidence product sold to buyers who verify things, an artifact whose own
cover date is months stale is the first thing an assessor notices.
Now origin-rendered with `s-maxage=86400, stale-while-revalidate` — dates track
real time to within a day, at one render per day per edge region. `max-age=0` on
purpose: a privately cached copy would show one prospect the same dated artifact
across visits, which is the same defect one hop closer to them. `runtime` is
pinned because the generator returns a Node Buffer and an edge promotion would
fail at runtime rather than at build. Build output confirms the route moved from
static to dynamic.
## 3. The build trace was anchored at the repo root with no exclusions
`outputFileTracingRoot` points at the repository root — correct, since the app
resolves `next` from a tree above itself — but with no `outputFileTracingExcludes`
it aims the tracer at 26 MB the application never imports: docs/ 6.2 MB, skills/
4.6 MB across 251 directories, integrations/ 772 KB, rules/ 624 KB, agents/
532 KB, examples/ 252 KB (measured with `du -sh` at b88b7ee).
Tracing only copies what an import graph reaches, so this bounds the work rather
than proving waste — but a single stray `require` of a fixture under `examples/`
would pull it into a serverless bundle with nothing to catch it.
`supabase/migrations/` is excluded for a different reason: 37 SQL files belong in
the repo and in `db push`, and nothing under app/ or lib/ imports a .sql file.
## 4. The security disclosure link was a 404
`SECURITY.md` linked to `../../security/advisories/new`, which from
`/<owner>/<repo>/blob/main/SECURITY.md` resolves to `/<owner>/<repo>/blob/
security/advisories/new`. A researcher following the disclosure path in our own
security policy landed on a GitHub error page. `SUPPORT.md` had the same defect
twice. All three are absolute now; a link scan of every root markdown file
reports 47 relative links and 0 broken.
## 5. Correction: the pricing grids do not both ship
The teardown's Section 3 said "two contradictory pricing grids ship". They do
not. `lib/pricing/__tests__/plans.test.ts` records that `PRICING_PLANS` is
dormant Stage-2 data with no non-test importer, and the 2026-08-15 live audit
confirms /pricing shows exactly one grid. Corrected in the document rather than
quietly softened.
What is true is latent: `plans.ts` holds Pro $199 / Growth $499/mo against
CLAUDE.md's Starter $299 / Pro $799, the $199 tier is the one the NEVER-DO list
forbids, and a file named `plans.ts` is what a future contributor treats as the
source of truth. The dormancy was hand-verified and recorded in a COMMENT —
which is precisely how the 90-vs-53 double count survived its own deletion. It
is now an assertion: every non-test file under app/, components/ and lib/ is
enumerated and the build fails if any imports the retired symbols. Self-tested
by adding an import and watching it fail by filename.
Reconciling the numbers stays a founder decision. Picking one in code would set
pricing by side effect, the same reasoning that left the 20%-vs-40% partner
ruling alone.
## Validation
tsc --noEmit exit 0
eslint 0 errors, 35 warnings (pre-existing, none in changed files)
test:coverage 224 files, 3116/3116 passed (was 223/3099), gate passed
build succeeded; /api/reports/sample now ƒ (Dynamic), was ○ (Static)
verify-no-leaks PASS
verify-structure PASS
root markdown links 47 relative, 0 broken
17 new tests. The readiness suite is mostly rejections — anonymous, wrong token,
unset token, blank token, a prefix of the real token, and a superstring of it —
plus assertions that the failure body names neither the header nor the variable
nor any service key, and that the public probe still answers exactly
`{status:"ok"}`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5
… produced `tasks/todo.md` — the audit entry now separates what shipped from what is left. Seven of nine findings are fixed in this branch; every one still open is a founder dashboard action with no code involved, ordered by revenue impact, with `STRIPE_WEBHOOK_SECRET` still first because a completed $499 purchase currently records no order and raises no alert. Two founder decisions are filed rather than taken: reconciling `plans.ts` against CLAUDE.md's Stage 2 grid, and whether the MIT proxy should be hard-gated at all. Both would set pricing by side effect if decided in code. `tasks/lessons.md` — three rules, each earned in this session: A capability cannot be deleted for security; it has to be replaced. Removing the licence bypass would have broken Mode C, whose only licensing path it was. A fix that closes a hole by removing a supported deployment mode is a regression wearing a security label. Hand-verified invariants recorded in prose are not guards. Three instances in one session — the pattern registry, the dormant pricing grid, the orphaned health module — all real verifications, all recorded where nothing could check them again. All three are assertions now, each self-tested by introducing the violation and watching the guard name the file. Two right answers can produce one wrong document. CLAUDE.md and the health route disagreed and both were defensible; the casualty was the operating procedure built on the documented one. Establish which side a deliberate decision stands behind — a guarding test is that evidence — then satisfy the need without dismantling the guard. Guards re-run after the edits: 82/82 across lib/detection, lib/pricing and app/api/health. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
A maintainability pass over the whole app. Everything removed here had zero
references — not "looked unused", but survived three analysis passes and then
`tsc`, 3092 tests and `next build`.
## What went, and why it was dead
31K components/dashboard/openclaw-templates.ts
27K components/landing/PlatformDashboard.tsx + PlatformDashboardClient
11K lib/gateway/providers/{index,anthropic,google,openai,openrouter}.ts
10K app/blog/posts/data.ts
10K lib/agent/memory-dna.ts
22K lib/integrations/siem/{index,elastic,sentinel,splunk,types}.ts
24K components/ui/{AnchorBadge,ComparisonFlow,ThreatFeed,PricingToggle,CodeBlock,CountdownTimer}.tsx
10K components/{LocalizedPrice,FloatingNav,SectionSpotlight,ScrollProgress}.tsx
5K components/landing/WhyHoundshield.tsx
5K components/dashboard/operator/OperationalReadiness.tsx
4K lib/brain-ai/firecrawl-updater.ts
0K lib/secret.ts
Three cases are worth naming individually.
**The PlatformDashboard chain.** `PlatformDashboard.tsx` is reachable — but only
from `PlatformDashboardClient.tsx`, which nothing imports. A live edge into a
dead root. Both go. (CLAUDE.md's rule that `PlatformDashboard` must stay
`ssr:false` protected a component no page renders; the rule outlived it.)
**The six `components/ui/` components.** Each had exactly one consumer in the
repository: `v3-components.test.tsx`, testing it. A component whose only caller
is its own test is not covered, it is embalmed — the test proves it compiles and
nothing proves anyone wants it. Their five suites went with them; the file keeps
the suites for components a page actually renders.
**`lib/integrations/siem/`.** An entire unwired subsystem — Splunk, Sentinel,
Elastic. `app/roadmap/page.tsx` lists SIEM forwarding as ROADMAP, so deleting it
falsifies no shipped claim; the roadmap entry stays as true as it was. Checked
before removing, because the NEVER-DO list turns on exactly that distinction.
`lib/secret.ts` was also a stdlib reinvention: `getSecret(name, fallback)` is
`process.env[name] ?? fallback`.
## Dependencies
Nine runtime dependencies had no import anywhere in app/, components/, lib/,
hooks/, scripts/, sdk/, test/ or any root config:
@react-three/fiber @remotion/player @remotion/three remotion three
date-fns next-themes react-hook-form react-markdown
A whole video and 3D rendering stack in the dependency tree of a compliance
proxy. `npm ci` goes from 861 packages to 766, and the production build from
~40s to ~16s on this machine.
`react-dom` also reports no direct import and is KEPT: Next.js requires it at
runtime whether or not application code names it. An automated unused-dependency
report is wrong about exactly this class, which is why the list was checked by
hand rather than piped into `npm uninstall`.
`npm audit --omit=dev` surfaced `fast-uri` (high, 4 advisories) and `fflate`
(moderate) once the tree was re-resolved. Fixed lockfile-only in the same change,
same approach as the browserslist bump: `found 0 vulnerabilities`.
## The one thing that was NOT dead
`lib/gateway/ws-handler.ts` was staged for deletion and restored. `server.ts` at
the app root imports it, and the analysis scanned app/, components/, lib/ and
hooks/ — not the repository root. `tsc` caught it in the first second of the
first verification run. It is a documented opt-in WebSocket entrypoint with its
own `dev:ws` and `start:ws` npm scripts, and it stays.
That miss is why `scripts/find-orphans.mjs` ships with this change rather than
staying a scratch file. Its header records all four failure modes found the hard
way — barrels, bare side-effect imports, dynamic imports, and reachability from
outside the scanned roots — and states the rule the fourth one produced: the
script proposes, the compiler disposes. Never delete from its output alone.
## Validation
tsc --noEmit exit 0
eslint 0 errors (5 pre-existing warnings)
test suite 224 files, 3092/3092 passed
(3117 before; the 25 removed covered deleted components)
build compiled successfully, 235/235 static pages generated
npm audit --omit=dev, found 0 vulnerabilities
verify-no-leaks PASS
npm ci 766 packages, was 861
Re-running `node scripts/find-orphans.mjs` now reports one orphan — ws-handler,
the known false positive — and eight modules imported only by tests, left alone
deliberately: several are pre-wired for shipping work and deleting them would
remove real coverage. They are the next pass, not this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5
|
CI record: The failure fired against the head before the cleanup commit, which already carried the fix. No action is outstanding. Two new advisories, not the browserslist one. Removing nine unused runtime dependencies re-resolved the tree and surfaced Fixed the same way as the browserslist bump: Re-validated against the new lockfile rather than the stale Worth noting from this run:
Generated by Claude Code |
…conflicts main advanced 3bc6df7 -> 23856c7 while this branch sat ready for review, bringing the NexusSwift addition, the Mode B audit-log hash chain, a readiness gate, and a refactor deleting 29 unreferenced files and 9 runtime dependencies. Three conflicts, resolved on their merits rather than by picking a side: CLAUDE.md — Session Start Protocol, step 3. Both sides rewrote the same line. main replaced the health check with the token-gated `/api/health/ready`, which is strictly better: `/api/health` is a bare liveness probe that returns `{"status":"ok"}` under every failure condition. Taking main's version alone would have dropped a real distinction this branch adds, because `lib/health/service-status.ts` reports `payments` / `payments_webhook` from the same diagnostics the new weekly alert uses — so readiness answers "is the money path CONFIGURED", and only the reconciler answers "was a sale actually missed". Kept main's route, appended that distinction. tasks/todo.md and tasks/lessons.md — append-style logs where each side is a pure addition. Both sides kept; main's dated section opens the lessons block and this branch's entries follow inside it. Verified the deletion refactor does not undercut this branch: none of the nine removed dependencies (@react-three/fiber, @remotion/*, date-fns, next-themes, react-hook-form, react-markdown, remotion, three) is imported by lib/stripe/report-fulfillment.ts, lib/stripe/money-path.ts, app/api/cron/reconcile-orders/route.ts or lib/email/templates/money-path-alert.ts. Gates re-run on the merged tree against the new lockfile, not carried over: npm ci exit 0 · tsc --noEmit clean · 3150 app tests pass (227 files, exit 0) · npm run build exit 0, registering both /api/cron/reconcile-orders and /api/health/ready · eslint 0 errors, none in the new files · npm audit --omit=dev --audit-level=high (the exact CI command) exit 0 · verify-structure PASS · 120 proxy tests pass · proxy bench p99 0.883 ms against a 10 ms budget. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GRc3QBQMgVNecjE7TLY1sG
…dit gate The Security Audit job (`npm audit --omit=dev --audit-level=high`) went red on this branch. One advisory gates it: fast-uri 3.0.0 - 3.1.5 (high, 4 advisories) node_modules/fast-uri, via ajv under ajv-formats and schema-utils It is a consequence of this PR, not a pre-existing failure: removing the nine unused dependencies re-resolved the tree, and the new resolution landed on fast-uri 3.1.5 where the old one had not. PR #327 hit the identical advisory from the identical cause. `npm audit fix` could not resolve it — the version is pinned through a transitive parent — so this adds a lockfile-level override instead: "overrides": { "fast-uri": "^3.1.7" } 3.1.7 is outside the vulnerable range, and `^3.1.7` sits inside ajv's own declared `^3.0.1`, so nothing is forced past a dependency's stated compatibility. The change is 3 lines of lockfile (3.1.5 -> 3.1.7) plus the overrides block; no direct dependency version moved, and `npm install` reported "changed 1 package". NOT fixed here, deliberately: `fflate` (moderate) resolves to 0.4.8 through `posthog-js@^0.4.8`. It does not fail this gate, which is `--audit-level=high`. Overriding it to 0.8.3 would push posthog-js outside its own declared range, on a library that runs on the live marketing site, to clear an advisory that blocks nothing. That trade is not worth taking without a reason to. Validated: tsc 0 · 3,114 tests / 222 files · build EXIT 0. fast-uri feeds ajv -> schema-utils -> webpack, so the build is the check that matters here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018GK6NpeRNRpdzRrEP9vuyM
Summary
Follow-up to #326, which merged at head
22a53f0while these three commits were still in flight. A merged PR cannot track new work, so this is a new PR: the branch was rebased ontomainat3bc6df7and carries only the commits #326 did not include.These close six of the seven remaining pre-launch teardown findings. Everything still open after this 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.Closes #
Change type
Impact and operating considerations
Operational impact, threat-model note, and rollback plan:
Six findings closed. The Mode B hash chain with
GET /v1/audit/verify; licence enforcement (server.tscomputedlicense.validand discarded it,license.tsmintedplan:"pro"on any network failure, and/api/license/validatenever existed — so one line in/etc/hostswas an unlimited Pro licence); the compose file binding0.0.0.0and publishing a plaintext CUI endpoint to the LAN; two unauthenticated proxy reads while every sibling route was guarded; a single-stage image shipping a C toolchain, the dev tree and compiled tests; and the daily pre-flight that returned green under every failure it exists to detect.The licence bypass could not simply be deleted. 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.One behaviour change operators must know about. A proxy with a licence key set that cannot verify it now returns 402 instead of serving. Air-gapped installs need
HOUNDSHIELD_OFFLINE_LICENSE+HOUNDSHIELD_LICENSE_PUBLIC_KEY; the 402 body names both. Deliberately unchanged: with no 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 report UNVERIFIABLE, never tampered — retro-hashing records whose integrity was never protected would be manufacturing evidence, which is the precise failure this change exists to prevent.The public health probe is untouched.
health-liveness-contract.test.tslocks it deliberately, and it still passes. The readiness capability lives in a new token-gated/api/health/readythat 404s an anonymous caller, a wrong token and an unset token identically, so it is never an oracle.New environment variables, all optional, all fail-closed when unset:
HOUNDSHIELD_ADMIN_TOKEN(still falls back to the licence key so no install breaks on upgrade, now warned about and reported by/health),HOUNDSHIELD_OFFLINE_LICENSE,HOUNDSHIELD_LICENSE_PUBLIC_KEY,HEALTH_DIAGNOSTIC_TOKEN.Rollback: all three commits revert independently. No migration, nothing to sequence.
Validation
npx tsc --noEmitnpm run lintnpm run test:coveragenpm run buildnpm run lintnpm run test:coveragenpm run benchResults, re-run after the rebase onto
3bc6df7:Every guard was proved to discriminate, the way
verify-no-leaks --self-testdoes:UPDATE(turning 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.{status:"ok"}.Offline licence round-tripped end to end against the compiled proxy, not just unit tests:
--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).Review checklist
A correction carried in from #326
The teardown's Section 3 said "two contradictory pricing grids ship". They do not.
PRICING_PLANSis dormant Stage-2 data with no non-test importer, and the 2026-08-15 live audit confirms/pricingshows exactly one grid. Retracted in the document rather than quietly softened.The risk is latent, not live:
plans.tsholds Pro $199 / Growth $499/mo against CLAUDE.md's Starter $299 / Pro $799, the $199 tier is the one the NEVER-DO list forbids, and a file namedplans.tsis what a future contributor treats as the source of truth. The dormancy was hand-verified and recorded in a comment — precisely 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
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.035/036/037, then034beforeMARKETING_POSTAL_ADDRESS.HEALTH_DIAGNOSTIC_TOKENin Vercel — until set,/api/health/ready404s by design.bfcbe54,9c9f2b9,ba8bf29); this is the one setting that would have stopped all three.ENCRYPTION_KEY=openssl rand -hex 32andTURNSTILE_SECRET_KEY.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.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
Generated by Claude Code