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.
| Credential | opts keys | Context variables |
|---|---|---|
| Access token | token | google.access.token |
| User refresh token | refresh-token, client-id, client-secret | google.oauth.refresh-token, google.oauth.client-id, google.oauth.client-secret |
| Service account | service-account (JSON Str or Map), subject | google.service.account, google.impersonate |
| API key (public data) | api-key | google.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.
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.
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.