WorkloadIdentity unstable
Authenticates an HttpClient by exchanging workload identity tokens for cached
access tokens. For Anthropic, OpenAI or OpenRouter, use the provider package's
AnthropicWorkloadIdentity, OpenAiWorkloadIdentity or
OpenRouterWorkloadIdentity module.
Constructors
Creates an HttpClient transform that authenticates requests with an access
token obtained by exchanging an identity token at a token endpoint.
When to use
Use to add workload identity federation to a provider client whose token
endpoint answers a POST request with a JSON object holding an
access_token and an expires_in number of seconds.
Details
Each exchange reruns identityToken and passes its trimmed value and a JSON-
accepting POST request to exchangeRequest. The response's access_token
is sent as Authorization: Bearer. Its lifetime is expires_in seconds,
falling back to defaultExpiresIn when absent.
Tokens are cached until 30 seconds before expiry or three quarters of their lifetime, whichever is later. Concurrent requests share one exchange; interrupting a waiter does not cancel it.
Exchanges use the HttpClient captured by make, not the transformed client.
Failures are HttpClientErrors: StatusCodeError for rejected exchanges,
TransportError for transport failures or redirects, DecodeError for
invalid responses, and EncodeError for failing or empty identity tokens.
Identity-token source failures are preserved as causes and must not contain
secrets.
Exchange errors omit the request body and retain only transport error messages. The literal identity token is redacted from those messages and rejection headers and bodies, including JSON-escaped occurrences.
Gotchas
- No background refresh: the next request after expiry waits for an exchange.
- A 401 invalidates only the token used by that request, without replay.
- The exchange is never retried.
- Every request carries the access token regardless of URL.
- The exchange client must not follow redirects or log request bodies.
FetchHttpClientredirects are disabled; redirect responses fail the exchange. tokenUrlmust usehttps, orhttponlocalhost,127.0.0.1or::1. Any other URL is a defect.exchangeRequestmay change only the body and headers; other changes are defects and send nothing. Put query parameters intokenUrl.defaultExpiresInmust be positive and finite ormakedefects. Numeric durations are milliseconds; responseexpires_inis seconds.- Only the literal identity token is redacted, after JSON decoding. A token endpoint that echoes it in another encoding is not covered.
- The response is limited to 1 MiB, and the access token to printable ASCII.
Signature
declare function make<E = never, R = never>(options: { readonly defaultExpiresIn?: Input; readonly exchangeRequest: (request: HttpClientRequest, identityToken: string) => HttpClientRequest; readonly identityToken: Effect<Redacted<string>, E, R>; readonly tokenUrl: string;}): Effect<(client: HttpClient) => HttpClient, never, HttpClient | R>Example Exchanging a token at an RFC 8693 style endpoint
import { Effect, FileSystem, Redacted } from "effect"import { WorkloadIdentity } from "effect/ai"import { HttpClientRequest } from "effect/http"
// Requires an `HttpClient` for the exchange, and a `FileSystem`.const transformClient = WorkloadIdentity.make({ tokenUrl: "https://auth.example.com/oauth/token", identityToken: Effect.gen(function*() { const fs = yield* FileSystem.FileSystem return Redacted.make(yield* fs.readFileString("/var/run/secrets/example.com/token")) }), exchangeRequest: (request, identityToken) => request.pipe(HttpClientRequest.bodyJsonUnsafe({ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", subject_token: identityToken, subject_token_type: "urn:ietf:params:oauth:token-type:jwt" })), defaultExpiresIn: "1 hour"})