CompositeTransport
WIPTry several transports in order. Cookie on the web app, Bearer on the API, same origin.
When to use
CompositeTransport lets you run two or more transports off a single
AuthEngine. The typical case: you serve a first-party web app and a
public API from the same hostname.
Web app (HTML, fetch with cookies) -> CookieTransport
API clients (CLI, mobile, integrations) -> BearerTransport
CompositeTransport tries each transport in order. The first one that
extracts a session wins.
Constructor
import {
BearerTransport,
CompositeTransport,
CookieTransport,
} from '@gentleduck/auth/core/transport'
const transport = new CompositeTransport({
primary: new CookieTransport({ secure: true }),
fallback: [
new BearerTransport(),
],
})
export const auth = new AuthEngine({ transport, /* ... */ })
primary- the transport used for issuing sessions (sign-in, refresh).fallback- additional transports that are only consulted onextract(). They never issue.
Order matters: primary is tried first.
Issue semantics
When the runtime issues a session (after a successful sign-in or rotation),
only primary is used:
const intents = await auth.flows.signIn({ providerId: 'password', input })
// transport.primary.issue() runs; fallbacks are not involved
This keeps the wire shape consistent - a sign-in via the web app sets
a cookie; a sign-in via the API returns a bearer token (configure the
API path to use a different auth instance with BearerTransport as
primary if you need both).
Extract semantics
extract() walks the chain:
1. primary.extract(req) -> CookieTransport
if returns a value -> use it
2. fallback[0].extract(req) -> BearerTransport
if returns a value -> use it
3. ...
4. all returned null -> request is unauthenticated
The first transport to return a non-null result wins. Subsequent transports do not run, so order matters when both could legitimately match (e.g. a request with both a cookie and a bearer header - cookie wins, because cookies are the safer first-party path).
Mixing JwtTransport and CookieTransport
A common production posture is JWT for the API, cookie for the web app:
const transport = new CompositeTransport({
primary: new CookieTransport({ secure: true }),
fallback: [
new JwtTransport({
signKey: { kid: 'k1', key: process.env.JWT_HS256! },
verifyKeys: [{ kid: 'k1', key: process.env.JWT_HS256! }],
issuer: 'https://api.example.com',
ttlMs: 15 * 60 * 1000,
}),
],
})
Now your /api/* routes accept JWTs from machine clients and cookies
from the web app - same handler, same auth.resolveSession() call.
Caveat: CSRF only on cookie path
authCsrfGuard() only runs CSRF checks if the resolved session came in via
a cookie. Bearer/JWT routes are exempt (no cookies, no CSRF) - but the
mixed router still needs to call authCsrfGuard() defensively. The guard
reads the transport hint and silently passes through for non-cookie
sessions.