Client SDK · Start here

Authentication.

Browser PKCE, device flow, token persistence, and API keys for the client SDK.

The SDK provides a managed auth tier: on successful login, a TokenManager is auto-configured on the client instance. Every subsequent request automatically carries a managed access token with single-flight, skew-aware auto-refresh.

OAuth client IDs

The following OAuth client ID is currently available for public integrations:

Client IDUse caseNotes
matilda-codePublic CLI / SDK / agent integrationsPKCE + device flow. The default choice for most integrations.

matilda-code is the only publicly accessible client ID at the moment. Additional client IDs will be documented here as access opens up.

auth.loginWithBrowser(opts)

Managed PKCE browser login (RFC 8252 loopback). Starts a temporary local server, opens the browser, receives the auth code, exchanges it for tokens, and auto-wires a TokenManager.

TypeScript
const tokens = await client.auth.loginWithBrowser({
  clientId: 'matilda-code',
  openBrowser: (url) => console.log(`Open: ${url}`),
});
console.log(tokens.accessToken);

BrowserLoginOptions

Fieldtypedescription
clientIdstringOAuth client alias (e.g. 'matilda-code').
scopestringSpace-separated OAuth scopes. Defaults to 'openid offline_access'.
identityProviderIdstringRoute straight to a federated IdP (e.g. Google SSO) instead of the hosted login page.
callbackPortnumberFixed loopback port. Recommended for FusionAuth redirect validation. Defaults to a random ephemeral port.
timeoutMsnumberHow long to wait for the browser callback. Defaults to 300_000 ms (5 min).
openBrowser(url: string) => void | Promise<void>Called with the authorize URL. If omitted, caller handles browser opening.
successRedirectstringURL the browser is 302-redirected to on success. Defaults to 'https://matilda.maincode.com/cli/signed-in'.
errorRedirectstringURL for the error case; otherwise a bare 400 text response.
fetchImpltypeof fetchOverride fetch (testing, custom transport). Defaults to global fetch.
tokenStoreStorageAdapterCustom token persistence. Defaults to memoryStorage().
tokenLock<T>(fn: () => Promise<T>) => Promise<T>Cross-process critical-section lock for token refresh (e.g. from createFileTokenStore).
onEvent(e: LoginFlowEvent) => voidSubscribe to login flow state events.

Returns

Promise<TokenSet> — the token set from the login flow. The TokenManager is also auto-configured on the client instance.

auth.loginWithDeviceFlow(opts)

Managed RFC 8628 device flow. Requests a device code, prints the user code and verification URL to stderr (by default), and polls until the user authorises.

TypeScript
const tokens = await client.auth.loginWithDeviceFlow({
  clientId: 'matilda-code',
  onEvent: (e) => {
    if (e.type === 'user_code') {
      console.log(`Visit ${e.verificationUri} and enter code: ${e.userCode}`);
    }
  },
});

If no onEvent handler is provided, the SDK prints the user code and verification URL to stderr automatically.

DeviceLoginOptions

Fieldtypedescription
clientIdstringOAuth client alias.
scopestringSpace-separated OAuth scopes. Defaults to 'openid offline_access'.
timeoutMsnumberPolling timeout. Defaults to 300_000 ms (5 min).
signalAbortSignalAbort the polling loop.
fetchImpltypeof fetchOverride fetch. Defaults to global fetch.
tokenStoreStorageAdapterCustom token persistence. Defaults to memoryStorage().
tokenLock<T>(fn: () => Promise<T>) => Promise<T>Cross-process lock for token refresh.
onEvent(e: LoginFlowEvent) => voidSubscribe to login flow events. If omitted, prints user code to stderr. Defaults to defaultDeviceOnEvent.

Returns

Promise<TokenSet>

auth.refreshToken(refreshToken, clientId)

Manually refresh an access token using a refresh token. This bypasses the TokenManager — use it only when you need raw token exchange.

TypeScript
const tokens = await client.auth.refreshToken(oldRefreshToken, 'matilda-code');
Fieldtypedescription
refreshTokenstringThe refresh token to exchange.
clientIdstringOAuth client alias.

Returns Promise<TokenSet>.

auth.getTokens()

Returns the current token set from the managed TokenManager, or null if not authenticated.

TypeScript
const tokens = await client.auth.getTokens();
if (tokens) {
  console.log(`Token expires at: ${new Date(tokens.expiresAt).toISOString()}`);
}

