fix: bind response authentication to admitted channels - #983
Draft
alexanderludwig wants to merge 1 commit into
Draft
fix: bind response authentication to admitted channels#983alexanderludwig wants to merge 1 commit into
alexanderludwig wants to merge 1 commit into
Conversation
alexanderludwig
force-pushed
the
fix/repsonse-aut
branch
from
September 4, 2026 09:21
2468802 to
60ed96f
Compare
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
Fixes two seller-runtime defects that both produced the same externally visible symptom: a paid request returned
200with a signedResponseAuthwhosechannelIdwas empty. The verifier correctly refuses to count such responses (channel_id_mismatch), so an otherwise healthy seller was scoredUNDETERMINEDacross every audit batch despite answering the probes correctly. This PR makes both failure modes impossible rather than weakening the verifier check.503 payment_unavailable) when the runtime has no initialized payment infrastructure, instead of being served free and unauthenticated.NeedAuth, andResponseAuth.402so the buyer renegotiates, instead of serving a channel-less paid response.ANTSEED_ENABLE_SETTLEMENT=falseremains 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). WhenchannelsClientwasnull, the entire402branch was skipped and the request fell straight through to the provider. Every downstream payment step (isBillable, spend recording, theResponseAuthchannel lookup) usedspm?.optional chaining, so with anullpayment manager they quietly evaluated to "nothing to bill" andchannelId: null. Result: a free200with an unauthenticated receipt, and no error anywhere.How a production seller gets there.
seller startsetpaymentsEnabled = falsewhenever a single 1.5 seth_chainIdprobe against the RPC failed at boot, printing a one-line warning and continuing.Node._initializePayments()returns early when payments are disabled, so neitherChannelsClientnorSellerPaymentManageris 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_unavailablebefore the provider is called ifsellerPaymentManagerorchannelsClientis missing (buyers already treat 5xx asseller-5xxand fail over). The startup RPC auto-disable (isRpcReachable+ the 1.5 s probe) is removed — payments stay enabled unlessANTSEED_ENABLE_SETTLEMENT=falseis set explicitly. A seller started that way with paid services now gets the runtime503on every paid request instead of serving them free.Reproduction. The new test
rejects paid requests when payment infrastructure is unavailablefails against the previous handler withexpected 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:
spm.hasSession(buyerPeerId)— an in-memorySetof active buyers.spm.getChannelByPeer(buyerPeerId)— a SQLitechannel_storerow lookup.ResponseAuthsigning: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 a200.Fix. Admission now requires both
hasSession()and an active channel-store row to agree; disagreement returns402so the buyer reopens the channel. On success the store row is captured asadmittedSessionbefore_executeRequest, andisBillable, the billing/NeedAuthblock, andResponseAuthall 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 executionmocksgetChannelByPeerto return a channel for admission andnullafterwards, and asserts the signedchannelIdis still the admitted one.rejects paid requests when the payment manager and channel store disagreecovers the402path.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_mismatchcheck is intentionally left as-is.Scope
Kept deliberately minimal:
seller-request-handler.tsis +34/−6 (the NeedAuth payload block is untouched), andseller/start.tsis 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:tier3pnpm --filter @antseed/node typecheckandtest— 1,020 tests passedpnpm --filter @antseed/cli typecheckandtest— 474 tests passedpackages/node/tests/seller-response-auth-compat.test.ts(each fails against the previous handler)git diff --check