/** * @typedef {object} SessionRecord * @property {string} sid * @property {string | null} uid * @property {object} claims * @property {number} issuedAt * @property {number} expiresAt Absolute TTL — after this the session is dead. * @property {number} lastSeenAt Refreshed on `touch`; drives the idle-TTL check. * @property {number} [freshAt] Last fresh-auth timestamp (sudo mode). * @property {string} [deviceLabel] * @property {string} [ip] * @property {string} [ua] * @property {string} [impersonatedBy] Admin user ID if this is an impersonation. * @property {boolean} isAnonymous * @property {boolean} revoked `true` once `revoke` has been called. * @property {string} [revokedReason] */ /** * @typedef {object} SessionStore * @property {(sid: string) => Promise} get * @property {(record: SessionRecord) => Promise} put * Insert-or-replace. Also updates the reverse `user → set(sid)` index. * @property {(sid: string, patch: Partial) => Promise} update * Merge-patch a session. Returns the updated record, or `null` if the * sid isn't present. * @property {(sid: string, reason?: string) => Promise} revoke * Mark a single session as revoked. Returns `true` if it was already * in the store (whether previously revoked or not), `false` if not * present. * @property {(uid: string, reason?: string) => Promise} revokeAllForUser * Revoke every session belonging to `uid`. Returns the count revoked. * @property {(uid: string, keepSid: string, reason?: string) => Promise} revokeAllExcept * Revoke every session for `uid` other than `keepSid`. Returns the * count revoked. * @property {(uid: string) => Promise} listByUser * Return every non-revoked, non-expired session for `uid`, newest first. * @property {(uid: string) => Promise} countActive * Number of non-revoked, non-expired sessions for `uid`. Cheaper than * materialising the array when you only need the count. * @property {() => void} [_stop] * Optional cleanup — memory store starts a sweep timer that * `_stop()` cancels. Called from tests; production doesn't need it. */ /** * @typedef {object} MemoryStoreOptions * @property {number} [maxSessions=100000] * Absolute cap on session count. Protects against a leaky app slowly * filling process memory. When hit, expired/revoked entries are swept * first; if still full, the least-recently-seen ANONYMOUS session is * evicted before any authenticated one — so an anonymous-session * flood can't force-logout real users. With `anonymous: true`, still * pair this store with an IP rate limit on session creation. * @property {number} [sweepMs=60000] * How often the store scans for expired entries and drops them. * Deferred cleanup is fine — expired records never verify, and * `listByUser` filters them out — but a periodic sweep keeps the * backing map from ballooning. */ /** * In-process session store. Single-worker deployments and integration * tests only. For anything that runs across more than one Node process, * use `@exortek/session/stores/redis`. * * @param {MemoryStoreOptions} [options] * @returns {SessionStore} */ export function memoryStore(options?: MemoryStoreOptions): SessionStore; export type SessionRecord = { sid: string; uid: string | null; claims: object; issuedAt: number; /** * Absolute TTL — after this the session is dead. */ expiresAt: number; /** * Refreshed on `touch`; drives the idle-TTL check. */ lastSeenAt: number; /** * Last fresh-auth timestamp (sudo mode). */ freshAt?: number | undefined; deviceLabel?: string | undefined; ip?: string | undefined; ua?: string | undefined; /** * Admin user ID if this is an impersonation. */ impersonatedBy?: string | undefined; isAnonymous: boolean; /** * `true` once `revoke` has been called. */ revoked: boolean; revokedReason?: string | undefined; }; export type SessionStore = { get: (sid: string) => Promise; /** * Insert-or-replace. Also updates the reverse `user → set(sid)` index. */ put: (record: SessionRecord) => Promise; /** * Merge-patch a session. Returns the updated record, or `null` if the * sid isn't present. */ update: (sid: string, patch: Partial) => Promise; /** * Mark a single session as revoked. Returns `true` if it was already * in the store (whether previously revoked or not), `false` if not * present. */ revoke: (sid: string, reason?: string) => Promise; /** * Revoke every session belonging to `uid`. Returns the count revoked. */ revokeAllForUser: (uid: string, reason?: string) => Promise; /** * Revoke every session for `uid` other than `keepSid`. Returns the * count revoked. */ revokeAllExcept: (uid: string, keepSid: string, reason?: string) => Promise; /** * Return every non-revoked, non-expired session for `uid`, newest first. */ listByUser: (uid: string) => Promise; /** * Number of non-revoked, non-expired sessions for `uid`. Cheaper than * materialising the array when you only need the count. */ countActive: (uid: string) => Promise; /** * Optional cleanup — memory store starts a sweep timer that * `_stop()` cancels. Called from tests; production doesn't need it. */ _stop?: (() => void) | undefined; }; export type MemoryStoreOptions = { /** * Absolute cap on session count. Protects against a leaky app slowly * filling process memory. When hit, expired/revoked entries are swept * first; if still full, the least-recently-seen ANONYMOUS session is * evicted before any authenticated one — so an anonymous-session * flood can't force-logout real users. With `anonymous: true`, still * pair this store with an IP rate limit on session creation. */ maxSessions?: number | undefined; /** * How often the store scans for expired entries and drops them. * Deferred cleanup is fine — expired records never verify, and * `listByUser` filters them out — but a periodic sweep keeps the * backing map from ballooning. */ sweepMs?: number | undefined; };