/** * ShellSession — async-iterable interactive PTY WebSocket session. * * Connects on `connect()`, reads the initial STATUS confirmation frame, and exposes * typed `send()` / `resize()` / `[Symbol.asyncIterator]()` / `close()`. * * When `reconnectConfig` is provided, transparently reconnects on unexpected disconnects * using the same `shellId` so the shell's working directory, environment, background jobs, * and up to 256 KB of buffered output are preserved on the server. * * Reconnect restores the *connection* on its own (it is driven by the socket close event, * not by your read loop, and `send()`/`resize()` wait for it). However, on reattach the * server replays the buffered output as inbound frames — to receive that replay (and to see * `bytesDropped` updated and `exitCode` set) you must be consuming the session with * `for await (const frame of shell)`. A write-only caller that never iterates stays * connected across drops but will not observe the replayed output. Keep a `for await` loop * running for the life of the session. * * @example * ```typescript * const shell = await client.openShell({ runtimeArn }) * try { * await shell.send('cat /etc/os-release\n') * for await (const frame of shell) { * if (frame.channel === ShellChannel.STDOUT) process.stdout.write(frame.text) * } * } finally { * await shell.close() * } * ``` */ import WebSocket from 'ws'; import type { ClientOptions } from 'ws'; import { Buffer } from 'buffer'; import { type ShellFrame } from './protocol.js'; import { type Logger, type ReconnectConfig } from './config.js'; /** * Callback that produces connection params for a new WebSocket. * Receives the current `shellId` and `sessionId` so both can be embedded in the * signed URL/headers. Both values are server-confirmed and may differ from the * values originally passed to `openShell` after the first connection. */ export type ConnectFn = (shellId: string, sessionId: string) => Promise<{ url: string; headers: Record; /** WebSocket subprotocols — used by OAuth auth (base64UrlBearerAuthorization). */ protocols?: string[]; }>; /** Options for constructing a ShellSession. */ export interface ShellSessionOptions { connectFn: ConnectFn; shellId?: string | undefined; sessionId?: string | undefined; reconnectConfig?: ReconnectConfig | undefined; /** * Interval in milliseconds between RFC 6455 Ping frames sent to keep the connection * alive through the KARP proxy (~60s idle timeout). Defaults to 30000ms. * Set to 0 to disable keepalive (e.g. when the caller manages pings externally). */ keepaliveIntervalMs?: number | undefined; /** * Optional logger for diagnostic output. When omitted, all logging is silent. * Pass `console` to enable, or any object implementing `{ debug, info, warn }`. */ logger?: Logger | undefined; /** * Optional WebSocket factory for testing — overrides `new WebSocket(...)`. * @internal */ _wsFactory?: ((url: string, protocols?: string[], options?: ClientOptions) => WebSocket) | undefined; } /** * Async-iterable shell session wrapping a live PTY WebSocket. * * Read-only observable attributes (updated by the session as events arrive): * - `shellId` — Server-confirmed shell identifier. Preserve to reconnect to the same PTY. * - `sessionId` — Runtime session ID routing to the VM. * - `reconnected` — True when the most recent connect reattached an existing PTY. * - `kicked` — True when another client connected with the same shellId (close 4000). * Check this after the `for await` loop exits to distinguish a kick from * a clean shell exit. * - `bytesDropped` — PTY ring-buffer bytes lost during the most recent disconnect, as * reported by the server in the reconnect confirmation frame. * Zero if no overflow occurred or on a fresh connection. * - `exitCode` — Shell process exit code. `null` until the shell exits; `0` for a clean * exit. Check this after the `for await` loop exits alongside `kicked`. */ export declare class ShellSession implements AsyncIterable { private _shellId; private _sessionId; private _reconnected; private _kicked; private _bytesDropped; private _exitCode; /** Server-confirmed shell identifier. */ get shellId(): string; /** Runtime session ID routing to the VM. */ get sessionId(): string; /** True when the most recent connect reattached an existing PTY. */ get reconnected(): boolean; /** * True when another client connected with the same shellId (close 4000). * Check after the `for await` loop exits to distinguish a kick from a clean exit. */ get kicked(): boolean; /** * PTY ring-buffer bytes lost during the most recent disconnect. * Zero when no overflow occurred or on a fresh connection. */ get bytesDropped(): number; /** * Shell process exit code. `null` until the shell exits; `0` for a clean exit. * Check after the `for await` loop exits alongside `kicked`. */ get exitCode(): number | null; private readonly connectFn; private readonly reconnectConfig; private readonly keepaliveIntervalMs; private readonly log; private readonly framer; private readonly _wsFactory; private _state; private _abortController; private readonly _sessionController; private _closeError; /** * Set while a reconnect is in flight, cleared when it settles. Shared so that the * iterator, the close/dead-detection handler, and send()/resize() all await the same * attempt rather than racing or each starting their own. Resolves to the reconnect * outcome (true = recovered, false = gave up). This is what makes *connection* recovery * iterator-independent — the socket is restored without a `for await` loop. Consuming the * replayed output still requires an active iterator (see the class docstring). */ private _reconnectPromise; constructor(opts: ShellSessionOptions); /** Connect and read the initial STATUS metadata frame. */ connect(): Promise; /** * Send text or raw bytes to the shell's stdin. * Pass a string for text commands; pass a Buffer for binary/escape sequences. * * If a reconnect is in flight, this waits for it and sends on the recovered * connection. Throws a descriptive `Error` (never the raw `ws` "readyState 3" * error) when the session is closed or could not be recovered. */ send(data: string | Buffer): Promise; /** Send a HEARTBEAT frame (0x05) to the server. */ sendHeartbeat(): Promise; /** Resize the terminal PTY. */ resize(width: number, height: number): Promise; /** * Resolve the live socket for a write, healing first if needed. Awaits an in-flight * reconnect (transparent recovery) and validates the *real* socket * readyState — not just the `_state` flag, which can lag a silently-dropped socket. * Throws a descriptive `Error` instead of leaking the raw `ws` * "readyState 3 (CLOSED)" error. */ private _writableSocket; /** Send a CLOSE frame (0xFF) to permanently kill the shell, then close the WebSocket. * The server kills the shell process (SIGHUP → SIGKILL) and responds with its own [0xFF]. * Unlike dropping the WebSocket (which detaches and allows reconnection), this is permanent. */ close(): Promise; /** * Forcibly terminates the underlying WebSocket without a clean handshake. * Useful in tests to simulate an abrupt network drop and trigger the reconnect path. * Has no effect if the session is not currently open. * @internal */ _terminateConnection(): void; /** * Async iterator — yields inbound ShellFrames, reconnecting on drop if configured. * * The loop exits silently (no throw) in three cases: shell exit, kicked by a new * client, or reconnect budget exhausted. Check `exitCode`, `kicked`, and * `bytesDropped` after the loop to distinguish them: * * ```typescript * for await (const frame of shell) { ... } * if (shell.kicked) { ... } // another client took over * if (shell.exitCode !== null) { ... } // shell process exited * if (shell.bytesDropped > 0) { ... } // ring-buffer overflow on reconnect * ``` */ [Symbol.asyncIterator](): AsyncIterator; private _startKeepalive; private _stopKeepalive; private _wsSend; /** Open WebSocket, capture 101 upgrade headers, then read metadata frame. */ private _connectWithUpgrade; /** Receive one raw binary message from the WebSocket. */ private _recvRaw; /** * Consume frames until a STATUS confirmation is found, stashing others in pendingFrames. * Returns the accumulated pending frames to be stored in the 'open' state. */ private _readMetadataFrame; private _isConfirmationStatus; /** * Record `bytesDropped` from a reconnection confirmation frame's metadata, if present. * `bytesDropped` reports PTY output lost from ring-buffer overflow during THIS disconnect * (per-disconnect, not session-cumulative — assign, don't accumulate). Present * only when greater than 0; absent on a clean reconnect. */ private _recordBytesDropped; private _isTerminationStatus; private _parseExitCode; private _iterate; /** Returns true when close() has been called. Used after await points to guard * against close() firing while the method was suspended. A method call prevents * TypeScript from narrowing away 'closed' comparisons after state assignments. */ private _isClosed; private _extractCloseCode; /** * Wait for the socket's authoritative 'close' to land after an 'error' woke the read * loop early. The 'close' handler sets `_closeError` (carrying the real close code) and * makes the reconnect decision, so this resolves as soon as `_closeError` is populated. * Bounded by a short timeout in case 'close' never follows (it always does on a * connected `ws`, but we must not hang the iterator on a misbehaving socket). */ private _waitForClose; /** * Decide whether a close code is auto-reconnectable, then start-or-join a reconnect. * * This is the single entry point for triggering reconnection. It is called from * wherever a disconnect is observed — the iterator's read error, the `ws.on('close')` * handler (dead-detection / silent drop while no one is iterating), or the keepalive * pong-timeout — so restoring the *connection* no longer depends on an active `for await` * loop. (Receiving the server's replayed output after reattach still requires iterating; * see the class docstring.) * * Returns a promise that resolves to the reconnect outcome (true = recovered). * Returns immediately with `false` for terminal close codes (4000 kicked, 1003 text, * 1000 normal) or when no `reconnectConfig` is set. Concurrent callers share one * in-flight attempt via `_reconnectPromise`. */ private _ensureReconnect; private _reconnectWithBackoff; private _runInnerRetryLoop; } //# sourceMappingURL=session.d.ts.map