Revoke a token from Kotlin, unsigned, following no redirect

Merged
JE
4d ago
+224-5

Signing out of HEY ends the session behind a token with DELETE /oauth/tokens. The Kotlin client had no way to send it safely: written by hand through the client, the revocation goes out under /oauth/tokens.json, refreshes and resends on a 401, and follows a redirect with the token in its body.

What changes (Kotlin)

HeyClient.revokeToken(clientId, token):

  • sends client_id and token in a form body under the bare path, never in the query, where logs would keep the token;
  • is unsigned: the client's own credentials may be the very token being revoked, so a 401 (for example invalid_client for another app's token) is the answer, not a reason to refresh them and send the revocation again;
  • follows no redirect (new Operation.noRedirects()): a DELETE keeps its body on a 307/308, so a hop would hand the token to whatever host the Location names. Any 3xx is an api_error carrying its status, with no hop. unsigned() alone still follows, as the storage reads it was made for want;
  • treats HEY's 200 {} for a token it no longer knows as no error;
  • is heard by the hooks as OAuth.RevokeToken, a write.

/oauth routes sit outside the API model (drift-check-reverse skips them), and the Go, Rust and TypeScript token exchanges are hand-written for the same reason. So this is a hand-written convenience, not a generated operation.

Red first

The first commit runs the tests against the revocation a caller would write by hand (request(DELETE, "/oauth/tokens").form(...)). Three of the five fail:

  • the path goes out as /oauth/tokens.json;
  • a 401 is refreshed and sent again (it hit the mock's "no more responses" 500);
  • a 307 is followed.

The two that pass (200 {} is no error; the hooks' name) pin what the fix must keep.

Verified

RevocationTest 5/5, make kt-check kt-check-drift, Kotlin conformance 219/219. Nothing outside kotlin/ changes. The repo-wide make check failures main already has (cargo deny's webpki-root-certs advisory; locally, Swift 6.3 warnings-as-errors) are untouched by this. Merges cleanly with #268 and #269.

Not done

  • No conformance case. No other SDK revokes a token. The runners read only top-level cases, and only TypeScript can mark one not applicable, so a case would need a RevokeToken in four SDKs this change doesn't touch.
  • Revocation for Go, Rust, TypeScript and Swift: none has one today. That's a follow-up if wanted.

Summary by cubic

Adds a revokeToken(clientId, token) extension on HeyClient so signing out of HEY revokes the session token over DELETE /oauth/tokens, a route hand-written like the /oauth exchanges in the other SDKs.

  • Sends client_id and token in a form body under the bare path, never in the query where logs would keep the token.
  • Sends unsigned, with none of a caller's own AuthStrategy headers or account filter: a 401 is the answer, not a reason to refresh credentials and resend the revocation.
  • Follows no redirect (new Operation.noRedirects()): a DELETE keeps its body on a 307/308, so a hop would hand the token to the redirect target. Any 3xx is taken as the answer without its body read, so a body lost mid-read cannot turn into a retried revocation; it is heard by the hooks as its status and surfaced as a HeyException.Api.
  • Treats HEY's 200 {} for a token it no longer knows as success and emits the hooks event OAuth.RevokeToken as a write; the README example shows the import a caller needs.