Google credential resolution, scopes, and the per-run token cache. Usually used indirectly through ::google::api/request.

Credential sources. Per-call opts win over context. When opts carries a primary credential (token, refresh-token, service-account or api-key), context is ignored for that call; otherwise context is used, with opts.subject and opts.scopes still applied on top. Within one source the order is: access token, refresh token, service account, API key.

Per-call safety. A credential key present in opts but blank or null is an Err naming the key, never a fallback to context: a tenant whose credential is missing must not run on the project's. Per-call credentials and every token Google returns are masked in call records (::hot::ctx/mark-secret), and nothing from opts is written to context.

Credentialopts keysContext variables
Access tokentokengoogle.access.token
User refresh tokenrefresh-token, client-id, client-secretgoogle.oauth.refresh-token, google.oauth.client-id, google.oauth.client-secret
Service accountservice-account (JSON Str or Map), subjectgoogle.service.account, google.impersonate
API key (public data)api-keygoogle.api.key

A service-account key may also be an authorized_user JSON file (what gcloud auth application-default login writes); it is treated as a refresh token.

Scopes. Service-account tokens are minted for the scopes the call asks for: opts.scopes, else the google.scopes context variable, else the scope the service function passes. Scopes are a Vec or a space/comma-separated Str; short names such as "drive.readonly" are expanded to https://www.googleapis.com/auth/drive.readonly. Access and refresh tokens carry whatever scopes the user granted at consent time, so the requested scope is not sent for them.

Token cache. Minted tokens are cached in the run's execution context (::hot::ctx, stored as secrets) under a key derived from the credential identity, the sorted scope set and the subject, until expires_in minus 60 seconds. The cache lives for one run: a run that makes 200 Drive calls mints one token, and the next run mints again. Explicit access tokens are never cached.

Values

CACHE_PREFIX

CACHE_PREFIX: Str "google.token-cache."

CREDENTIAL_KEYS

CREDENTIAL_KEYS: Vec [
  "token",
  "api-key",
  "refresh-token",
  "client-id",
  "client-secret",
  "service-account",
  "subject",
  "scopes",
  "retry"
]

EXPIRY_MARGIN_SECONDS

EXPIRY_MARGIN_SECONDS: Int 60

IDENTITY_KEYS

IDENTITY_KEYS: Vec [
  "token",
  "api-key",
  "refresh-token",
  "client-id",
  "client-secret",
  "service-account",
  "subject"
]

SECRET_KEYS

SECRET_KEYS: Vec [
  "token",
  "api-key",
  "refresh-token",
  "client-secret"
]

SERVICE_ACCOUNT_SECRET_FIELDS

SERVICE_ACCOUNT_SECRET_FIELDS: Vec [
  "private_key",
  "client_secret",
  "refresh_token"
]

TOKEN_RESPONSE_SECRET_FIELDS

TOKEN_RESPONSE_SECRET_FIELDS: Vec [
  "access_token",
  "refresh_token",
  "id_token"
]

Functions

access-token

fn (scope: Str | Vec): Str
fn (scope: Str | Vec, opts: Map): Str

Resolve an access token for scope (Str or Vec) from opts and context. Returns the bearer token string, masked in call records, or a TokenError when no credentials are configured or the exchange fails. Service-account and refresh-token tokens are cached for the run.

Example

token ::google::auth/access-token("drive.readonly")

// Per call: act as one tenant's user with their stored refresh token
token2 ::google::auth/access-token("drive.readonly", {
  refresh-token: "1//0g-tenant-refresh-token",
  client-id: "123.apps.googleusercontent.com",
  client-secret: "GOCSPX-secret"
})

bearer

fn (creds: Map, scopes: Vec): Map

Resolve a bearer token for resolved credentials and scopes, using the per-run cache. Returns {token, cache-key, cached} (cached is true when the token came from the cache, so a 401 should evict and retry), or a TokenError. API-key credentials have no bearer token.

blank-credential

fn (opts: Map): Str?

The first credential key opts has with a blank or null value, else null (pure). Such a key is an error, never a fallback to context.

build-service-account-claims

fn (client-email: Str, scope: Str, subject: Any): Map

Build the RFC 7523 assertion claims for a service-account token request. scope is space-separated; subject (optional) enables domain-wide delegation, the user to impersonate.

cache-entry

fn (tokens: Map, now: Int): Map

The cached form of a token response: {access-token, expires-at} (epoch ms), expiring EXPIRY_MARGIN_SECONDS early.

credentials-from

fn (source: Map): Map

Turn a credential source Map (the opts shape) into resolved credentials: {kind: "token" | "refresh" | "service-account" | "api-key", ...}. Returns a TokenError when nothing usable is set or a refresh token lacks its client-id. Pure; no network.

ctx-source

fn (): Map

