ext-oauth

    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    9 hours ago
    Downloads
    6

    #posoco-ext-oauth

    Targets: native + js ship the defaults. The traits below compile on every target, but the default HTTP transport and the callback-server factories exist only on native and js; a wasm-gc host injects its own impls.

    Shared OAuth machinery for Posoco providers — the OAuthProvider contract, credential stores, RFC 8628 device flow, RFC 6749 refresh, RFC 7591 dynamic registration + metadata discovery, and a hardened PKCE authorization-code browser flow. The package owns the shared flow logic; providers supply endpoints and client config.

    #Ports contributed

    This package is a library, not an Agent extension: it defines no Extension. Hosts and provider packages implement or consume these traits:

    TraitRole
    OAuthProviderthe provider contract: provider_id / login / refresh / to_auth
    AuthInteractionhost UI channel — AuthUrl / DeviceCode / Progress messages plus is_cancelled
    AuthPromptInteractionoptional prompt seam (Secret / Select), kept separate from AuthInteraction so OAuth-only hosts stay source compatible
    CallbackServerlocal loopback callback seam for the PKCE browser flow
    OAuthHttpTransportHTTP seam used by every flow; DefaultOAuthHttpTransport ships on native + js only
    CredentialStore / ProviderCredentialStore / ApiKeyStorecredential storage; ProviderCredentialStore keeps one tagged record per provider and preserves the user's explicit auth-method choice across restarts

    NoopAuthInteraction, NoopAuthPromptInteraction, and the InMemory* stores ship for headless hosts and tests.

    #Usage

    moon add posoco/ext-oauth

    // moon.pkg: "posoco/ext-oauth" @oauth

    let server : &@oauth.CallbackServer = @oauth.create_callback_server()
    let transport : &@oauth.OAuthHttpTransport =
    @oauth.DefaultOAuthHttpTransport::DefaultOAuthHttpTransport()
    let credential = @oauth.run_pkce_browser_flow(
    {
    client_id: "your-client-id",
    authorize_url: "https://auth.example.com/authorize",
    token_url: "https://auth.example.com/token",
    redirect_uri: "http://localhost:1455/auth/callback",
    callback_port: 1455,
    scope: "openid profile email offline_access",
    extra_authorize_params: [],
    fallback_port: Some(1457),
    },
    interaction, // your &AuthInteraction impl
    server,
    transport,
    )

    #PKCE browser flow

    run_pkce_browser_flow runs the full authorization-code grant with PKCE (S256) against a local loopback callback server. Prefer it whenever the provider's authorization server supports it — PKCE binds the authorization request to a verifier known only to the client. The device flow below remains the login path for providers that require it (Kimi Code subscription login is RFC 8628 device flow).

    #Configuration

    PkceFlowConfig fields:

    FieldMeaning
    client_idpublic client id, sent in the authorize URL and the token exchange
    authorize_urlprovider authorization endpoint, e.g. https://auth.openai.com/oauth/authorize
    token_urlprovider token endpoint, e.g. https://auth.openai.com/oauth/token
    redirect_uriloopback callback URI, e.g. http://localhost:1455/auth/callback
    callback_portport the local callback server binds
    scopescope string, url-encoded into the authorize URL
    extra_authorize_paramsprovider-specific authorize params (e.g. Codex's originator), appended url-encoded in array order
    fallback_portretry port when callback_port is occupied — see the rewrite rule below

    #How a run unfolds

    1. Generate the PKCE pair and a random state.
    2. Build the authorize URL: response_type=code, client id, url-encoded redirect_uri + scope, code_challenge + code_challenge_method=S256, state, then extra_authorize_params.
    3. Start the callback server on callback_port; when that bind fails, retry fallback_port if configured and different.
    4. Notify the host to open the URL (AuthMessage::AuthUrl).
    5. Wait for the callback (code, state); with prompt? a manual paste races the server (below).
    6. Validate the state, then exchange code + verifier for tokens.
    7. Stop the server on success and on every raise path after start — a state mismatch or a failed exchange still releases the port. stop is safe to call more than once.

    Fallback-port rule — OAuth requires an identical redirect_uri in the authorize request and the token exchange, so when the fallback port wins, the port segment of redirect_uri is rewritten to the port actually bound; the authorize URL and the exchange both carry it. If both binds fail, the primary bind error is raised.

    State validation — the received state (server callback or pasted) must equal the generated one. A mismatch raises OAuthError::ParseError("oauth state mismatch") before any token exchange.

    Token exchange — a form-encoded grant_type=authorization_code POST (carrying code, code_verifier, redirect_uri) over the injected transport. The optional parse_credential? hook replaces the lenient default parser, which maps an invalid_grant error to OAuthError::InvalidGrant and fills missing refresh_token / expires_in / token_type with "" / 3600 / "Bearer".

    #Manual-paste fallback

    Pass prompt? : &AuthPromptInteraction and the flow races a paste prompt (an AuthPromptRequest::Secret whose message carries the authorize URL) against the callback server:

    • Accepted input is a full callback URL containing code and state query params, or a bare code=..&state=.. query string. A bare code without state is rejected.
    • The pasted state is validated exactly like the server's.
    • An unparseable paste (or a cancelled prompt) raises inside its race task; @async.any(allow_failure=true) ignores raised errors, so the pending callback wait continues and the server can still win.
    • A winning paste stops the server itself before returning.

    #Security properties

    • CSRF state — base64url of 16 random bytes (22 chars), validated on callback; a forged state mints no credential and sends no token request.
    • PKCE verifier — base64url of 48 random bytes (64 chars; RFC 7636 §4.1 allows 43–128). The challenge is base64url(sha256(verifier)), method S256.
    • Entropy chain — oauth_random_bytes probes the backend entropy source: native uses a C getentropy stub (256-byte chunks, EINTR retry, empty result on any failure or unsupported platform); js uses crypto.getRandomValues. A time-seeded LCG runs only when the probe returns anything but exactly n bytes — the wasm / failed-probe fallback, never the intended path for credential backends.
    • Bounded wait — wait_callback raises ExpiredToken after the default 5-minute deadline.
    • No leaked port — the server stops on every exit path after start.

    #The CallbackServer seam

    CallbackServer is the IO seam that keeps the shared flow free of platform HTTP APIs: start(port), wait_callback() -> (code, state), stop(). Each target ships an impl plus a create_callback_server factory:

    TargetImplMechanism
    nativeNativeCallbackServer@http.Server on 127.0.0.1; wait_callback polls accept directly (no background accept loop), answers 200 "Login successful" on a code and 404 otherwise
    jsJsCallbackServerBun.serve; its fetch handler writes code/state into the struct, returns 400 on an error param

    Other targets ship no impl — hosts implement the trait.

    #Device flow (RFC 8628)

    Device flow stays supported for providers whose authorization servers require it:

    // moon.pkg: "posoco/ext-oauth" @oauth

    let config = @oauth.DeviceFlowConfig(
    client_id="your-client-id",
    device_auth_endpoint="https://example.com/device/authorize",
    token_endpoint="https://example.com/token",
    )
    let credential = @oauth.run_device_flow(config, interaction)

    • DeviceFlowConfig::with_default_headers merges provider identity headers into every device-flow POST; the flow's own Content-Type / Accept always override them.
    • Polling sleeps interval seconds (response value, default 5) between polls, honours slow_down (server interval, or +5s), raises ExpiredToken at expires_in (default 900s), and checks is_cancelled between iterations.
    • verification_uri_complete is preferred for display when present; only absolute http/https URIs without whitespace or control characters are accepted.
    • authorization_pending / slow_down are control flow; access_denied, expired_token, and invalid_grant are typed failures. HTTP 400 bodies are parsed (RFC 8628 uses 400 for pending); status >= 500 is terminal without parsing the body.
    • run_device_flow constructs the default transport (native/js only); the wasm variant raises Cancelled — use run_device_flow_with_transport with a host-supplied transport there.

    ApiKeyStore

    pub(open) trait ApiKeyStore {
    async fn read(Self, provider_id : String) -> ApiKeyCredential? raise OAuthError
    async fn write(Self, provider_id : String, credential : ApiKeyCredential) -> Unit raise OAuthError
    async fn delete(Self, provider_id : String) -> Unit raise OAuthError
    }

    Stores provider-neutral API-key credentials. Hosts may back this with the same secure store as OAuth credentials, but the two contracts remain separate so providers never receive an untyped secret map.

    AuthInteraction

    pub(open) trait AuthInteraction {
    fn notify(Self, message : AuthMessage) -> Unit
    fn is_cancelled(Self) -> Bool
    }

    AuthPromptInteraction

    pub(open) trait AuthPromptInteraction {
    async fn prompt(Self, request : AuthPromptRequest) -> String raise OAuthError
    }

    Optional prompt seam for provider-owned API-key login. It is deliberately separate from AuthInteraction so existing OAuth-only hosts remain source compatible; a host that advertises API-key login implements both traits.

    CallbackServer

    pub(open) trait CallbackServer {
    async fn start(self : Self, port : Int) -> Unit raise OAuthError
    async fn wait_callback(self : Self) -> (String, String) raise OAuthError
    fn stop(self : Self) -> Unit
    }

    Local HTTP callback server for PKCE browser flow. Each target provides an impl:
    • native: @http.Server
    • js: Bun.serve via extern "js"

    CredentialStore

    pub(open) trait CredentialStore {
    async fn read(Self, provider_id : String) -> Credential? raise OAuthError
    async fn write(Self, provider_id : String, credential : Credential) -> Unit raise OAuthError
    async fn delete(Self, provider_id : String) -> Unit raise OAuthError
    }

    Stores OAuth credentials keyed by provider_id. Hosts implement this (file-based, keychain, etc.); providers are agnostic to the storage.

    OAuthHttpTransport

    pub(open) trait OAuthHttpTransport {
    async fn post(Self, request : OAuthHttpRequest) -> OAuthHttpResponse raise OAuthError
    async fn get(Self, url : String, headers : Map[String, String]) -> OAuthHttpResponse raise OAuthError = _
    }

    HTTP POST seam. DefaultOAuthHttpTransport provides a native+js impl that calls moonbitlang/async/http; wasm/wasm-gc has no default (hosts supply their own transport or OAuth HTTP flows are unavailable on that target).

    OAuthProvider

    pub(open) trait OAuthProvider {
    fn provider_id(Self) -> String
    async fn login(Self, interaction : &AuthInteraction) -> Credential raise OAuthError
    async fn refresh(Self, credential : Credential) -> Credential raise OAuthError
    fn to_auth(Self, credential : Credential) -> AuthHeader
    }

    A provider that can obtain and refresh OAuth credentials.

    Lifecycle:
    1. login(interaction) — run the OAuth flow (device or PKCE), returning a fresh Credential.
    2. refresh(credential) — exchange a refresh_token for new tokens.
    3. to_auth(credential) — convert a Credential to HTTP auth headers.

    ProviderCredentialStore

    pub(open) trait ProviderCredentialStore {
    async fn read(Self, provider_id : String) -> ProviderCredential? raise OAuthError
    async fn write(Self, provider_id : String, credential : ProviderCredential) -> Unit raise OAuthError
    async fn delete(Self, provider_id : String) -> Unit raise OAuthError
    }

    Canonical provider credential storage. Unlike the legacy OAuth-only and API-key-only stores below, this contract has one tagged record per provider and therefore preserves the user's explicit authentication-method choice across process restarts and host recomposition.

    OAuthError

    pub(all) suberror OAuthError {
    AuthPending
    SlowDown
    AccessDenied
    ExpiredToken
    InvalidGrant
    HttpError(String)
    ParseError(String)
    Cancelled
    } derive(Eq,
    Debug
    )

    OAuthError::equal

    fn OAuthError::equal(OAuthError, OAuthError) -> Bool

    OAuthError::not_equal

    fn OAuthError::not_equal(x : OAuthError, y : OAuthError) -> Bool

    ApiKeyCredential

    pub(all) struct ApiKeyCredential {
    secret : String
    metadata : Map[String, String]
    }

    A provider-neutral stored API-key credential. The secret is opaque to the host and is interpreted only by the provider extension that owns it.

    ApiKeyCredential::ApiKeyCredential

    fn ApiKeyCredential::ApiKeyCredential(secret~ : String, metadata? : Map[String, String]) -> ApiKeyCredential

    ApiKeyCredential::from_json

    fn ApiKeyCredential::from_json(json : Json) -> ApiKeyCredential?

    Deserialize a generic API-key entry. Provider validation happens later in its ApiKeyFactory; malformed generic shape is rejected here.

    ApiKeyCredential::to_json

    fn ApiKeyCredential::to_json(self : ApiKeyCredential) -> Json

    Serialize without exposing provider-specific fields to the host.

    AuthHeader

    pub(all) enum AuthHeader {
    Bearer(String)
    Header(Map[String, String])
    } derive(Eq,
    Debug
    )

    How to apply the credential to an HTTP request.

    AuthHeader::equal

    fn AuthHeader::equal(AuthHeader, AuthHeader) -> Bool

    AuthHeader::not_equal

    fn AuthHeader::not_equal(x : AuthHeader, y : AuthHeader) -> Bool

    AuthMessage

    pub(all) enum AuthMessage {
    AuthUrl(String)
    DeviceCode(String, String)
    Progress(String)
    }

    Message types sent to the host during OAuth.

    AuthPromptOption

    pub(all) struct AuthPromptOption {
    id : String
    label : String
    description : String?
    }

    Provider-neutral prompt options used by non-OAuth authentication flows. The host renders these without knowing which provider requested them.

    AuthPromptOption::AuthPromptOption

    fn AuthPromptOption::AuthPromptOption(id~ : String, label~ : String, description? : String?) -> AuthPromptOption

    AuthPromptRequest

    pub(all) enum AuthPromptRequest {
    Secret(message~ : String)
    Select(message~ : String, options~ : Array[AuthPromptOption])
    }

    A provider-neutral interactive authentication request. Secret values must never be echoed by hosts or included in diagnostics.

    Credential

    pub(all) struct Credential {
    access_token : String
    refresh_token : String
    expires_at : Int
    token_type : String
    metadata : Map[String, String]
    }

    A stored OAuth credential. All tokens are opaque strings to ext-oauth; provider-specific fields (like OpenAI's account_id) go in metadata.

    Credential::Credential

    fn Credential::Credential(access_token~ : String, refresh_token~ : String, expires_at~ : Int, token_type? : String, metadata? : Map[String, String]) -> Credential

    Credential::from_json

    fn Credential::from_json(json : Json) -> Credential?

    Deserialize from JSON. Returns None on parse failure.

    Credential::is_expired

    fn Credential::is_expired(self : Credential, now_epoch : Int) -> Bool

    Check if the access token is expired (with a 60-second safety margin).

    Credential::metadata_value

    fn Credential::metadata_value(self : Credential, key : String) -> String?

    Read one provider-owned metadata value without exposing the map shape to a host adapter. Codex uses this accessor for its account id header.

    Credential::to_auth_header

    fn Credential::to_auth_header(self : Credential) -> AuthHeader

    Build a Bearer auth header from a credential.

    Credential::to_json

    fn Credential::to_json(self : Credential) -> Json

    Serialize to JSON for persistence.

    DefaultOAuthHttpTransport

    pub(all) struct DefaultOAuthHttpTransport {
    }

    Marker struct for the default @http-backed transport. Constructed on any target; its OAuthHttpTransport impl is compiled only on native+js.

    DefaultOAuthHttpTransport::DefaultOAuthHttpTransport

    fn DefaultOAuthHttpTransport::DefaultOAuthHttpTransport() -> DefaultOAuthHttpTransport

    DefaultOAuthHttpTransport::get

    async fn DefaultOAuthHttpTransport::get(_self : DefaultOAuthHttpTransport, url : String, headers : Map[String, String]) -> OAuthHttpResponse raise OAuthError

    DefaultOAuthHttpTransport::post

    DeviceAuthResponse

    pub(all) struct DeviceAuthResponse {
    device_code : String
    user_code : String
    verification_uri : String
    verification_uri_complete : String?
    expires_in : Int
    interval : Int
    }

    Response from the device authorization endpoint (RFC 8628 §3.2).

    DeviceFlowConfig

    pub(all) struct DeviceFlowConfig {
    client_id : String
    device_auth_endpoint : String
    token_endpoint : String
    scope : String
    default_headers : Map[String, String]
    }

    Configuration for a device authorization flow.

    DeviceFlowConfig::DeviceFlowConfig

    fn DeviceFlowConfig::DeviceFlowConfig(client_id~ : String, device_auth_endpoint~ : String, token_endpoint~ : String, scope? : String) -> DeviceFlowConfig

    Build a DeviceFlowConfig with a sane empty default_headers. Providers that carry identity headers call .with_default_headers after.

    DeviceFlowConfig::with_default_headers

    fn DeviceFlowConfig::with_default_headers(self : DeviceFlowConfig, headers : Map[String, String]) -> DeviceFlowConfig

    Return a copy of self with headers merged into default_headers. Callers' own request-scoped Content-Type / Accept still win.

    InMemoryApiKeyStore

    pub(all) struct InMemoryApiKeyStore {
    credentials : Map[String, ApiKeyCredential]
    }

    In-memory API-key store for deterministic extension tests.

    InMemoryApiKeyStore::InMemoryApiKeyStore

    fn InMemoryApiKeyStore::InMemoryApiKeyStore() -> InMemoryApiKeyStore

    InMemoryApiKeyStore::delete

    async fn InMemoryApiKeyStore::delete(self : InMemoryApiKeyStore, provider_id : String) -> Unit raise OAuthError

    InMemoryApiKeyStore::read

    async fn InMemoryApiKeyStore::read(self : InMemoryApiKeyStore, provider_id : String) -> ApiKeyCredential? raise OAuthError

    InMemoryApiKeyStore::write

    async fn InMemoryApiKeyStore::write(self : InMemoryApiKeyStore, provider_id : String, credential : ApiKeyCredential) -> Unit raise OAuthError

    InMemoryCredentialStore

    pub(all) struct InMemoryCredentialStore {
    credentials : Map[String, Credential]
    }

    In-memory credential store for tests/dev. Not persistent.

    InMemoryCredentialStore::InMemoryCredentialStore

    fn InMemoryCredentialStore::InMemoryCredentialStore() -> InMemoryCredentialStore

    InMemoryCredentialStore::delete

    async fn InMemoryCredentialStore::delete(self : InMemoryCredentialStore, provider_id : String) -> Unit raise OAuthError

    InMemoryCredentialStore::read

    async fn InMemoryCredentialStore::read(self : InMemoryCredentialStore, provider_id : String) -> Credential? raise OAuthError

    InMemoryCredentialStore::write

    async fn InMemoryCredentialStore::write(self : InMemoryCredentialStore, provider_id : String, credential : Credential) -> Unit raise OAuthError

    InMemoryProviderCredentialStore

    pub(all) struct InMemoryProviderCredentialStore {
    credentials : Map[String, ProviderCredential]
    }

    In-memory canonical store used by router and host conformance tests.

    InMemoryProviderCredentialStore::InMemoryProviderCredentialStore

    fn InMemoryProviderCredentialStore::InMemoryProviderCredentialStore() -> InMemoryProviderCredentialStore

    InMemoryProviderCredentialStore::delete

    async fn InMemoryProviderCredentialStore::delete(self : InMemoryProviderCredentialStore, provider_id : String) -> Unit raise OAuthError

    InMemoryProviderCredentialStore::read

    InMemoryProviderCredentialStore::write

    async fn InMemoryProviderCredentialStore::write(self : InMemoryProviderCredentialStore, provider_id : String, credential : ProviderCredential) -> Unit raise OAuthError

    NativeCallbackServer

    pub(all) struct NativeCallbackServer {
    code : String
    server :
    Server
    ?
    }

    NativeCallbackServer::NativeCallbackServer

    fn NativeCallbackServer::NativeCallbackServer() -> NativeCallbackServer

    NativeCallbackServer::start

    async fn NativeCallbackServer::start(self : NativeCallbackServer, port : Int) -> Unit raise OAuthError

    NativeCallbackServer::stop

    NativeCallbackServer::wait_callback

    async fn NativeCallbackServer::wait_callback(self : NativeCallbackServer) -> (String, String) raise OAuthError

    NoopAuthInteraction

    pub(all) struct NoopAuthInteraction {
    }

    A no-op AuthInteraction for headless/CI use. All notifications are discarded and cancellation is never signalled.

    NoopAuthInteraction::NoopAuthInteraction

    fn NoopAuthInteraction::NoopAuthInteraction() -> NoopAuthInteraction

    NoopAuthInteraction::is_cancelled

    fn NoopAuthInteraction::is_cancelled(_self : NoopAuthInteraction) -> Bool

    NoopAuthInteraction::notify

    fn NoopAuthInteraction::notify(_self : NoopAuthInteraction, _message : AuthMessage) -> Unit

    NoopAuthPromptInteraction

    pub(all) struct NoopAuthPromptInteraction {
    }

    Headless prompt implementation for tests. It fails loudly because an unattended host must not pretend it obtained an API key.

    NoopAuthPromptInteraction::NoopAuthPromptInteraction

    fn NoopAuthPromptInteraction::NoopAuthPromptInteraction() -> NoopAuthPromptInteraction

    NoopAuthPromptInteraction::prompt

    async fn NoopAuthPromptInteraction::prompt(_self : NoopAuthPromptInteraction, _request : AuthPromptRequest) -> String raise OAuthError

    OAuthHttpRequest

    pub(all) struct OAuthHttpRequest {
    url : String
    body : String
    headers : Map[String, String]
    }

    OAuthHttpRequest::OAuthHttpRequest

    fn OAuthHttpRequest::OAuthHttpRequest(url~ : String, body~ : String, headers~ : Map[String, String]) -> OAuthHttpRequest

    OAuthHttpResponse

    pub(all) struct OAuthHttpResponse {
    status : Int
    body : String
    }

    OAuthHttpResponse::OAuthHttpResponse

    fn OAuthHttpResponse::OAuthHttpResponse(status~ : Int, body~ : String) -> OAuthHttpResponse

    PkceFlowConfig

    pub(all) struct PkceFlowConfig {
    client_id : String
    authorize_url : String
    token_url : String
    redirect_uri : String
    callback_port : Int
    scope : String
    extra_authorize_params : Array[(String, String)]
    fallback_port : Int?
    }

    Configuration for the PKCE authorization-code browser flow.

    PkcePair

    pub(all) struct PkcePair {
    code_verifier : String
    code_challenge : String
    code_challenge_method : String
    }

    A PKCE pair: the verifier is kept secret, the challenge is sent in the authorization request.

    ProviderCredential

    pub(all) enum ProviderCredential {
    OAuth(Credential)
    ApiKey(ApiKeyCredential)
    }

    The single active credential record for a provider. A provider may expose both OAuth and API-key login, but a host must persist exactly one selected method under the provider's canonical id. Keeping the method tag beside the opaque payload prevents a host from silently preferring OAuth when both legacy stores happen to contain a value.

    ProviderCredential::from_json

    fn ProviderCredential::from_json(json : Json) -> ProviderCredential?

    Decode the canonical tagged persistence shape. Provider-specific validation remains in the provider extension; this only validates the generic record envelope and opaque credential payload.

    ProviderCredential::method_id

    fn ProviderCredential::method_id(self : ProviderCredential) -> String

    Stable provider-neutral method id used by command payloads and storage diagnostics. This never includes credential material.

    ProviderCredential::to_json

    Stable tagged persistence representation. The tag and payload live in one record so a host cannot accidentally restore OAuth and API-key credentials as two simultaneously active methods.

    ResourceMetadata

    pub(all) struct ResourceMetadata {
    authorization_servers : Array[String]
    }

    ResourceMetadata::ResourceMetadata

    fn ResourceMetadata::ResourceMetadata(authorization_servers~ : Array[String]) -> ResourceMetadata

    ServerMetadata

    pub(all) struct ServerMetadata {
    authorization_endpoint : String
    token_endpoint : String
    device_authorization_endpoint : String?
    registration_endpoint : String?
    scopes_supported : Array[String]
    }

    ServerMetadata::ServerMetadata

    fn ServerMetadata::ServerMetadata(authorization_endpoint~ : String, token_endpoint~ : String, device_authorization_endpoint? : String, registration_endpoint? : String, scopes_supported? : Array[String]) -> ServerMetadata

    create_callback_server

    fn create_callback_server() -> NativeCallbackServer

    Factory: create a native CallbackServer.

    fetch_resource_metadata

    async fn fetch_resource_metadata(uri : String, transport : &OAuthHttpTransport) -> ResourceMetadata raise OAuthError

    fetch_server_metadata

    async fn fetch_server_metadata(metadata_url : String, transport : &OAuthHttpTransport) -> ServerMetadata raise OAuthError

    generate_pkce

    fn generate_pkce() -> PkcePair

    Generate a PKCE pair. The verifier is base64url of 48 random bytes (64 chars, RFC 7636 §4.1 allows 43-128 unreserved characters).

    oauth_form_url_encode

    fn oauth_form_url_encode(value : String) -> String

    Percent-encode one form value using the shared OAuth URL encoder. Providers use this for client ids, device codes, and refresh tokens so secrets cannot change the request shape when they contain reserved characters.

    register_client

    async fn register_client(registration_endpoint : String, client_name : String, transport : &OAuthHttpTransport) -> String raise OAuthError

    run_device_flow

    async fn run_device_flow(config : DeviceFlowConfig, interaction : &AuthInteraction) -> Credential raise OAuthError

    Run the complete device flow with the default async HTTP transport.

    Native/js only: this convenience constructs a DefaultOAuthHttpTransport, whose OAuthHttpTransport impl is compiled only on those backends. On wasm/wasm-gc, use run_device_flow_with_transport with a host-supplied transport.

    run_device_flow_with_transport

    async fn run_device_flow_with_transport(config : DeviceFlowConfig, interaction : &AuthInteraction, transport : &OAuthHttpTransport) -> Credential raise OAuthError

    Run a device flow with an injected HTTP transport. This is the canonical seam for deterministic tests and hosts that need explicit request tracing.

    run_pkce_browser_flow

    async fn run_pkce_browser_flow(config : PkceFlowConfig, interaction : &AuthInteraction, server : &CallbackServer, transport : &OAuthHttpTransport, parse_credential? : (Json) -> Credential raise OAuthError, prompt? : &AuthPromptInteraction) -> Credential raise OAuthError

    Run the full PKCE browser flow:
    1. Generate PKCE pair + random state
    2. Construct authorize URL with challenge + state (+ extras)
    3. Start callback server (retry on fallback_port when configured)
    4. Notify host to open the URL (AuthInteraction::notify)
    5. Wait for the callback (code, state); with prompt, race a manual paste prompt against the server
    6. Validate state, exchange code + verifier for tokens
    7. Stop the server on success AND on every raise path after start

    The server parameter is a target-specific CallbackServer impl. The transport parameter is an OAuthHttpTransport impl (DefaultOAuthHttpTransport on native/js, host-supplied on wasm). Both are target seams so this shared flow stays free of platform HTTP APIs. parse_credential defaults to the lenient parse_token_response below.

    run_refresh_flow

    async fn run_refresh_flow(token_endpoint~ : String, client_id~ : String, refresh_token~ : String, transport~ : &OAuthHttpTransport) -> Credential raise OAuthError