Credential resolution, the per-run access-token cache, and the opt-in store-backed token keeper. Usually used indirectly through ::xero::api.

Sources. Per-call opts override context, which is how one run acts for many users:

Credentialopts keysContext variables
Access tokenaccess-tokenxero.access.token
Token keepertoken-store (+ token-store-key)xero.token.store, xero.token.store-key
Refresh tokenrefresh-tokenxero.refresh.token
Custom Connectioncustom-connection: true (+ scopes)xero.custom-connection, xero.scopes
Appclient-id, client-secretxero.client.id, xero.client.secret

When opts carries a primary credential (access-token, token-store, refresh-token or custom-connection), context credentials are ignored for the call; the app's client id and secret still fall back to context. Order within a source: access token, keeper, refresh token, Custom Connection. A credential key in opts that is blank or null is an Err naming the key, never a fallback to context. Per-call credentials are marked, so they are masked in call records.

Run cache. Refreshed and client-credentials tokens are cached in the run's context (as secrets) until a minute before expiry, with the newest refresh token, so later refreshes in the same run use the rotated value. Xero rotates the refresh token on every refresh, so without the keeper call current-refresh-token(opts) and persist it before the run ends; the old value only works for 30 more minutes.

Token keeper (opt-in). Set xero.token.store (or opts.token-store) to a ::hot::store Map name. Entries are keyed by token-store-key (default "default"). Key by the user authorisation, not the tenant: one refresh token covers every organisation the user connected, so two tenant entries holding copies of it would invalidate each other on rotation. The keeper reuses a fresh access token, otherwise refreshes and writes the new tokens back with ::hot::store/put-if.

Race note. Two runs that find the same stale entry both refresh with the same refresh token. Within Xero's 30-minute grace period both succeed, but each gets a different new refresh token; put-if keeps exactly one (the loser re-reads and uses the winner's tokens). The loser's refresh token is discarded, which is safe because the stored one is the newest. For heavy concurrency, refresh from a single scheduled function. Store values hold tokens in the platform store; restrict who can read it.

Context Vars

Optional
  • xero.access.token
  • xero.refresh.token
  • xero.client.id
  • xero.client.secret
  • xero.custom-connection
  • xero.scopes
  • xero.token.store
  • xero.token.store-key

Values

CACHE_PREFIX

CACHE_PREFIX: Str "xero.token-cache."

CREDENTIAL_KEYS

CREDENTIAL_KEYS: Vec [
  "access-token",
  "refresh-token",
  "client-id",
  "client-secret",
  "custom-connection",
  "scopes",
  "token-store",
  "token-store-key",
  "tenant-id",
  "if-modified-since",
  "idempotency-key",
  "retry",
  "idempotent"
]

DEFAULT_STORE_KEY

DEFAULT_STORE_KEY: Str "default"

EXPIRY_MARGIN_SECONDS

EXPIRY_MARGIN_SECONDS: Int 60

NO_FALLBACK_KEYS

NO_FALLBACK_KEYS: Vec [
  "access-token",
  "refresh-token",
  "token-store",
  "token-store-key",
  "client-id",
  "client-secret"
]

Functions

access-token

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

An access token for a call from opts and context (refreshing and caching as needed), or a TokenError.

app-opts

fn (creds: Map): Map

bearer

fn (creds: Map): Map

Resolve a bearer token for resolved credentials with the run cache: {token, cache-key, cached} or a TokenError.

cache-key

fn (creds: Map): Str?

The run-context key a token is cached under (hashed), or null for explicit access tokens.

credentials-from

fn (source: Map): Map

Turn a source Map into resolved credentials: {kind: "token"}, {kind: "keeper", store, key, seed-refresh-token, ...}, {kind: "refresh", refresh-token, ...} or {kind: "client-credentials", scopes, ...}, or a TokenError. Pure.

ctx-source

fn (): Map

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

current-refresh-token

fn (): Str?
fn (opts: Map): Str?

The newest refresh token known to this run for the credentials in opts / context: the rotated value after a refresh in this run, else the configured one (null for access tokens and Custom Connections). Persist it before the run ends when you are not using the keeper.

evict

fn (creds: Map, cache-key: Str?): Bool

Expire a cached access token (after a 401), keeping the newest refresh token; also expires a keeper entry's access token.

has-primary-credential

fn (source: Map): Bool

True when source carries access-token, token-store, refresh-token or custom-connection: true.

is-fresh

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

True when an entry has an access token before its margin-adjusted expiry.

is-true

fn (value: Any): Bool

keeper-entry

fn (store-name: Str, key: Str): Map?

Read a keeper entry (null when absent). It holds tokens: never log it.

keeper-mint

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

A fresh entry for keeper credentials: the stored one when fresh, else refresh and write back with put-if (re-reading on a lost race).

keeper-seed

fn (store-name: Str, key: Str, tokens: Str | Map): Map

Seed (or replace) a keeper entry, e.g. right after ::xero::oauth/exchange-code. Pass the TokenSet to store its access token too, or just a refresh token Str. Returns the entry.

tokens ::xero::oauth/exchange-code(cb.code, redirect-uri)
::xero::auth/keeper-seed("xero-tokens", user-id, tokens)

mark-entry

fn (entry: Any): Any

Mask the tokens in a keeper entry and return it unchanged. Null passes through. Exposed for testing.

merge-sources

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

Pick the source for a call: opts when it has a primary credential (client id and secret still fall back to ctx), else ctx. Pure.

mint-entry

fn (creds: Map, cached: Any, now: Int): Map

newest-refresh-token

fn (entry: Any, fallback: Str?): Str?

The refresh token to use: the entry's (newest) one, else the fallback.

refresh-into-entry

fn (creds: Map, refresh-token: Str, now: Int): Map

resolve-credentials

fn (opts: Map): Map

Resolve the credentials for a call from opts and context, marking per-call credentials so they are masked in call records. A credential key in opts that is blank or null is a TokenError naming the key.

store-map

fn (name: Str)

token-entry

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

The cached or stored form of a TokenSet: {access-token, expires-at, refresh-token, updated-at}, expires-at pulled in by EXPIRY_MARGIN_SECONDS.