cratefield-oauth-client
OAuth 2.0 for the harness (issue #531): the authorize URL, the code
exchange, the refresh and the revocation, spoken once over the harness's
HttpClient port — so a module runs it
unchanged on Workers and native. No provider SDK, no discovery document:
the provider's three URLs are configuration.
ProviderConfignames the endpoints, the client credentials, the scopes, and where the credentials go on token calls (form body, or HTTP Basic per RFC 6749 §2.3.1).OAuthClientspeaks the token endpoint: authorization-code exchange (with PKCE, RFC 7636, when the provider supports it), refresh, and RFC 7009 revocation. Failures come back asOAuthError, which carries the RFC 6749 §5.2error/error_descriptionpair and the raw body, because providers put provider-specific meaning in that prose.TokenSealeris how a caller stores what came back. The trait is the future seam for the secrets store;XChaChaSealeris the implementation the harness already chose (ADR 0102): XChaCha20-Poly1305, a fresh nonce per encryption, the ciphertext bound to its row and column.send_with_refreshis the whole 401 policy: one refresh, one replay, never a loop.devicespeaks RFC 8628 device authorization:request_codegets auser_codethe human types at the verification URI, andpollwaits out the interval (honouringslow_down) until the venture issues its credential. The credential is the venture's own contract, sopollreturns the raw JSON —poll_as::<T>when the caller has a type.
let client = OAuthClient::new(http, &config);
let tokens = client.exchange_code(&code, &redirect_uri, None).await?;
let kept = tokens.rotated_refresh_token(&previous_refresh_token);
Device authorization (RFC 8628), for the client that has no browser — a CLI
or a screen. The human approves on another device; the poll runs on clock,
so it obeys the server's interval without a timer of its own:
let auth = request_code(http, "https://api.example.com/v1/device-auth/code",
"client-1", Some("read"), Some("My CLI")).await?;
println!("Go to {} and enter {}", auth.verification_uri, auth.user_code);
let credential: Credential = poll_as(http, clock, "https://api.example.com/v1/device-auth/token",
"client-1", &auth).await?;
wasm-safe: randomness comes from getrandom (wasm backend enabled on
wasm32), time stays in the caller, HTTP goes through the port.