Release candidate — 1.0.0-rc.9
StitchAPI
GuidesAuth

Secret resolvers

Resolve secrets lazily at call time with env() and credential.file() instead of hard-coding them.

Every auth strategy takes a secret as a reference, not a value, and resolves it at call time — reach for env() and credential.file() so the declaration carries a capability, not a credential you have to commit.

The resolvers are two namespaces, not four loose functions. env is the environment-variable source and carries its own requiredness modifier (env('NAME') throws when unset, env.optional('NAME') resolves to absent); credential groups the sources that are not the process environment (credential.file, credential.from).

Example

import {  } from 'stitchapi';
import { , ,  } from 'stitchapi/auth';

// env() reads process.env.API_TOKEN when the stitch is called, not now.
const  = ({
    : 'https://api.example.com',
    : '/me',
    : (('API_TOKEN')),
});

// credential.file() resolves from ~/.stitch/secrets.json, falling back to the env var.
const  = ({
    : 'https://api.example.com',
    : '/billing',
    : (.('BILLING_TOKEN')),
});

The token is never read at declaration time, so the secret never enters the stitch's source — only the name of where to find it does.

Options

A secret is a string | (() => string). Every strategy — bearer, apiKey, basic, oauth2 — accepts one and resolves it the moment a request goes out, so the same declaration works across processes and machines without baking a value in.

env(name) returns a thunk that reads process.env[name] at call time and throws missing env var <name> if it is unset — failing loudly at the call rather than silently sending an empty token.

env.optional(name) is the same source without the throw: it resolves to undefined when the variable is unset or empty, and bearer then sends the request unauthenticated rather than failing. See Optional credentials.

credential.file(name) reads ~/.stitch/secrets.json if present and the key is there, otherwise falls back to the environment variable of the same name, and throws if neither has it.

credential.from(source, name) resolves from an injected source instead — an object with a get(name) method (a NestJS ConfigService, a secrets-manager client) or a plain (name) => value function — for apps that supply config without ever touching process.env. Like env(), it throws on a missing or empty value.

~/.stitch/secrets.json is unencrypted plaintext JSON. Restrict its permissions (chmod 600 ~/.stitch/secrets.json), add it to your .gitignore, and never commit it. For production workloads prefer a proper secrets manager or env() backed by your deployment platform's secret injection.

Why call-time resolution matters: the value is fetched only when a call is made, so you never commit a secret and the stitch holds a capability rather than the credential itself — the capability boundary the product is built on. You can pass a plain string (discouraged — that hard-codes the value), or any () => string thunk of your own, so plugging in a vault or secrets manager is just supplying a different resolver.

Anti-pattern: don't pass a secret as a plain string — it is read at declaration time and lives in your source and history, collapsing the capability back into a committed credential; pass env() or credential.file() instead so the value resolves at call time and the stitch carries a capability, not a credential. See Capability, not credential.

See also

On this page