Auth script contract
A script auth provider runs your JavaScript in the engine’s
sandboxed QuickJS runtime to obtain a token. This page is the exact contract.
Signature
Section titled “Signature”async function authenticate({ env, config, secret, fetch, sender }) { // … return { token, expiresIn };}The function must be named authenticate, must be async, and takes a single object.
What you are given
Section titled “What you are given”| Name | Type | What it is |
|---|---|---|
env | Record<string, string> | The active environment’s variables — e.g. env.baseUrl |
config | Record<string, string> | The provider’s non-secret config map |
secret | (name) => string | Read a secret by keychain name. Throws if missing |
fetch | async (url, opts?) => Response | HTTP fetch — method, headers, body in; status, ok, headers, text(), json() out |
sender | { id, label, tenant, secretRef?, vars } | null | The selected sender to authenticate as, or null if the provider has none |
What you must return
Section titled “What you must return”| Field | Type | Required | Notes |
|---|---|---|---|
token | string | yes | The credential, injected per the provider’s inject block |
expiresIn | number | no | Lifetime in seconds |
Return expiresIn whenever the server tells you one. It is how the cache knows when to
refetch; without it, Tinspec cannot know the token went stale and you will see avoidable 401s.
A complete example
Section titled “A complete example”async function authenticate({ env, config, secret, fetch, sender }) { const res = await fetch(`${env.baseUrl}/oauth/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "password", client_id: config.clientId, client_secret: secret("users-api.CLIENT_SECRET"), username: sender.id, password: secret(sender.secretRef), }).toString(), });
if (!res.ok) { throw new Error(`login failed: ${res.status} ${await res.text()}`); }
const json = await res.json(); return { token: json.access_token, expiresIn: json.expires_in };}Handling senders
Section titled “Handling senders”sender is null when the provider has no senders. Handle both cases if your provider might
be used either way:
const user = sender?.id ?? config.defaultUser;const password = secret(sender?.secretRef ?? "users-api.DEFAULT_PASSWORD");A sender’s non-secret vars are available as sender.vars, so per-identity values like a
tenant id or a role need no extra plumbing.
Errors
Section titled “Errors”Throw. The message surfaces in the app and on the send that triggered the resolution. Be
specific — login failed: 401 with the body text is far more useful six months later than a
bare failure.
A secret(...) call for a name that does not exist on this machine throws on its own, which
is usually the right behaviour: secrets are per-machine by design and never sync, so a
teammate who pulls your project must set their own.
The sandbox
Section titled “The sandbox”The runtime is QuickJS, embedded in the engine. It gives you standard JavaScript plus the five arguments above.
What it does not give you:
- No filesystem, no process, no environment variables beyond
env. - No network except the provided
fetch. - No npm modules, no
import, norequire. - No access to the keychain beyond
secret(name)for names the provider declares.
URLSearchParams, JSON, btoa/atob-style encoding, and the usual language built-ins are
available; assume the standard library and nothing host-specific.
Where the script lives
Section titled “Where the script lives”kind: script providers reference the file by workspace-relative path:
authProviders: - name: legacy-sso kind: script script: .tinspec/auth/legacy-sso.js config: clientId: test-harness secretRefs: - users-api.CLIENT_SECRET senders: - id: admin@example.com secretRef: users-api.admin.PASSWORDIt is committed with the project, so it reviews like any other code — which is the point. An auth flow that lives in a script somebody pasted into a GUI is a flow nobody else can fix.
The same runtime elsewhere
Section titled “The same runtime elsewhere”A chain’s script node uses the same sandbox with a different entry point —
async function run(vars, ctx), where the keys of the returned object become output
variables. See Build a chain.