Host-implemented credential resolution, library-implemented attachment.
The host resolves what to attach, per request; the spec's securityScheme dictates how it is attached. Credential values never enter the sandbox or the transcript.
Refresh has two modes, and a host can use either or both:
- Proactive —
resolve/4runs on every request, so a host that knows a token's expiry refreshes insideresolve/4before handing it back. - Reactive —
unauthorized/4(optional) is called when the upstream answers 401 to a credentialresolve/4supplied: for revoked tokens, server-side session resets, and the many APIs whoseexpires_incannot be trusted. Returning{:retry, credential}re-sends the identical request once with the new credential.
Summary
Types
Where the request is going, resolved before credential attachment.
Callbacks
Resolve a credential for one request. context is the opaque identity map
the host passed into the execute handler (tenant, user, org).
Called once when the upstream answers 401 after a credential from resolve/4
was attached. Return {:retry, credential} to have the library re-attach the
new credential and re-send the same request exactly once; return :pass to
hand the 401 back unchanged. Same error contract as resolve/4: a binary
{:error, msg} crosses verbatim; any non-binary reason is logged and
replaced with a fixed string. If this callback raises/exits/returns garbage,
the ORIGINAL 401 response is returned (never converted into a transport error).
Functions
Turn a resolved credential into headers and query params for one request.
Types
@type request_info() :: %{ method: String.t(), base_url: String.t(), host: String.t() | nil, path: String.t() }
Where the request is going, resolved before credential attachment.
path is the OpenAPI path template — path params are not substituted
(e.g. "/pets/{id}", not "/pets/42"). base_url may itself include a
path prefix (e.g. "https://api.example.com/v1"); the full wire path is
base_url's path segment concatenated with the substituted path, not
path alone. method is always lowercase ("get", not "GET").
Callbacks
@callback resolve( api_name :: String.t(), scheme :: map() | nil, request :: request_info(), context :: map() ) :: {:ok, credential()} | {:error, term()}
Resolve a credential for one request. context is the opaque identity map
the host passed into the execute handler (tenant, user, org).
Error contract: a binary {:error, message} crosses back to the
sandbox/model verbatim — hosts must keep secrets out of binary error
messages. Any non-binary {:error, reason} (e.g. {:expired, token}) is
logged in full and replaced with a fixed, redacted string before it
reaches the sandbox.
@callback unauthorized( api_name :: String.t(), scheme :: map() | nil, request :: request_info(), context :: map() ) :: {:retry, credential()} | :pass | {:error, term()}
Called once when the upstream answers 401 after a credential from resolve/4
was attached. Return {:retry, credential} to have the library re-attach the
new credential and re-send the same request exactly once; return :pass to
hand the 401 back unchanged. Same error contract as resolve/4: a binary
{:error, msg} crosses verbatim; any non-binary reason is logged and
replaced with a fixed string. If this callback raises/exits/returns garbage,
the ORIGINAL 401 response is returned (never converted into a transport error).
Functions
@spec attach(map() | nil, credential()) :: {:ok, %{headers: [{String.t(), String.t()}], query: map()}} | {:error, String.t()}
Turn a resolved credential into headers and query params for one request.
Credential values are screened for non-printable bytes first: a secret
with a stray newline (the classic env-var-with-trailing-\n) would
otherwise reach Mint, whose invalid_header_value error embeds the raw
value in its message and leaks it into whatever logs that message. Errors
from here never echo the credential.