Returns Promise<TokenSet | null>.

auth.logout()

Clears the token store, destroys the TokenManager, and disconnects the client's getToken provider.

TypeScript
await client.auth.logout();

Returns Promise<void>.

Token persistence

By default, tokens are stored in memory (memoryStorage()). For cross-process persistence (e.g. CLI sessions), use createFileTokenStore from the /auth/node subpath:

TypeScript
import Matilda from '@maincode-ai/matilda-client-sdk';
import { createFileTokenStore } from '@maincode-ai/matilda-client-sdk/auth/node';
import { homedir } from 'node:os';
import { join } from 'node:path';

const tokenPath = join(homedir(), '.matilda', 'tokens.json');
const { store, lock } = createFileTokenStore(tokenPath);

const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' });

await client.auth.loginWithBrowser({
  clientId: 'matilda-code',
  tokenStore: store,
  tokenLock: lock,
});

The file store uses a 0600 JSON file with a lockfile-based single-writer lock to prevent cross-process refresh races.

StorageAdapter interface

TypeScript
interface StorageAdapter {
  get(key: string): string | null | Promise<string | null>;
  set(key: string, value: string): void | Promise<void>;
  remove(key: string): void | Promise<void>;
}

Implement this to store tokens in a database, keychain, or any custom backend.

TokenManager interface

TypeScript
interface TokenManager {
  getAccessToken(opts?: { forceRefresh?: boolean }): Promise<string>;
  getTokens(): Promise<TokenSet | null>;
  setTokens(tokens: TokenSet): Promise<void>;
  clear(): Promise<void>;
}

Created via createTokenManager(deps) from /auth/node. The SDK auto-creates one on login.

TokenSet interface

TypeScript
interface TokenSet {
  accessToken: string;
  refreshToken?: string;
  idToken?: string;       // OIDC id_token when 'openid' scope is granted
  expiresAt: number;      // epoch milliseconds
}

AuthError class

TypeScript
class AuthError extends Error {
  readonly code: string;       // OAuth error code (e.g. 'invalid_grant', 'authorization_pending')
  readonly retryable: boolean; // true for transient 5xx/network; false for revoked tokens
}

LoginFlowEvent type

TypeScript
type LoginFlowEvent =
  | { type: 'state'; status: 'idle' | 'awaiting_user' | 'exchanging' | 'authenticated' | 'error' }
  | { type: 'authorize_url'; url: string }
  | { type: 'user_code'; userCode: string; verificationUri: string; verificationUriComplete?: string };

Standalone auth subpath (/auth)

The /auth subpath provides the raw PKCE and device-flow helpers without the managed TokenManager. These are deprecated in favour of the managed client.auth.* methods, but remain available for integrators who need direct protocol access:

ExportDescription
loginWithBrowser(coreAuthUrl, opts)Raw PKCE browser login. Returns TokenSet.
loginWithDeviceFlow(coreAuthUrl, opts)Raw device flow. Returns TokenSet.
refreshToken(coreAuthUrl, clientId, refreshToken)Raw token refresh. Returns TokenSet.
createPkcePair()Generate PKCE code_verifier + code_challenge (S256).
buildAuthorizeUrl(coreAuthUrl, opts)Construct the authorize URL.

The /auth/node subpath adds the Node-only adapters on top of the isomorphic core:

ExportDescription
createLoginFlow(opts)Headless login controller for loopback + device transports.
createTokenManager(deps)Per-session token manager with single-flight refresh.
createFileTokenStore(filePath)0600 JSON file store with cross-process lock.
createLoopbackReceiver(opts)RFC 8252 loopback redirect receiver.
fetchAuthServerMetadata(issuer, fetchImpl?, opts?)RFC 8414 metadata discovery.
memoryStorage()In-memory StorageAdapter.
beginLogin(authorizationEndpoint, params)Stateless "begin" half of a redirect/BFF login.
completeLogin(tokenEndpoint, params, fetchImpl?)Stateless "complete" half.

API Key Management

The SDK provides convenience methods for managing mc_live_ API keys. These hit the same JwtGuard-protected endpoints the developer dashboard uses — the session JWT from a prior loginWithBrowser() or loginWithDeviceFlow() call is carried automatically by the TokenManager.

auth.createApiKey(opts)

Mints a new API key. The secret is returned only at creation time — store it immediately.

TypeScript
await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' });

