Release candidate — 1.0.0-rc.6
StitchAPI
GuidesAuth

cookieSession

Log in once, capture and replay cookies, and refresh the session on a status code or a soft 200 wall.

Use cookieSession when an API authenticates with a login form and a cookie instead of a token, and you want the session captured, replayed, and refreshed for you — the stitch holds the login, the caller never touches a cookie.

Example

import { , ,  } from 'stitchapi';

// The login is itself a stitch — its Set-Cookie response seeds the session.
const  = ({
    : 'POST',
    : 'https://api.example.com',
    : '/login',
    : 'form',
});

const  = ({
    : 'https://api.example.com',
    : '/me',
    : ({
        ,
        : '*',
        : () => ({
            : { : ('USER')(), : ('PASS')() },
        }),
        : [401],
    }),
});

The first call to me() runs login once, captures its cookies, and replays them; a 401 afterward re-runs the login and retries, all behind the capability boundary.

Options

Point login at the stitch that authenticates, and pass loginInput to supply its credentials, resolved at call time with env().

cookie selects what to keep: a single cookie name, or '*' (equivalently jar: true) to capture and replay the whole Set-Cookie jar.

refresh is one envelope (CONTRACT.md P24). A bare StatusMatch — a number, a list, or a predicate — is shorthand for refresh: { on }: the status(es) that re-run the login (default [401], and refresh: 401refresh: { on: [401] }). Reach refresh.when — a content predicate for soft 200 walls where a login page is served with a 200 — through the envelope form: refresh: { when: (res) => … }. Give two stitches the same key plus a shared store to share one session. See Reference → Auth strategies for every field.

Durable state and an external recovery loop

cookieSession does the mechanical capture-and-replay, refreshing on the wall — but it can't model a host that owns durable, categorised auth state and its own recovery loop (mark a session backoff after a rate-limit, failed after bad creds, and retry on its own schedule). Two optional host-owned hooks hand that decision back to you. They fire once per actual login attempt — inside the single-flight refresh, never once per coalesced caller — and a throwing hook never crashes the call.

  • onRefresh({ ok, status }) runs after every (re)login attempt with its outcome (ok = a cookie was captured), so you can persist session state and clear or extend a cooldown.
  • onAuthFailure(info) runs when an attempt captured no cookie, with a category you map to your own status: 'unauthenticated' (a refresh status, e.g. 401 — bad/expired creds), 'rate-limited' (429, with retryAfter parsed from Retry-After), 'network' (the login threw before any response — with the thrown error), or 'unknown'. info.phase is 'apply' for a cold session or 'refresh' for a wall that was hit mid-stream.
import { , ,  } from 'stitchapi';
import type { AuthFailureResult, RefreshResult } from 'stitchapi';

// Your own durable store — a row in a database, a Redis hash, anything.
declare const : {
    (): <void>;
    (: string): <void>;
    (: number): <void>;
};

const  = ({
    : 'POST',
    : 'https://api.example.com',
    : '/login',
    : 'form',
});

const  = ({
    : 'https://api.example.com',
    : '/me',
    : ({
        ,
        : '*',
        : () => ({
            : { : ('USER')(), : ('PASS')() },
        }),
        // Persist the outcome of every attempt.
        : async ({  }: RefreshResult) => {
            if () await .();
        },
        // Categorise a failure and drive your own recovery loop.
        : async (: AuthFailureResult) => {
            switch (.) {
                case 'rate-limited':
                    // Back off until the server says we may retry.
                    await .(
                        .() + (. ?? 60_000),
                    );
                    break;
                case 'unauthenticated':
                    // Bad/expired creds — stop hammering; surface for re-auth.
                    await .('credentials rejected');
                    break;
                case 'network':
                    // Transport failure — your loop decides whether to retry.
                    await .(.() + 30_000);
                    break;
                default:
                    await .('unknown auth failure');
            }
        },
    }),
});

The hooks are purely additive: omit them and cookieSession behaves exactly as before. They observe and record — they never change whether the stitch logs in or retries (that stays governed by refresh). Your external loop reads the durable state and decides when to call the stitch again.

Anti-pattern: don't lean on refresh.on status codes alone to catch an expired session — a soft 200 wall, where the login page comes back with status 200, slips straight past it and your stitch reads the login HTML as data; add a refresh.when content predicate so the session re-logs in on that body too. See STITCH_AUTH_WALL.

See also

On this page