The credential source read from context, in the same shape as opts.

Context Vars: google.access.token, google.oauth.client-id, google.oauth.client-secret, google.oauth.refresh-token, google.service.account, google.impersonate, google.api.key

effective-scopes

fn (default-scopes: Any, opts: Map): Vec

The scopes a call requests: opts.scopes when set, else the google.scopes context variable, else default-scopes (what the service function asked for). Returns a normalised Vec.

Set google.scopes when a service account's domain-wide delegation grant lists different scopes than the package defaults (for example only drive.readonly); it then applies to every call in the run, so include every scope those calls need.

Context Vars: google.scopes

evict-token

fn (cache-key: Str?): Bool

Drop a cached token (e.g. after a 401). No-op for a null key.

expand-scope

fn (scope: Str): Str

Expand a short scope name to its full URL: "drive.file" becomes "https://www.googleapis.com/auth/drive.file". Full URLs and the OpenID scopes (openid, email, profile) pass through.

has-primary-credential

fn (source: Map): Bool

True when source carries a primary credential: token, refresh-token, service-account or api-key.

is-blank-value

fn (value: Any): Bool

True for null, "", and whitespace-only strings (context placeholders wire "" for unset env vars).

is-fresh

fn (entry: Any, now: Int): Bool

True when a cache entry exists and has not reached its (margin-adjusted) expiry.

mark-credentials

fn (opts: Map): Map

Mask every credential in opts in this run's call records, and return opts unchanged. A service-account key that is a JSON Str is masked whole, and its secret fields separately once parsed.

mark-fields

fn (source: Map, names: Vec): Map

Mask the non-blank values of names in source in this run's call records, and return source unchanged.

mark-token-response

fn (tokens: Map): Map

Mask a token response's access_token, refresh_token and id_token in call records, and return it unchanged. Call it before a token leaves the function that obtained it.

merge-sources

fn (opts: Map, ctx: Map): Map

Pick the credential source for a call: opts alone when it carries a primary credential, otherwise ctx with opts.subject layered on top. Pure; resolve-credentials passes the live context.

mint-token

fn (creds: Map, scopes: Vec): Map

Mint a fresh token response for resolved credentials (no cache).

normalize-scopes

fn (scopes: Any): Vec

Normalise a scope value to a de-duplicated Vec of full scope URLs. Accepts null (empty), a Vec, or a Str separated by spaces or commas.

::google::auth/normalize-scopes("drive.readonly, gmail.readonly")
// ["https://www.googleapis.com/auth/drive.readonly",
//  "https://www.googleapis.com/auth/gmail.readonly"]

now-ms

fn (): Int

parse-service-account

fn (key: Any): Map

Parse a service-account key (JSON Str or Map) and check it has client_email and private_key. An authorized_user key (from gcloud auth application-default login) is returned as-is with type: "authorized_user". Returns the Map or a TokenError. Its secret fields are masked in call records from here on, whichever source the key came from.

refresh-credentials

fn (client-id: Any, client-secret: Any, refresh-token: Str): Map

refresh-token-grant

fn (client-id: Str, client-secret: Str?, refresh-token: Str): Map

Exchange a refresh token for an access token. Returns the token response or a TokenError such as "google: token exchange failed: invalid_grant (Token has been expired or revoked.)". The refresh token, client secret and response tokens are masked in call records.

resolve-credentials

fn (): Map
fn (opts: Map): Map

Resolve the credentials for a call from opts and context (see the namespace doc for precedence). A credential key in opts that is blank or null is a TokenError naming the key. Credentials from opts are masked in call records before any request is made.

Example

creds ::google::auth/resolve-credentials({token: "ya29.tenant-access-token"})
creds.kind   // "token"

service-account-token

fn (service-account: Map, scope: Str, subject: Any): Map

Exchange a service-account key (parsed Map) for an access token with the given space-separated scope. Returns the token response (access_token, expires_in), masked in call records, or a TokenError.

token-cache-key

fn (creds: Map, scopes: Vec): Str?

The context key a minted token is cached under, or null for credentials that are never cached (access tokens, API keys). Keyed by credential identity, sorted scope set and subject. Refresh tokens ignore scopes (the refresh grant always returns every granted scope).

token-error

fn (message: Str): TokenError
fn (prefix: Str, payload: Any): TokenError

Build a TokenError. The 1-arity form wraps a plain message; the 2-arity form prefixes the payload of a failed ::oauth2 call ({status, body} or a transport error).

Types

TokenError

TokenError type {
    message: Str,
    status: Int?,
    error-code: Str?,
    error-description: Str?,
    body: Any?
}

A credential or token-endpoint failure. message is readable, e.g. "google: token exchange failed: invalid_grant (Token has been expired or revoked.)"; error-code and error-description carry Google's OAuth error / error_description when the token endpoint answered.