test(hardening): de-flake firehose sleeps + tier/consistency correctness fixes - #92
Closed
cloakmaster wants to merge 2 commits into
Closed
test(hardening): de-flake firehose sleeps + tier/consistency correctness fixes#92cloakmaster wants to merge 2 commits into
cloakmaster wants to merge 2 commits into
Conversation
…ess fixes Mechanical test hardening plus two small correctness fixes (each TDD'd). Test hardening: - firehose.listener.test.ts: replace the three fixed setTimeout sleeps (the reconnect-sentinel and self-test-ping positive waits, and the alive-sentinel negative wait) with the existing poll-until pattern; the negative no-event assertion now uses fake timers so absence is deterministic. These fixed waits caused CI flakes in #62 and #64. - sdk-claim: add a drift guard (merkle-parity.test.ts) that runs every schemas/merkle-test-vectors.json inclusion vector through the inlined verifyInclusion (merkle-internal.ts), pinning it in lockstep with @foxbook/core (the vectors are core-generated ground truth). Correctness fixes: - core verifyConsistency: the m===0 branch accepted ANY oldRoot vacuously; now enforce bytesEqual(oldRoot, EMPTY_TREE_ROOT) so a forged zero-leaf root can't "prove" consistency. Added a negative vector. - claim/by-handle: tier2_pending (an unverified domain claim awaiting DNS/endpoint proof, app-state-only with no Merkle leaf) reported verification_tier 1; map it to 0. The tier>=1 leaf lookup now correctly skips it. Added handler + response-shape tests. Tooling: - add a .husky/pre-push hook running `pnpm test` (turbo-cached unit suite) so a red branch never leaves the machine.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
foxbook | c0b3a9c | Jun 02 2026, 06:14 AM |
cloakmaster
added a commit
that referenced
this pull request
Jun 2, 2026
…93) (#94) * chore(lint): clear biome warnings + remove dead code, gate CI on warnings Drive the biome warning count to zero without changing behavior: - Export base64urlDecode from @foxbook/core and drop the byte-identical duplicate copy in apps/api/src/claim/handlers.ts (import from core). - Remove 2 dead biome-ignore comments in validators capability.test.ts and the dead eslint-disable in apps/api/src/firehose/listener.ts (the repo has no eslint, and the loop condition reads `stopped` so it was never a constant condition). - Scope lint/style/noNonNullAssertion off for TEST globs only (so test indexing bangs stop warning) — kept on for src, fixing the real src non-null assertions (sha256, jws, claim/repository, merkle-repository, smoke-test-revoke) with bounded `?? 0` / destructure-guard rewrites. - Apply the one safe useTemplate fix in claim.repository.test.ts. - Add --error-on-warnings to the biome step in CI so the count can't silently creep back up. Verification: `pnpm biome check --error-on-warnings` (clean, exit 0), `pnpm typecheck` (24/24), `pnpm --filter @foxbook/api test` (86 pass), `pnpm --filter @foxbook/core test` (59 pass), validators (72 pass), plus core-isolation / no-deskduck / generated pre-commit checks. * docs: fix doc inaccuracies (verifyAgentCard arity, firehose default, env vars, ADR count, 0007 header) - README hero + RFC + did:foxbook samples: add required asset_type to verifyAgentCard options - README: move firehose out of Live; it is disabled by default since #84 (FOXBOOK_FIREHOSE_ENABLED) - env-vars.md + .env.example: document FOXBOOK_FIREHOSE_ENABLED; .env.example adds DATABASE_URL_DIRECT + FOXBOOK_LOG_SIGNING_KEY_HEX, drops stale VERCEL_TOKEN/SENTRY_AUTH_TOKEN + Day-N prose - RATIONALE.md: ADR count eight->nine, add ADR 0009 line, fix misattributed 60s cache-policy sentence (API dynamic-state, not all read endpoints) - STATE-AT-STABLE-MODE-CLOSE.md: fix truncated Discussion #73 link - ADR 0007: add standard metadata block to match other ADRs * docs(spec): reconcile did:foxbook method spec to shipped code The did:foxbook method spec advertised behaviour the reference deployment does not implement. Filing to W3C as-is would describe a method that cannot be resolved. Downgrade the spec to describe what actually ships (no new endpoints — ADR 0008 freeze): - §1.1/§4.4/§6.2/§9: the signing path uses insertion-order canonical JSON (JSON.stringify semantics with caller-fixed key order; ADR 0005, core/src/crypto/canonical.ts, packages/db/src/merkle-repository.ts), NOT RFC 8785 JCS. The JCS canonicalizer in core/src/crypto/jcs.ts is a separate interop-only path, not used to sign leaves/STHs/JWS. Move RFC 8785 from Normative to Informative. Mirror the same fix in docs/specs/W3C-REGISTRY-SUBMISSION.md. - §3.2/§3.2.4/§7.1: there is no GET /agents/<did> resolver and no /.well-known/jwks.json. Rewrite resolution as a client-side projection over the real endpoints: /api/v1/claim/by-handle/..., /leaf/<index>, /inclusion/<index>, /root, and /.well-known/foxbook.json (where log_signing_public_key_hex lives). - §4.2: consistency proofs are GET /consistency?old=N&new=M (query params), not /consistency/<from>/<to> path segments. - §3.3/§3.4: rewrite the rotation and revocation leaf examples to match schemas/tl-leaf.v1.json exactly (signing-key-registration uses prior_ed25519_public_key_hex + new_ed25519_public_key_hex + recovery_key_signature + published_at, dropping previous_leaf_index; revocation uses revoked_key_hex + recovery_key_signature + revocation_timestamp + reason_code enum). - §2.1: relax the MSI ABNF/regex to ^did:foxbook:[0-9A-HJKMNP-TV-Z]{26}$ to match core/src/did.ts (the code does not enforce the [0-7] leading-character restriction). Fix the same regex in the W3C runbook. Docs only; no code change. * ops(uptime): split Neon-touching probes to */30 + document autosuspend as the #1 free-tier survival setting Reduce how often the uptime monitor wakes the free-tier Neon compute while keeping fast detection of a hard outage. uptime.yml: add a second cron so the static landing (no DB) keeps its */15 cadence while the two DB-backed probes (api/healthz, transparency/root) ride a slower */30 cron — roughly halving monitor-induced Neon wakeups. Each matrix entry is tagged with the cron it runs on; the probe step is gated via github.event.schedule so the off-schedule endpoints are skipped (and workflow_dispatch still probes everything). The 'Fail run if endpoint is down' behavior is unchanged: a skipped probe yields an empty outcome, so the fail step simply doesn't fire. OPERATIONS.md: in § 'Neon compute-hour exhaustion', make enabling Neon autosuspend / scale-to-zero the prominent #1 action (the Fly /health and uptime changes only reduce wakeups; they do nothing if the compute can never idle), add a one-line 'how to check current usage' pointer to the compute-hours graph, and restate that Launch ($19/mo) removes the cap entirely. Update the 'Uptime monitoring' runbook to describe the split schedule and the detection-latency tradeoff. * chore(ci): harden supply chain — dependabot, SHA-pinned actions, prod prune - add .github/dependabot.yml: weekly grouped minor/patch updates for npm (root), pip (pyproject), gomod (apps/log-daemon-go), and github-actions. - pin every workflow `uses:` to a 40-char commit SHA with a trailing # vX.Y.Z comment. superfly/flyctl-actions/setup-flyctl was on @master in the token-bearing deploy job — now pinned to ed8efb3 (v1.6). - add `permissions: { contents: read }` to deploy-api.yml (least privilege for the tag-gated Fly deploy). - bump vulnerable deps: hono ^4.12.23 (api + transparency), turbo ^2.9.16. - apps/api/Dockerfile: `pnpm prune --prod` after install to drop devDependencies from the prod image. tsx moved to @foxbook/api dependencies so it survives the prune — the container boots with `node --import tsx ./src/main.ts` and needs it at runtime. Guarded by a new prod-runtime-deps test. - ci.yml: non-blocking SCA step (`pnpm audit --audit-level=high || true`). * test(hardening): de-flake firehose sleeps + tier/consistency correctness fixes Mechanical test hardening plus two small correctness fixes (each TDD'd). Test hardening: - firehose.listener.test.ts: replace the three fixed setTimeout sleeps (the reconnect-sentinel and self-test-ping positive waits, and the alive-sentinel negative wait) with the existing poll-until pattern; the negative no-event assertion now uses fake timers so absence is deterministic. These fixed waits caused CI flakes in #62 and #64. - sdk-claim: add a drift guard (merkle-parity.test.ts) that runs every schemas/merkle-test-vectors.json inclusion vector through the inlined verifyInclusion (merkle-internal.ts), pinning it in lockstep with @foxbook/core (the vectors are core-generated ground truth). Correctness fixes: - core verifyConsistency: the m===0 branch accepted ANY oldRoot vacuously; now enforce bytesEqual(oldRoot, EMPTY_TREE_ROOT) so a forged zero-leaf root can't "prove" consistency. Added a negative vector. - claim/by-handle: tier2_pending (an unverified domain claim awaiting DNS/endpoint proof, app-state-only with no Merkle leaf) reported verification_tier 1; map it to 0. The tier>=1 leaf lookup now correctly skips it. Added handler + response-shape tests. Tooling: - add a .husky/pre-push hook running `pnpm test` (turbo-cached unit suite) so a red branch never leaves the machine. * fix(adapter-endpoint-challenge): SSRF guard on outbound fetch + per-IP claim rate limit The Tier-2 endpoint-challenge adapter fetched a claimant-supplied URL (apps/api/src/claim/body-schema.ts accepts endpoint_url) with no scheme or host validation. Reachable via two unauthenticated POSTs (/claim/start-domain then /claim/verify-endpoint), it was an SSRF primitive: point endpoint_url at cloud metadata (169.254.169.254), loopback, or RFC-1918/ULA internal services and read the JSON we round-trip. SSRF guard (two layers, sharing one isBlockedIp policy): 1. Pre-flight (guard.ts: assertOutboundAllowed) before any socket — https-only, then resolve the hostname and reject if ANY candidate IP is private/loopback/link-local/ULA/metadata (0.0.0.0, 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1, ::, fc00::/7, fe80::/10, incl. IPv4-mapped v6). Bare IP literals are classified directly with no DNS. Fail-closed on resolver error. 2. Connect-time pin (node-transport.ts) — the default transport drives the request through node:https with a guarded dns.lookup that re-applies isBlockedIp to the exact address the socket is about to dial, closing the DNS-rebinding window between pre-flight and connect. Handles the all:true array shape so a private record in a multi-A response can't slip through. Redirects: redirect:"manual" + reject every 3xx (a Location can re-open the hole the pre-flight closed; a real challenge endpoint returns the JWS directly). The adapter's existing interface is unchanged; new optional EndpointVerifyOptions.resolveHostname lets tests drive the pre-flight deterministically, and the existing fetch seam is preserved. Rate limit (apps/api/src/claim/rate-limit.ts): per-IP in-memory token bucket on the mutating claim POST routes (keyed off Fly-Client-IP / X-Forwarded-For), 429 + Retry-After when empty. Blunts the amplification/abuse the outbound fetch enables. Per-instance by design (documented) — a coarse safety valve, not a distributed quota. The read-only GET /claim/by-handle is intentionally excluded. Tests: private/loopback/metadata + non-https targets rejected before any fetch; redirect to a private host blocked; connect-time guard blocks a loopback resolution (rebind defence); normal public https still round-trips (resolver/fetch mocked). Rate-limit: bucket math, per-IP isolation, refill, and the route 429 boundary. * docs(rationale): drop /api/v1/discover from Cache-Control list (sets none) * docs(ops): Neon autosuspend step references the 'main' branch (matches restore runbook) --------- Co-authored-by: Ben <ben@inkog.io>
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Quick-win PR-D: mechanical test hardening plus two small, TDD'd correctness fixes. Each behavior change ships with a failing-first test.
Changes
Test hardening
apps/api/__tests__/firehose.listener.test.ts— replaced the three fixedsetTimeoutsleeps with the poll-until pattern the self-test-wedge test already uses:pollUntil(reconnect && subscribed)pollUntil(selectCalls >= 1)These fixed wall-clock waits were the source of the CI flakes in docs: stable-mode Phase 2 — documentation lockdown #62 and docs(ops): stable-mode Phase 4 — operations runbook + uptime monitoring #64.
packages/sdk-claim/__tests__/merkle-parity.test.ts(new) — drift guard that runs everyschemas/merkle-test-vectors.jsoninclusion vector through sdk-claim's inlinedverifyInclusion(merkle-internal.ts), asserting parity with@foxbook/core. The vectors are core-generated ground truth, so green here means the inlined copy hasn't drifted. Verified the guard actually fails whenmerkle-internal.tsdiverges.Correctness fixes
core/src/merkle/tree.tsverifyConsistency— them===0branch accepted anyoldRootvacuously. Now enforcesbytesEqual(oldRoot, EMPTY_TREE_ROOT)so a forged zero-leaf root can't "prove" consistency. Added a negative vector (wrong old_root, bit-flipped empty root, non-empty proof at m=0 all rejected). Updated the prior "accepts any old_root (vacuous)" test.apps/api/src/claim/handlers.tsclaimByHandle—tier2_pendingreportedverification_tier: 1, over-reporting an unverified domain claim (awaiting DNS/endpoint proof, app-state-only with no Merkle leaf) as Gist-verified. Mappedtier2_pending → 0; the existingtier >= 1leaf lookup now correctly skips it (no leaf to surface). Added handler + response-shape tests.Tooling
.husky/pre-push(new) — runspnpm test(turbo-cached unit suite; DB-integration suites self-skip withoutDATABASE_URL) so a red branch never leaves the machine. Cached runs are near-instant (FULL TURBO).Verification
All green:
pnpm --filter @foxbook/api test— 88 passed / 4 skipped (was 86)pnpm --filter @foxbook/core test— 59 passed / 2 skipped (was 58)pnpm --filter @foxbook/sdk-claim test— 67 passed / 4 skipped (was 63)pnpm typecheck— cleanFirehose suite run 3x back-to-back with no flake; the deliberate drift-injection check confirmed the sdk-claim guard and the consistency negative test both fail when their target regresses.