import { SmrtClassOptions } from '@happyvertical/smrt-core'; import { CreateSessionOptions } from '../collections/SessionCollection.js'; import { Membership } from '../models/Membership.js'; import { Session } from '../models/Session.js'; import { User } from '../models/User.js'; /** * Session context with user and permissions */ export interface SessionContext { /** The User record */ user: User; /** Active membership for the current tenant, if one was resolved */ membership?: Membership | null; /** Resolved permission slugs */ permissions: string[]; /** Tenant ID from session (if any) */ tenantId: string | null; /** Session ID */ sessionId: string; } /** * Result of {@link SessionService.switchTenant}. * * A successful switch into a non-null tenant ROTATES the session id (#1354 * follow-up): a brand-new {@link Session} is minted and the old one is revoked, * so a captured pre-switch id stops validating. Callers MUST persist `sessionId` * (e.g. re-set the session cookie) after a rotation. */ export interface SwitchTenantResult { /** Whether the switch succeeded (true also for a `null` clear). */ switched: boolean; /** * The session id to use going forward: the NEW id after a rotation, the * unchanged id after a `null` clear, or `null` when the switch failed * (unknown session or non-member — fail-closed). */ sessionId: string | null; /** The resulting session (new on rotation; existing on clear; null on failure). */ session: Session | null; /** True only when a fresh session id was minted (non-null tenant switch). */ rotated: boolean; } /** * Options for SessionService */ export interface SessionServiceOptions extends SmrtClassOptions { /** Default session TTL in seconds (default: 7 days) */ defaultTTL?: number; /** Cookie name (default: 'sid') */ cookieName?: string; /** Whether to auto-extend sessions on access (default: false) */ autoExtend?: boolean; } /** * SessionService provides high-level session management that combines * session storage with user and permission loading. * * This is the main service to use for session-based authentication. * * @example * ```typescript * const sessionService = await SessionService.create({ * db: { type: 'sqlite', url: 'app.db' }, * defaultTTL: 7 * 24 * 60 * 60, // 7 days * }); * * // Create session after login * const sessionId = await sessionService.createSession(userId, tenantId); * * // Load session context on each request * const context = await sessionService.loadSessionContext(sessionId); * if (context) { * console.log('User:', context.user.email); * console.log('Permissions:', context.permissions); * } * * // Destroy session on logout * await sessionService.destroySession(sessionId); * ``` */ export declare class SessionService { private options; private sessionCollection; private userCollection; private membershipCollection; private permissionResolver; private defaultTTL; private autoExtend; constructor(options: SessionServiceOptions); /** * Initialize collections */ initialize(): Promise; /** * Create a new session for a user * * @param userId - The user ID * @param tenantId - Optional tenant context * @param options - Additional session options * @returns The session ID */ createSession(userId: string, tenantId?: string, options?: Partial): Promise; /** * Load full session context (user + permissions) * * Returns null if session is invalid or user doesn't exist */ loadSessionContext(sessionId: string): Promise; /** * Get the initialized database connection backing this session service. */ getDatabase(): import('@happyvertical/sql').DatabaseInterface; /** * Refresh session (extend expiry, update lastAccessed) */ refreshSession(sessionId: string): Promise; /** * Destroy a session (revoke it) */ destroySession(sessionId: string): Promise; /** * Destroy all sessions for a user (logout from all devices) */ destroyAllUserSessions(userId: string): Promise; /** * Switch tenant context for a session. * * A session's `tenantId` is the tenant-isolation key for every `@TenantScoped` * query, so it must never be set to a tenant the session's user is not an * active member of — otherwise a caller could read/write another tenant's data * by feeding an arbitrary id here (e.g. straight from untrusted form data). * * Fail-closed (#1400): the user's ACTIVE membership in the target tenant is * verified BEFORE any write. A non-member switch returns * `{ switched: false, ... }` and mutates nothing. * * Session-id ROTATION (#1354 follow-up): a successful switch into a non-null * tenant mints a BRAND-NEW session (fresh secure id, fresh TTL) for the same * user with the new tenant, then REVOKES the old session — so any captured * pre-switch session id immediately stops validating, shrinking the blast * radius of a leaked id across a privilege/tenant boundary. The device context * (user agent, IP, custom data) carries over to the new session. Callers MUST * persist the returned `sessionId` (e.g. re-set the cookie). * * Passing `null` clears the tenant context, is always allowed, and stays * in-place (no rotation — there is no privilege boundary being crossed). * * @returns A {@link SwitchTenantResult}; check `switched` for success. */ switchTenant(sessionId: string, tenantId: string | null): Promise; /** * Get all active sessions for a user (for "manage sessions" UI) */ getUserSessions(userId: string): Promise; /** * Clean up expired sessions (run periodically) */ cleanupExpiredSessions(): Promise; /** * Check if a permission is granted for the session */ hasPermission(sessionId: string, permission: string): Promise; /** * Get session data */ getSessionData(sessionId: string, key: string): Promise; /** * Set session data */ setSessionData(sessionId: string, key: string, value: unknown): Promise; /** * Static factory method */ static create(options: SessionServiceOptions): Promise; } //# sourceMappingURL=SessionService.d.ts.map