const key = await client.auth.createApiKey({
  name: 'ci-runner',
  // scopes: ['api:code'],                    // omit → server default
  // expiresAt: '2026-12-31T23:59:59Z',       // omit → never expires
});

console.log(key.secret);        // mc_live_...  — shown only once
console.log(key.keyPrefix);     // mc_live_abcd
console.log(key.id);            // UUID for revocation

CreateApiKeyOptions

Fieldtypedescription
namestringHuman-readable key name (1–80 chars).
scopesstring[]Permission scopes (e.g. ['api:code']). Defaults to the server default.
expiresAtstringISO 8601 expiry timestamp. Omit for no expiry.

Returns Promise<ApiKeyWithSecret>.

auth.listApiKeys()

Lists all non-revoked API keys for the authenticated user. Secrets are never included — only the keyPrefix for identification.

TypeScript
const keys = await client.auth.listApiKeys();
for (const key of keys) {
  console.log(`${key.keyPrefix}  ${key.name}  ${key.revokedAt ? 'REVOKED' : 'ACTIVE'}`);
}

Returns Promise<ApiKey[]>.

auth.revokeApiKey(id)

Revokes a key by ID. The key immediately stops working for authentication.

TypeScript
await client.auth.revokeApiKey(key.id);
Fieldtypedescription
idstringThe key UUID (from createApiKey or listApiKeys).

Returns Promise<ApiKey> (the revoked key with revokedAt set).

ApiKey interface

TypeScript
interface ApiKey {
  id: string;
  name: string;
  keyPrefix: string;           // e.g. 'mc_live_abcd1234'
  scopes: string[];
  createdAt: string;           // ISO 8601
  lastUsedAt: string | null;
  revokedAt: string | null;
  expiresAt: string | null;
}

ApiKeyWithSecret interface

Extends ApiKey with the one-time secret:

TypeScript
interface ApiKeyWithSecret extends ApiKey {
  secret: string;              // full key, e.g. 'mc_live_...' — shown only at creation
}

CLI — matilda-key

The package ships a matilda-key CLI binary that mints API keys via device-flow login. This is the easiest way for social-login (Google/Apple) users to get an API key without writing code.

Installation

The binary is available via npx (no global install required) or after installing the package:

Shell
npx matilda-key create-api-key --name "my-key"
# or, if installed globally:
matilda-key create-api-key --name "my-key"

Usage

Shell
matilda-key create-api-key --name <key-name> [--scopes ...] [--expires-at ...] [--api-base-url ...] [--client-id ...]
Fieldtypedescription
--namestringAPI key name (1–80 chars).
--scopesstringComma-separated scopes (e.g. api:code,api:chat). Defaults to the server default.
--expires-atstringISO 8601 expiry date. Defaults to never expiring.
--api-base-urlstringAPI base URL. Defaults to https://matilda.maincode.com/api.
--client-idstringOAuth client ID / alias. Defaults to matilda-code.

The MATILDA_API_BASE_URL environment variable is also honoured as a fallback for --api-base-url.

What happens when you run it

  1. Device-flow login — a verification URL and code are printed to stderr. Open the URL, sign in with Google or Apple, enter the code.
  2. Key minting — once authenticated, an API key is created from the session JWT.
  3. Output — key metadata (ID, name, prefix, scopes, expiry) is printed to stderr. The secret is printed to stdout.

The stdout/stderr separation is deliberate: the secret on stdout is clean and pipeable, while the login flow and metadata remain visible on the terminal via stderr.

Piping the secret

Shell
# Capture into an env var (login flow still visible on terminal):
MATILDA_API_KEY=$(matilda-key create-api-key --name "ci-runner")

# Pipe to a file:
matilda-key create-api-key --name "ci-runner" > /tmp/key.txt

# Use in CI:
export MATILDA_API_KEY="$(cat /tmp/key.txt)"

Full example

Shell
$ matilda-key create-api-key --name "ci-runner" --scopes api:code

Starting device-flow login...

  Open https://matilda.maincode.com/device and enter code: ABCD-1234

  [state] awaiting_user_verification
  [state] token_received
Login successful!

Minting API key "ci-runner"...

API key created successfully.
  ID:          8f3a2b1c-...
  Name:        ci-runner
  Prefix:      mc_live_abcd1234
  Scopes:      api:code
  Expires:     never
  Created at:  2026-08-18T10:30:00.000Z

Secret printed to stdout. Store it securely it won't be shown again.

The secret (mc_live_...) is on stdout; everything else is on stderr.