Skip to content

feat(auth,payments): revoke_all and redrive — two endpoints the SDK never grew - #24

Merged
pviti merged 1 commit into
mainfrom
feature/revoke-all-and-redrive
Aug 27, 2026
Merged

feat(auth,payments): revoke_all and redrive — two endpoints the SDK never grew#24
pviti merged 1 commit into
mainfrom
feature/revoke-all-and-redrive

Conversation

@pviti

@pviti pviti commented Aug 27, 2026

Copy link
Copy Markdown
Member

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_allclient.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/redriveclient.payments.redrive

Re-enqueues the one shape of stuck a retry can fix: pending with the signed bytes already held —
prepared and signed, never landed. Nothing about the payment changes.

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.

Specs

Both pin what a wrong implementation gets wrong:

  • revoke_all surfaces the cutoff, not merely the boolean;
  • 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.

🤖 Generated with Claude Code

…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>
@pviti
pviti merged commit 84cb400 into main Aug 27, 2026
1 check passed
@pviti
pviti deleted the feature/revoke-all-and-redrive branch August 27, 2026 21:18
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