Repository navigation
refactor(buzz-relay): share NIP-FI assertion evaluation across adapters - #7990
Merged
Merged
Conversation
… map (S4) Adds the NIP-FI admin command endpoint, a bounded per-issuer deny-until-TTL map, and cross-pod disconnect fan-out. The deny check runs after each root and audio socket registers its proven identity, so a concurrent disconnect either finds the socket in its close scan or the check finds the entry. Hooks attach at the S3 admission and pairing points without changing S3 terminal-frame or close-code behavior. Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The root-AUTH witness could pass with the handler's deny check removed; add a pre-registration case the close scan misses and parse OK frames. Restore the clippy allowance to the eight-argument audio handler. Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ents Constrain the no-map baseline to 200/404 so identical upstream failures cannot satisfy the equality check. Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The full expiry sweep ran on every write while holding the shard mutex that every admission's is_denied also takes. It now runs only when a write would otherwise hit capacity; reads already compare against now, and replay checks treat a lingering expired jti as absent. Cross-pod merges share the local merge path, removing an unreachable! that could kill the consumer task. jti is capped at 512 bytes so the 2x-capacity reservation budget bounds memory. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
…criber starts The broadcast receiver was created only after AppState construction and JWKS warm, so disconnects relayed during boot hit a receiverless channel and were dropped. Subscribing first lets the receiver buffer them until the consumer runs. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
…jecting A pod whose clock trailed the origin rejected at-ceiling commands the origin had already accepted with 200, leaving the target's sessions open on that pod. The consumer now clamps until to its own ceiling and still merges and closes; malformed pubkey, unknown issuer, and unrepresentable timestamps stay rejected. Also documents that enforce-mode issuers must carry maximum_command_age_seconds and authorized_principals (startup already required them), and drops a 4s wall-clock admission test whose expiry behavior is pinned with an injected clock. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
FI-TRACE-DENIAL-ORACLE requires every authorization_denied row to be byte-identical, but S3 key mismatch and lease expiry closed with Close(None) while S4 deny-set hits and admin disconnects closed 1008. Every root and audio authorization_denied now goes through one first-writer-wins transition that enqueues the denial frame only for the winner and closes 1008, so a concurrent registry scan can no longer queue a second audio denial. OK(true) is also skipped when a concurrent disconnect already cancelled the session. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
…i-port * origin/main: test(desktop): fix flaky forum empty-draft replacement e2e (#7981) chore(release): release Buzz Desktop version 0.5.26 (#7980) fix(desktop): correct Grok Build setup guide URL (#4846) fix(db): audit partition catalog before creation (#6515) feat(mobile): show contextual names in channel conversations (#7895) Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Pins the current contract of the three assertion-verification sites (HTTP guard, WS upgrade, HTTP admission) and the two key-equality checks before they are consolidated: verifier error classes, transport-before-verifier precedence, NIP-98-before-assertion ordering, mode short-circuits, and claimless-assertion pairing denials. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The HTTP guard, WS upgrade check and HTTP admission each carried their own copy of extract-then-verify-then-map, and HTTP admission and WS pairing each carried their own key-equality check. A relay-private nip_fi_core now owns evaluation, denial rendering and the pairing predicate so later changes (community binding, shadow mode) land in one place. Behavior is unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…cation A routed request with a valid NIP-98 proof now counts both assertion verifications (the HTTP guard, then handler admission) and shows that admission's own failure decides the response. Also moves test-only imports out of production scope in nip_fi_http and points the router ownership comment at nip_fi_core. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… claim The (iss, jti) reservation lived only in the receiving pod's deny map, so one captured command JWT was accepted once per pod, each acceptance re-publishing a cluster-wide disconnect. The verifier now takes an atomic Redis SET NX EX claim after authentication, mirroring the NIP-98 guard, and fails closed when Redis errors. The claim is released when the local insert hits capacity so a 503 deny set full stays retryable. Co-authored-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
…i-port * origin/main: fix(agents): stop built-in prompts from teaching sleep polling (#7992) feat(relay): add direct staff ban/timeout/delete with staff guard (#7883) fix(ci): gate security review on repo write access (#7986) feat(acp): wrap workers at the subprocess launch boundary (#7985) feat(buzz-relay): idempotent owner community deletion with quota reservation (#7969) feat(mobile): show contextual names in lists, Search and Pulse (#7896) Add Kimi Code's default install path to managed-agent binary discovery (#5997) Co-authored-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Truncating the remaining validity let the shared claim expire up to a second before a verifier trailing by the full skew stopped accepting the JWT; with skew above the guard's 120s floor the floor did not hide it. Also gate the local-only verify_at behind test-utils and state the fail-closed guarantee precisely. Co-authored-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
…blishes A forged command carrying a real command's (iss, jti) must be rejected without touching the shared claim, so the legitimate command still succeeds on another pod. The Redis variants subscribe to the disconnect channel to prove rejected replays and guard errors publish nothing while the accepted command publishes once. Co-authored-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
A fixed sleep could miss a late prohibited publish or fail on a slow legitimate one, and a dead collector read as zero. Each count now waits for an accepted sentinel command's publication, giving every case a positive control, and fails loudly if the collector stops. Also split the verify() doc into the normal claim lifecycle and the separate ways a claim can outlive a failure. Co-authored-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Pins exact status, content type, challenge and body bytes for every pre-success rejection and the header -> body -> pubkey -> verifier order before the route is moved onto the shared parser and CommandError rendering. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…I disconnect The disconnect route kept its own copy of the Bearer parser and hand-typed denial bodies. It now uses nip_fi_core::extract_bearer_token/http_denial and renders every command rejection through CommandError, so the strings live in one place. Reachable header bytes are visible ASCII or tab (HeaderValue::to_str), where the old Unicode and new ASCII whitespace checks agree. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…nip-fi-auth-consolidation Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Route transport rows reached the verifier, which returned the same 403, so dropping a transport check went unnoticed. Send them with a malformed body, and pin a comma-joined value ahead of the missing-verifier check. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
A capacity failure after the Redis claim relied on a DEL to keep the command retryable; a failed DEL left the jti burned. Reserving a pending slot under the shard lock first means DenySetFull never touches Redis, and an RAII Reservation frees the slot on replay, guard error, or cancellation. CommandReplayGuard::release is removed as it has no callers. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Disconnect publishes are detached tasks, so a sentinel publish alone did not prove earlier ones had landed. Spawning them through a TaskTracker on AppState lets the test wait for completion first; production still never waits on it. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
merge_entry only exempted keys already in entries, so with a full shard a remote deny for a key this pod had reserved was dropped, and lost entirely if the local attempt then lost the shared claim. The publish-count test now drains every pod that could publish, not just the sentinel pod. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The nip_fi_core doc line landed under nip_fi_gate's, leaving the gate undocumented, and two comments still pointed at nip_fi_http for logic that moved to nip_fi_core. The NIP-98-order test now notes where it departs from the spec's admission order and why routed clients don't observe it. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
http_denial still used expect() while the disconnect route's command_denial fell back to 500, and plain_response fell back to an empty 200. All three branches are unreachable with constant inputs; they now fall back to a bare denial status so the disconnect route fails one way, closed. The module doc also names the disconnect route as a second consumer of the shared helpers. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
The core, HTTP, upgrade, and router tests each carried their own copy of the same fixed-result, call-counting verifier stub, so a VerifyAssertion change had to be repeated four times. They now share one in nip_fi_core::tests. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
…ed builder A ConnectionState field change should touch one test builder, not three. The shared builder now accepts a NIP-FI assertion and returns every outbound receiver, so the session tests stop hand-building the struct. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
The capacity-rejection counter on the DenySetFull arm was untested: deleting it left the suite green. Pin that it counts DenySetFull exactly once and never counts other command-error arms. Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
marked this pull request as ready for review
October 1, 2026 20:51
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
wpfleger96
enabled auto-merge (squash)
October 1, 2026 22:37
bradseiler
approved these changes
Oct 1, 2026
wpfleger96
added a commit
that referenced
this pull request
Oct 1, 2026
* commit '9b083957f^': Include thread roots in agent activity events (#8029) fix(relay): gate owner-only kinds in shared fan-out access filter (#8006) feat(acp): add BUZZ_GIT_IDENTITY switch for agent commit identity (#8024) fix(relay): deny channel writes when the channel lookup fails (#8007) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
added a commit
that referenced
this pull request
Oct 1, 2026
wpfleger96
added a commit
that referenced
this pull request
Oct 1, 2026
🤖 Follows #7990 (merged). Binds every enforce-mode NIP-FI assertion to the community served at the request's `Host`. Before this, an assertion was accepted if its `aud` matched any value in a deployment-wide set. That meant an issuer trusted by one community could mint an assertion another community accepted. **Rollout consequence: an enforce relay will not start without `BUZZ_NIP_FI_COMMUNITIES`.** `off` and `deny_protected` do not read it, so repair mode still boots. ## What changes - **Host resolution runs first.** In enforce mode, right after the `off` and `deny_protected` checks, the `Host` must map to a configured community. An unmapped or missing `Host` returns 503 `authorization unavailable`. This happens at all three call sites: the router guard, the upgrade (WS root h1/h2 and audio), and HTTP admission. Off mode behaves exactly as before. - **The allowlist is checked before JWKS.** `VerifyAssertion::verify_assertion(token, &CommunityBinding)` rejects an issuer the community does not authorize before any key-source lookup. Clients can't tell this rejection apart from an unknown `iss`. `aud` must exactly equal the community's canonical URI. - **New config: `BUZZ_NIP_FI_COMMUNITIES`.** It is a JSON array of `{canonical_uri, authorized_issuers}`. Startup fails if: - a `canonical_uri` is anything other than a canonical `https://<host>[:port]`: the URL parser must reserialize the configured authority byte-for-byte, so whitespace, control characters, backslashes, uppercase, a redundant `:443`, and non-canonical IPv6 are rejected rather than repaired. The authority must also already be Host-normalized, so a trailing dot or `:80` is rejected and the Host map key always equals the `aud` authority exactly (never empty); - two communities share a `Host`; - an allowlisted issuer is missing from `BUZZ_NIP_FI_ISSUERS`; - an allowlist is empty; - a configured issuer belongs to no community. Errors name entries by index only. - **`IssuerPolicy.audiences` is removed.** Command JWTs use the new per-issuer `command_audiences`, so #7977's admin-disconnect command behavior is unchanged. A config that still sets `audiences`, even to `null`, fails with a migration message instead of being silently ignored. - **`AssertionPolicyId` now covers the community allowlist.** It hashes the `aud` of each community that authorizes the issuer, so changing the allowlist changes the ID. This is code only; no spec file changes here. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
tlongwell-block
pushed a commit
that referenced
this pull request
Oct 2, 2026
Main added nine commits since the previous merge. The ones that matter here rework NIP-FI admission under /buzz/v1: shared assertion evaluation across adapters (#7990), binding assertions to the request Host's community (#8028), and the community ban gaps (#8005, #8006, #8007, #8036). Main changes 55 files and adds no migration. Git merged the three files both sides change without conflict: buzz-relay's api/bridge.rs, api/mod.rs and router.rs. No hand edit was needed. This branch's diff against main is line for line the same before and after the merge: 31 files, +5,726 / -81. /buzz/v1 still authenticates through admit_nip_fi_http_on_state. Its signature is unchanged and it now resolves the Host's community itself, so the accessory routes take the new binding from the same shared admission as the bridge routes. The three /buzz/v1 NIP-FI wire-contract tests pass on the merged tree. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
abipalli
pushed a commit
to abipalli/buzz
that referenced
this pull request
Oct 7, 2026
…rs (block#7990) 🤖 Stack: block#7990 → block#8028 The three production NIP-FI assertion-verification sites (the HTTP assertion guard in `router.rs`, `check_nip_fi_at_upgrade` in `nip_fi_upgrade.rs`, and `admit_nip_fi_http` in `nip_fi_http.rs`) each carried their own extract → verify → map-to-denial sequence, and HTTP admission and `enforce_nip_fi_key_pairing` each carried their own key-equality check. The admin disconnect route (`POST /api/nip-fi/disconnect`) kept a second copy of the header parser and hand-typed its denial bodies. Community binding and shadow mode both need to change exactly those decisions, so they now live in one place. **No behavior change.** Every denial class, status code, body, header and log line is preserved, including `deny_protected` (503), the startup-race 503, transport-before-verifier precedence, NIP-98-before-assertion ordering, and guard + handler double verification. - New relay-private `nip_fi_core` owns `extract_bearer_token` and `http_denial` (moved from `nip_fi_http`), `evaluate_attached_assertion` (the single `verify_assertion` call for all three adapters, returning a typed `AssertionRejection`), and `asserted_key_matches` (the pairing predicate; a claimless assertion never matches). `http_denial` and the disconnect route's plain-text renderer fall back to the bare denial status instead of panicking or answering an empty 200; both fallbacks are unreachable because every status and header value is a constant. - Adapters keep their own mode short-circuits, exemptions, ordering, metrics and `code()` debug logs; the guard still logs nothing. - The disconnect route parses its header with `extract_bearer_token`, renders header failures with `http_denial`, and renders every other rejection (malformed body, bad pubkey, no command verifier, every `CommandError`) through `CommandError::http_status()`/`response_body()`. Its check order (header → body → pubkey → verifier present → verify), `DenySetFull` warning and metric, success body and cross-pod publish are unchanged. The old Unicode whitespace check and the shared ASCII one agree on every reachable input, because `HeaderValue::to_str` only admits visible ASCII and tab. - The 12 HTTP admission callers, 3 WS upgrade placements, deny-map checks, expected-URL constructors and the command/admin proof path are untouched. - Characterization tests land before each refactor, green against the unmodified code, and pass unchanged after it. A routed test counts both verifications on one request (guard, then handler admission) and shows admission's own failure decides the response. Disconnect-route tests pin exact status, content type, challenge and body for every rejection, the observable check order, and that only `DenySetFull` increments `buzz_nip_fi_disconnect_capacity_rejections_total`. The test modules share one counting `ScriptedVerifier` from `nip_fi_core::tests`, and the session tests build connections through `connection::tests::test_conn`. `nip_fi_core::tests::` is added to the unit selectors in `Justfile` and `scripts/run-tests.sh`. 🤖 Implemented by Hayt. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
abipalli
pushed a commit
to abipalli/buzz
that referenced
this pull request
Oct 7, 2026
…8028) 🤖 Follows block#7990 (merged). Binds every enforce-mode NIP-FI assertion to the community served at the request's `Host`. Before this, an assertion was accepted if its `aud` matched any value in a deployment-wide set. That meant an issuer trusted by one community could mint an assertion another community accepted. **Rollout consequence: an enforce relay will not start without `BUZZ_NIP_FI_COMMUNITIES`.** `off` and `deny_protected` do not read it, so repair mode still boots. ## What changes - **Host resolution runs first.** In enforce mode, right after the `off` and `deny_protected` checks, the `Host` must map to a configured community. An unmapped or missing `Host` returns 503 `authorization unavailable`. This happens at all three call sites: the router guard, the upgrade (WS root h1/h2 and audio), and HTTP admission. Off mode behaves exactly as before. - **The allowlist is checked before JWKS.** `VerifyAssertion::verify_assertion(token, &CommunityBinding)` rejects an issuer the community does not authorize before any key-source lookup. Clients can't tell this rejection apart from an unknown `iss`. `aud` must exactly equal the community's canonical URI. - **New config: `BUZZ_NIP_FI_COMMUNITIES`.** It is a JSON array of `{canonical_uri, authorized_issuers}`. Startup fails if: - a `canonical_uri` is anything other than a canonical `https://<host>[:port]`: the URL parser must reserialize the configured authority byte-for-byte, so whitespace, control characters, backslashes, uppercase, a redundant `:443`, and non-canonical IPv6 are rejected rather than repaired. The authority must also already be Host-normalized, so a trailing dot or `:80` is rejected and the Host map key always equals the `aud` authority exactly (never empty); - two communities share a `Host`; - an allowlisted issuer is missing from `BUZZ_NIP_FI_ISSUERS`; - an allowlist is empty; - a configured issuer belongs to no community. Errors name entries by index only. - **`IssuerPolicy.audiences` is removed.** Command JWTs use the new per-issuer `command_audiences`, so block#7977's admin-disconnect command behavior is unchanged. A config that still sets `audiences`, even to `null`, fails with a migration message instead of being silently ignored. - **`AssertionPolicyId` now covers the community allowlist.** It hashes the `aud` of each community that authorizes the issuer, so changing the allowlist changes the ID. This is code only; no spec file changes here. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
This branch was successfully deployed
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.
🤖 Stack: #7990 → #8028
The three production NIP-FI assertion-verification sites (the HTTP assertion guard in
router.rs,check_nip_fi_at_upgradeinnip_fi_upgrade.rs, andadmit_nip_fi_httpinnip_fi_http.rs) each carried their own extract → verify → map-to-denial sequence, and HTTP admission andenforce_nip_fi_key_pairingeach carried their own key-equality check. The admin disconnect route (POST /api/nip-fi/disconnect) kept a second copy of the header parser and hand-typed its denial bodies. Community binding and shadow mode both need to change exactly those decisions, so they now live in one place.No behavior change. Every denial class, status code, body, header and log line is preserved, including
deny_protected(503), the startup-race 503, transport-before-verifier precedence, NIP-98-before-assertion ordering, and guard + handler double verification.nip_fi_coreownsextract_bearer_tokenandhttp_denial(moved fromnip_fi_http),evaluate_attached_assertion(the singleverify_assertioncall for all three adapters, returning a typedAssertionRejection), andasserted_key_matches(the pairing predicate; a claimless assertion never matches).http_denialand the disconnect route's plain-text renderer fall back to the bare denial status instead of panicking or answering an empty 200; both fallbacks are unreachable because every status and header value is a constant.code()debug logs; the guard still logs nothing.extract_bearer_token, renders header failures withhttp_denial, and renders every other rejection (malformed body, bad pubkey, no command verifier, everyCommandError) throughCommandError::http_status()/response_body(). Its check order (header → body → pubkey → verifier present → verify),DenySetFullwarning and metric, success body and cross-pod publish are unchanged. The old Unicode whitespace check and the shared ASCII one agree on every reachable input, becauseHeaderValue::to_stronly admits visible ASCII and tab.DenySetFullincrementsbuzz_nip_fi_disconnect_capacity_rejections_total. The test modules share one countingScriptedVerifierfromnip_fi_core::tests, and the session tests build connections throughconnection::tests::test_conn.nip_fi_core::tests::is added to the unit selectors inJustfileandscripts/run-tests.sh.🤖 Implemented by Hayt.