Skip to content

fix: bind response authentication to admitted channels - #983

Draft
alexanderludwig wants to merge 1 commit into
feat/kbf-verificationfrom
fix/repsonse-aut
Draft

fix: bind response authentication to admitted channels#983
alexanderludwig wants to merge 1 commit into
feat/kbf-verificationfrom
fix/repsonse-aut

Conversation

@alexanderludwig

@alexanderludwig alexanderludwig commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes two seller-runtime defects that both produced the same externally visible symptom: a paid request returned 200 with a signed ResponseAuth whose channelId was empty. The verifier correctly refuses to count such responses (channel_id_mismatch), so an otherwise healthy seller was scored UNDETERMINED across every audit batch despite answering the probes correctly. This PR makes both failure modes impossible rather than weakening the verifier check.

  • Paid requests now fail closed (503 payment_unavailable) when the runtime has no initialized payment infrastructure, instead of being served free and unauthenticated.
  • The admitted payment channel is captured once, before provider execution, and reused for billing, NeedAuth, and ResponseAuth.
  • Inconsistent session vs. channel-store state returns 402 so the buyer renegotiates, instead of serving a channel-less paid response.
  • Seller startup no longer silently disables payments after a single failed RPC probe. ANTSEED_ENABLE_SETTLEMENT=false remains the explicit opt-out.

Bug 1 — paid requests served with no payment infrastructure

Cause. The only admission gate for paid requests was guarded by if (this._deps.channelsClient && !spmAuthorized). When channelsClient was null, the entire 402 branch was skipped and the request fell straight through to the provider. Every downstream payment step (isBillable, spend recording, the ResponseAuth channel lookup) used spm?. optional chaining, so with a null payment manager they quietly evaluated to "nothing to bill" and channelId: null. Result: a free 200 with an unauthenticated receipt, and no error anywhere.

How a production seller gets there. seller start set paymentsEnabled = false whenever a single 1.5 s eth_chainId probe against the RPC failed at boot, printing a one-line warning and continuing. Node._initializePayments() returns early when payments are disabled, so neither ChannelsClient nor SellerPaymentManager is created for the lifetime of the process. One slow or briefly unreachable RPC at startup was enough to put a seller into this state indefinitely.

Fix. Paid requests return 503 payment_unavailable before the provider is called if sellerPaymentManager or channelsClient is missing (buyers already treat 5xx as seller-5xx and fail over). The startup RPC auto-disable (isRpcReachable + the 1.5 s probe) is removed — payments stay enabled unless ANTSEED_ENABLE_SETTLEMENT=false is set explicitly. A seller started that way with paid services now gets the runtime 503 on every paid request instead of serving them free.

Reproduction. The new test rejects paid requests when payment infrastructure is unavailable fails against the previous handler with expected 200 to be 503.

Bug 2 — admission and response signing read different state

Cause. The handler consulted "does this buyer have a channel?" three separate times through two different sources of truth:

  1. Admission: spm.hasSession(buyerPeerId) — an in-memory Set of active buyers.
  2. Post-response billing: spm.getChannelByPeer(buyerPeerId) — a SQLite channel_store row lookup.
  3. ResponseAuth signing: spm.getChannelByPeer(buyerPeerId) again.

If the in-memory set and the store disagreed (e.g. after restart hydration), or the channel was evicted/closed while the provider was still generating — which can take tens of seconds — the request was admitted, ran, and then steps 2 and 3 found no channel. Spend was not recorded and the response was signed with channelId: null, again as a 200.

Fix. Admission now requires both hasSession() and an active channel-store row to agree; disagreement returns 402 so the buyer reopens the channel. On success the store row is captured as admittedSession before _executeRequest, and isBillable, the billing/NeedAuth block, and ResponseAuth all read from that captured object instead of re-querying. State changes mid-inference can no longer alter what the response is bound to.

Reproduction. binds response auth to the channel admitted before provider execution mocks getChannelByPeer to return a channel for admission and null afterwards, and asserts the signed channelId is still the admitted one. rejects paid requests when the payment manager and channel store disagree covers the 402 path.

Note on attribution

The exact trigger on the affected production seller (RPC outage at boot, restart, environment change, or local state drift) cannot be determined without its runtime logs. Both defects produce the identical observable and both are closed here. The verifier's channel_id_mismatch check is intentionally left as-is.

Scope

Kept deliberately minimal: seller-request-handler.ts is +34/−6 (the NeedAuth payload block is untouched), and seller/start.ts is a pure deletion of the RPC-probe auto-disable. No new startup prerequisite checks or API changes.

Validation

  • pnpm run build:tier0 / build:tier1 / build:tier2 / build:tier3
  • pnpm --filter @antseed/node typecheck and test — 1,020 tests passed
  • pnpm --filter @antseed/cli typecheck and test — 474 tests passed
  • Regression tests in packages/node/tests/seller-response-auth-compat.test.ts (each fails against the previous handler)
  • git diff --check

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.

1 participant