import './utils/disposable'; import { MessageBus } from './MessageBus'; import { SubscriptionManager } from './SubscriptionManager'; import type { Channel, FrameKind, FramePayload, Logger, Unsubscribe } from './types'; /** Everything AuthManager needs from its owner, injected to keep it decoupled. */ export interface AuthManagerDeps { bus: MessageBus; subs: SubscriptionManager; /** Shared cross-tab store; the auth token lives under `AUTH_TOKEN_KEY`. */ syncStore: Map; isLeader: () => boolean; /** Current leader socket state, or undefined when this tab holds no socket. */ socketState: () => string | undefined; /** Force a reconnect of the leader socket. */ reconnect: () => void; /** Route an outgoing frame (leader transmits; follower forwards via bus). */ dispatch: (kind: FrameKind, payload: FramePayload) => void; /** Leave a topic subscription (SharedWebSocket.unsubscribe). */ unsubscribeTopic: (topic: string) => void; /** Server event that signals auth was revoked (proto.authRevoked). */ authRevokedEvent: string; /** Token provider for periodic refresh (options.refresh ?? options.auth). */ refresh?: () => string | Promise; /** Refresh interval in ms; disabled when unset or <= 0. */ refreshInterval?: number; log: Logger; } /** * Owns runtime authentication: login/logout, cross-tab auth-state sync, the * leader-only token refresh timer, re-auth on reconnect, server-revocation * handling, and the auth-scoped channel/topic sets that auto-leave on logout. * * Extracted from SharedWebSocket so this cross-cutting concern is one unit. * It still leans on the owner for the things it can't own alone (the socket, * the dispatch pipeline, topic teardown) via the injected deps. */ export declare class AuthManager implements Disposable { private readonly deps; private _isAuthenticated; /** Auth-scoped channels — auto-left on deauth/revocation. */ private readonly authChannels; /** Auth-scoped topics — auto-unsubscribed on deauth/revocation. */ private readonly authTopics; private refreshTimer; /** Wall-clock time of the last token refresh — drives the catch-up check. */ private lastRefreshAt; /** Guards against overlapping refreshes (interval tick vs. catch-up). */ private refreshing; /** True when the refresh loop is not running (not leader / stopped / disposed). */ private refreshStopped; private cleanups; constructor(deps: AuthManagerDeps); get isAuthenticated(): boolean; /** Track an auth-scoped channel so it auto-leaves on deauth/revocation. */ registerAuthChannel(name: string, ch: Channel): void; unregisterAuthChannel(name: string): void; registerAuthTopic(topic: string): void; unregisterAuthTopic(topic: string): void; onAuthChange(fn: (authenticated: boolean) => void): Unsubscribe; /** * Authenticate on the existing connection: sync the token to all tabs and * send the auth-login frame. If the leader socket had failed (e.g. expired * creds), the fresh token restarts it. */ authenticate(token: string): void; /** * Deauthenticate: auto-leave auth channels/topics, send auth-logout, and * sync the cleared state across tabs. The connection stays open for public * events. */ deauthenticate(): void; /** * Apply an auth-state change broadcast over the bus (fired by every tab, * including the originator via broadcast self-delivery): update local state, * drop auth-scoped subscriptions on logout, and notify `onAuthChange`. */ applyRemoteAuthState(authenticated: boolean | undefined): void; /** Re-send the auth-login frame from synced state after a fresh connect. */ reauthenticate(): void; /** * Start the leader-only periodic token refresh. When the timer fires and the * connection is authenticated, the new token flows back through * `authenticate()` so subscribers stay synced and the socket re-issues * auth-login. Idempotent. */ startRefresh(): void; stopRefresh(): void; /** * Catch up a refresh that a backgrounded leader missed. Browsers throttle (or * freeze) timers in hidden tabs, so the periodic refresh can lapse and the * token expire. Call this when the tab becomes visible again: if more than an * interval has elapsed since the last refresh, refresh immediately. No-op on * followers or when refresh isn't configured. */ refreshIfStale(): void; private canRefresh; /** * Self-rescheduling tick (not setInterval): each run lines up the next from * *now*, so a catch-up refresh on re-activation also resets the cadence * instead of racing a still-pending interval. */ private scheduleRefresh; private runRefresh; [Symbol.dispose](): void; }