/** A server-side BFF session. The browser only ever sees {@link BffSession.sessionId} (in an httpOnly * cookie), never the tokens. Timestamps are epoch milliseconds. */ export interface BffSession { /** Opaque, unguessable session id. Equals the value stored in the session cookie. */ sessionId: string; /** The tenant this session belongs to (see IBffTenantResolver). Undefined in single-tenant mode. */ tenantKey?: string; /** OIDC session id (`sid`) from the id_token, used to match session-scoped back-channel logout. */ sid?: string; /** Authenticated subject (`sub`). */ subject: string; /** Raw id_token, retained for `id_token_hint` on logout. */ idToken: string; /** Current access token (for downstream APIs; never sent to the browser). */ accessToken: string; /** Current refresh token, if `offline_access` was granted. */ refreshToken?: string; /** When the access token expires (epoch ms). */ accessTokenExpiresAt: number; /** Absolute session expiry (epoch ms), independent of token refreshes. */ expiresAt: number; /** Non-sensitive id_token claims surfaced to the SPA via `/bff/user`. */ claims: Record; } /** Server-side storage for BFF sessions. Replace to move sessions onto other infrastructure (one of the * seams that let the core run at the edge later). */ export interface IBffSessionStore { get(sessionId: string): Promise; /** Create or replace a session. Implementations must index it by `subject` and (when present) `sid`. */ set(session: BffSession): Promise; remove(sessionId: string): Promise; /** Delete every session matching an OIDC `sid` (session-scoped logout). Returns the count removed. * * `tenantKey` scopes the lookup to one tenant, and implementations MUST honour it. `sub` and `sid` are * unique only within an issuer, so an unscoped removal lets a logout accepted from one tenant's IdP * terminate another tenant's sessions for a colliding value — and `/bff/backchannel-logout` accepts a * valid token from ANY configured tenant, which makes that a cross-tenant denial of service. */ removeBySid(sid: string, tenantKey?: string): Promise; /** Delete every session for a subject (subject-scoped logout — the form Authagonal emits). Returns count. * * `tenantKey` scopes the lookup exactly as in {@link removeBySid}. */ removeBySubject(subject: string, tenantKey?: string): Promise; /** * OPTIONAL cross-replica lock for one session's refresh. Return false if another holder has it. * * Without this, `RefreshCoordinator`'s single-flight is per-PROCESS — a `Map` on one instance — while the * session and its rotating refresh token live in a store shared by every replica. Two replicas can * therefore read the same session, both see it needs refreshing, and both redeem the same refresh token. * That is indistinguishable from a stolen-token replay, and an IdP's response to replay is to revoke the * whole grant family — so the multi-instance deployment the README recommends can sign a user out * everywhere as a matter of routine, under nothing more than concurrent load. * * Any backend works: all this needs is "at most one holder for a short time". Implement it with `SET NX PX` * on Redis, or a conditional write anywhere else. The .NET twin does the same thing through * `ILeaseProvider`. * * With it unimplemented the behaviour is unchanged, and a multi-instance BFF then depends on the IdP's * refresh-reuse grace window (`Auth:RefreshTokenReuseGraceSeconds`, 30 in the protocol layer but **0 — * strict** in the Authagonal.Server host's own default) to absorb the double redemption. */ acquireRefreshLock?(sessionId: string, ttlMs: number): Promise; /** Releases {@link IBffSessionStore.acquireRefreshLock}. Implement both or neither. */ releaseRefreshLock?(sessionId: string): Promise; } /** Single-process in-memory session store. Fine for one instance; use a shared store (e.g. Redis) for more. * Expired sessions are evicted lazily on read. */ export declare class MemorySessionStore implements IBffSessionStore { private readonly sessions; private readonly bySid; private readonly bySub; get(sessionId: string): Promise; set(session: BffSession): Promise; remove(sessionId: string): Promise; removeBySid(sid: string, tenantKey?: string): Promise; removeBySubject(subject: string, tenantKey?: string): Promise; private purge; }