feat(auth,payments): revoke_all and redrive — two endpoints the SDK never grew - #24
Merged
Merged
Conversation
…ever grew Both have been on the gateway for weeks and reachable from none of the three SDKs. This is the Ruby half; rail0-ts#31 and rail0-go#38 carry the same pair. POST /auth/revoke_all ends EVERY session of the calling address. #logout cannot be that answer: it is per TOKEN, so an address with five live sessions needs five tokens the caller does not have. This is per ADDRESS and reaches the ones it never saw — including any an attacker is holding. The gateway records a cutoff INSTANT rather than enumerating tokens, so a session minted a moment before the call is refused by its own `iat`: there is nothing to enumerate and nothing to miss, which is what makes it durable where a denylist is not. `cutoff` is returned because it says exactly which sessions died, which `revoked: true` cannot. POST /payments/:id/transactions/:transaction_id/redrive re-enqueues the one shape of stuck a retry can fix: `pending` with the signed bytes already held, prepared and signed but never landed. Nothing about the payment changes. Callers should offer it on the row's `redrivable` flag — the same predicate the gateway guards the route with — and not on `status == "pending"`: a pending row holding no signed transaction is not redrivable, and there the next step is submitting the signature. The specs pin what a wrong implementation gets wrong: that revoke_all surfaces the CUTOFF and not merely the boolean, and that redrive addresses the PAYMENT-SCOPED path. The gateway resolves the transaction id through the payment, so a client that built that path from the transaction id alone would look correct until it retried a stranger's broadcast. 168 examples, 0 failures; rubocop clean. README documents both, including why logout and revoke_all are different questions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Both have been on the gateway for weeks and reachable from none of the three SDKs. Found while
auditing all three against the gateway's 67-operation surface — this is the Ruby half
(rail0-ts#31, rail0-go#38 carry the same pair).
POST /auth/revoke_all→client.auth.revoke_allEnds every session of the calling address.
#logoutcannot be that answer: it is per token,so an address with five live sessions needs five tokens the caller does not have. This is per
address and reaches the ones it never saw — including any an attacker is holding.
The gateway records a cutoff instant rather than enumerating tokens, so a session minted a
moment before the call is refused by its own
iat: there is nothing to enumerate and nothing tomiss, which is what makes it durable where a denylist is not.
cutoffis returned because it saysexactly which sessions died, which
revoked: truecannot.POST /payments/:id/transactions/:transaction_id/redrive→client.payments.redriveRe-enqueues the one shape of stuck a retry can fix:
pendingwith the signed bytes already held —prepared and signed, never landed. Nothing about the payment changes.
Offer it on the row's
redrivableflag — the same predicate the gateway guards the route with —and not on
status == "pending": a pending row holding no signed transaction is notredrivable, and there the next step is submitting the signature.
Specs
Both pin what a wrong implementation gets wrong:
revoke_allsurfaces the cutoff, not merely the boolean;redriveaddresses the payment-scoped path. The gateway resolves the transaction id throughthe payment, so a client that built that path from the transaction id alone would look correct
until it retried a stranger's broadcast.
168 examples, 0 failures; rubocop clean. README documents both, including why
logoutandrevoke_allare different questions.🤖 Generated with Claude Code