Skip to content

Hash-chain the Mode B audit log, make the licence enforceable, restore the readiness gate - #327

Merged
thecelestialmismatch merged 4 commits into
mainfrom
claude/houndshield-presale-audit-eepk9u
Sep 3, 2026
Merged

Hash-chain the Mode B audit log, make the licence enforceable, restore the readiness gate#327
thecelestialmismatch merged 4 commits into
mainfrom
claude/houndshield-presale-audit-eepk9u

Conversation

@thecelestialmismatch

Copy link
Copy Markdown
Owner

Summary

Follow-up to #326, which merged at head 22a53f0 while these three commits were still in flight. A merged PR cannot track new work, so this is a new PR: the branch was rebased onto main at 3bc6df7 and 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.ts created proxy_events with no hash column, no previous-hash column and no signature. The only createHash("sha256") in the entire 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 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

  • Bug fix
  • New capability
  • Security hardening
  • Documentation or developer-experience improvement
  • Refactor or maintenance
  • Breaking change

Impact and operating considerations

  • Authentication, sessions, identity, or authorization
  • Sensitive-data handling, logging, telemetry, or outbound requests
  • Proxy detection, policy evaluation, block/quarantine behavior, or performance
  • Database schema, migrations, retention, or access controls
  • Deployment, environment variables, integrations, or scheduled work
  • Evidence, reporting, or audit-chain behavior

Operational impact, threat-model note, and rollback plan:

Six findings closed. The Mode B hash chain with GET /v1/audit/verify; licence enforcement (server.ts computed license.valid and discarded it, license.ts minted plan:"pro" on any network failure, and /api/license/validate never existed — so one line in /etc/hosts was an unlimited Pro licence); the compose file binding 0.0.0.0 and 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_LICENSE carries 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.mjs issues 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 TABLE and 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.ts locks it deliberately, and it still passes. The readiness capability lives in a new token-gated /api/health/ready that 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

  • Web plane: npx tsc --noEmit
  • Web plane: npm run lint
  • Web plane: npm run test:coverage
  • Web plane: npm run build
  • Proxy: npm run lint
  • Proxy: npm run test:coverage
  • Proxy: npm run bench
  • Manual verification (below)
  • Documentation-only validation (links, commands, and claims checked)

Results, re-run after the rebase onto 3bc6df7:

tsc --noEmit          exit 0
test suite            224 files, 3117/3117 passed
eslint                0 errors, 35 warnings (pre-existing, none in changed files)
build                 succeeded; /api/reports/sample now ƒ Dynamic, was ○ Static
proxy lint            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       --self-test PASS, scan PASS
verify-structure      PASS
root markdown links   47 relative, 0 broken
dependency tree       47 MB prod-only vs 139 MB full  (du -sh, measured)

Every guard was proved to discriminate, the way verify-no-leaks --self-test does:

  • Audit chain — the suite does not verify data it just wrote. It runs 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_ROW for an in-place edit, BROKEN_LINK for a deletion.
  • Offline licence — 14 tests, mostly rejections: wrong signing key, payload edited after signing, token lifted from another customer, expired, non-date expiry, no public key, unreadable public key, malformed, non-JSON payload.
  • Readiness route — 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"}.
  • Pricing dormancy — added an import, watched the guard fail by filename, removed it.

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 returns valid:false.

Two things could not be verified here, and neither is claimed:

  • No Docker daemon in this environment, so no image-size figure is asserted — only the measured 139 MB → 47 MB dependency tree, which is the dominant term. CI builds the image on every proxy PR.
  • Live HTTP probing of houndshield.com is refused by this environment (403 to CONNECT).

Review checklist

  • The change is focused and does not include unrelated refactoring.
  • Tests cover changed behavior, or the omission is explained above.
  • No credentials, customer data, sensitive prompts, or production exports were added.
  • Public claims are scoped, evidence-based, and consistent with the selected deployment boundary.
  • Documentation and configuration guidance were updated where needed.
  • Database migrations, configuration changes, and rollout dependencies are documented where applicable.
  • I performed a self-review and addressed obvious failure paths.

A correction carried in from #326

The teardown's Section 3 said "two contradictory pricing grids ship". They do not. 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. Retracted in the document rather than quietly softened.

The risk is latent, not live: 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 — 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

  1. 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.
  2. Open the Payment Link in a browser and confirm it is live at $499 — buy.stripe.com is egress-blocked from every automated environment, so no test can ever check it.
  3. Apply 035/036/037, then 034 before MARKETING_POSTAL_ADDRESS.
  4. HEALTH_DIAGNOSTIC_TOKEN in Vercel — until set, /api/health/ready 404s by design.
  5. Branch protection requiring the CI type check — three production deploys failed on a type check inside one 25-minute window (bfcbe54, 9c9f2b9, ba8bf29); this is the one setting that would have stopped all three.
  6. ENCRYPTION_KEY = openssl rand -hex 32 and TURNSTILE_SECRET_KEY.

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. Deciding either in code would set pricing by side effect.

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.

🤖 Generated with Claude Code

https://claude.ai/code/session_011UNmNvaCuGcZz418MoTyB5


Generated by Claude Code

…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
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
compliance-firewall-agent Ready Ready Preview Sep 3, 2026 9:35pm UTC

@supabase

supabase Bot commented Sep 3, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project qifynzuyrdxmxlumpsrq because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

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

Copy link
Copy Markdown
Owner Author

CI record: npm audit (prod deps) (compliance-firewall-agent) failed on 59a9234, and is green on 544937a.

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 fast-uri 3.0.0–3.1.5 (high — four advisories: host confusion via skipped IDN canonicalization, SSRF via malformed IPv6 normalization, SSRF via repeated hostname percent-decoding, host confusion via percent-encoded scheme normalization) and fflate (moderate — unzipSync infinite loop on malformed ZIP64, reached through jspdf).

Fixed the same way as the browserslist bump: npm audit fix --package-lock-only --omit=dev, lockfile only.

before   npm audit --omit=dev  ->  2 vulnerabilities (1 moderate, 1 high)
after    npm audit --omit=dev  ->  found 0 vulnerabilities

Re-validated against the new lockfile rather than the stale node_modules: npm ci (766 packages, down from 861), npx tsc --noEmit clean, 224 files / 3092 tests passing, npm run build succeeded with 235/235 static pages.


Worth noting from this run: Build & Publish proxy image passed. That is the multi-stage Dockerfile compiling end to end — the one claim in this PR I explicitly could not verify locally, since this environment has no Docker daemon. The image now builds without the C toolchain and without the devDependency tree.

npm audit (prod deps) (proxy) also went from cancelled to success; it had been killed by its failing sibling job on the old head.


Generated by Claude Code

@thecelestialmismatch
thecelestialmismatch marked this pull request as ready for review September 3, 2026 21:38
@thecelestialmismatch
thecelestialmismatch merged commit 23856c7 into main Sep 3, 2026
12 checks passed
thecelestialmismatch pushed a commit that referenced this pull request Sep 3, 2026
…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
thecelestialmismatch pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants