import { type TerminalControlGrantClaims } from '../core/terminal-control-grant.js'; import { type ControlAuditSink } from './control-audit.js'; export declare const DEFAULT_TERMINAL_CONTROL_TTL_MS: number; export interface TerminalDashboardActor { userId: string; authSessionId: string; expiresAt: number; /** * `controlled` is the Workbench lease model. A trusted central-platform * owner retains its historical always-write role; teammate/guest identities * are permanently read-only and cannot promote themselves via takeover. */ terminalCapability?: 'controlled' | 'owner' | 'readonly'; } export interface TerminalControlSocket { readonly destroyed?: boolean; destroy(): void; } export type TerminalControlTakeoverResult = { ok: true; mode: 'controlled'; expiresAt: number; reused: boolean; /** The acquisition this lease is now on; absent for the leaseless * platform-owner role, which has nothing to compare or release. */ acquisition?: string; } | { ok: false; error: 'authentication_expired' | 'control_busy' | 'terminal_operation_forbidden' /** The caller sent an acquisition id this process refuses to bind. Failing * closed matters: silently minting one instead would hand back a lease * whose CAS id the caller does not know, i.e. an uncompensatable lease. */ | 'invalid_acquisition'; }; export type TerminalControlReleaseResult = { ok: true; mode: 'readonly'; released: boolean; } | { ok: false; error: 'control_owned_by_another_session' | 'terminal_operation_forbidden' /** A conditional release named an acquisition that is no longer the current * one. The lease stays exactly as it is — reporting `readonly` here would * tell the caller the opposite of the truth. */ | 'control_lease_superseded'; }; /** Internal central-proxy material. The signed token never leaves the loopback * hop; `acquisition` is the CAS id this grant was minted against, so socket * registration and disconnect can both be scoped to that exact acquisition * rather than to "whatever lease this login happens to hold now". */ export interface TerminalProxyGrant { token: string; scope: 'read' | 'write'; acquisition?: string; } /** Bounds for a client-minted acquisition id. Opaque to this process — it only * ever gets compared for equality and echoed back to its own minter — so the * charset is kept URL-safe and the length bounded, nothing more. */ export declare function isTerminalAcquisitionId(value: unknown): value is string; export interface TerminalControlManagerOptions { secret: string; audit: ControlAuditSink; ttlMs?: number; now?: () => number; setTimer?: typeof setTimeout; clearTimer?: typeof clearTimeout; grantId?: () => string; /** Fallback acquisition id for callers that do not mint one (legacy/tokenless * entry points). Production uses a random nonce. */ acquisitionId?: () => string; } /** * Server-authoritative single-controller lease per terminal session. The * browser sees only mode/expiry. The signed write grant remains in this * process and is injected on the dashboard -> worker loopback hop. */ export declare class TerminalControlManager { private readonly leases; /** P1-5 revocation index: read-only bridged sockets per auth session, so a * logout/expiry can close every read stream that authentication opened. */ private readonly readSocketsByAuthSession; private readonly secret; private readonly audit; private readonly ttlMs; private readonly now; private readonly schedule; private readonly cancel; private readonly nextGrantId?; private readonly nextAcquisitionId; constructor(opts: TerminalControlManagerOptions); /** * Acquire (or re-acquire) the write lease. * * `acquisitionId` is minted by the CALLER before the request goes out; this * process only binds it. See `TerminalControlLease.acquisitionId` for why the * direction matters. Omitting it keeps the historical behavior for entry * points that have no compensation path of their own. */ takeover(actor: TerminalDashboardActor, sessionId: string, acquisitionId?: string): TerminalControlTakeoverResult; /** * Give up the lease. `expectedAcquisition` makes it a compare-and-swap: the * release only applies while the lease is still the acquisition the caller * named. * * Compensation paths (a takeover whose receipt outlived its pane; a pane that * closed before its socket ever registered) MUST pass it. Without it, "release * whatever this login holds on this session" also releases a lease that a newer * pane of the same login has since taken over — same auth session, same lease * object, so no identity check can tell the two apart. */ release(actor: TerminalDashboardActor, sessionId: string, expectedAcquisition?: string): TerminalControlReleaseResult; state(actor: TerminalDashboardActor, sessionId: string): { mode: 'readonly' | 'controlled'; owned: boolean; expiresAt?: number; fixed?: boolean; acquisition?: string; }; /** Internal-only grant selection for the central terminal proxy. The * acquisition id lets the proxy prove that a write lease is still on the exact * same acquisition after the asynchronous worker WebSocket handshake. */ grantForProxy(actor: TerminalDashboardActor, sessionId: string): TerminalProxyGrant; /** Backward-compatible narrow accessor used by direct manager tests. */ grantFor(actor: TerminalDashboardActor, sessionId: string): string; registerWritableSocket(actor: TerminalDashboardActor, sessionId: string, socket: TerminalControlSocket, expectedAcquisition?: string): { registered: boolean; acquisition?: string; }; /** * A writable bridge for `acquisition` went away — give that acquisition's lease * up. * * Scoped to the CURRENT acquisition on purpose. A same-login takeover reuses * this very lease object without reissuing the signed grant, so an older pane's * socket closing used to tear down the lease the NEWER pane had just acquired * (its own socket may not even have bridged yet). Once the acquisition has * rotated, the old socket's close is simply not about this lease any more. */ disconnect(actor: TerminalDashboardActor, sessionId: string, acquisition: string | undefined): boolean; /** * Index a read-only bridged socket under the auth session that opened it. * `releaseByAuthSession` (fired on logout/session expiry) then closes it * immediately instead of waiting for the worker-side grant expiry timer. * Returns a deregistration closure for the socket's own natural close. */ registerReadSocket(authSessionId: string, socket: TerminalControlSocket): () => void; releaseByAuthSession(authSessionId: string): number; expireDue(): number; private expireSessionIfDue; private invalidate; } /** Parse a bounded operator-tunable lease TTL while preserving a short cap. */ export declare function terminalControlTtlFromEnv(env?: NodeJS.ProcessEnv): number; /** Type-only assertion used by downstream callers without exporting leases. */ export type TerminalControlGrant = TerminalControlGrantClaims; //# sourceMappingURL=terminal-control.d.ts.map