Skip to content
// EFFECT DAYS 2026

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.

1 exports Since v4.1.3 Source

Constructors

make

unstable
Since v4.1.3

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. FetchHttpClient redirects are disabled; redirect responses fail the exchange.
  • tokenUrl must use https, or http on localhost, 127.0.0.1 or ::1. Any other URL is a defect.
  • exchangeRequest may change only the body and headers; other changes are defects and send nothing. Put query parameters in tokenUrl.
  • defaultExpiresIn must be positive and finite or make defects. Numeric durations are milliseconds; response expires_in is 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"
})