import type { Config } from './config.js'; import { type Op } from './ot.js'; /** Name shown to humans who have the pad open while this server edits it. */ export declare const CLIENT_NAME = "rustpad-mcp"; /** * Bounds on the connection. Every network wait has a wall-clock limit and * every buffer a size limit: the server side of this protocol is whatever * RUSTPAD_URL points at, and a hostile or broken upstream must cost a failed * tool call, not a hung process or unbounded memory. */ export interface SessionLimits { openTimeoutMs: number; ackTimeoutMs: number; /** Idle window that ends the initial burst. */ settleIdleMs: number; /** Wall-clock cap on the whole settle phase, chatty server or not. */ settleDeadlineMs: number; maxQueuedMessages: number; /** * UTF-16 units the queue may hold across all buffered messages. * * The count above is not a memory bound: a thousand messages at the frame * limit is a gigabyte, measured at 424 MB for four hundred of them, and * the process this server usually runs in is allowed 192 MB. The frames * arrive between two turns of the event loop — faster than they are * folded whenever the upstream is on a fast link — so the bound has to be * on what is held, not on how many things are held. */ maxQueuedBytes: number; /** * Largest single message accepted, enforced *before* it is buffered: the * WebSocket implementation is told this number and fails the connection on * a frame header that announces more, so a hostile upstream cannot make the * process hold a 128 MB message (undici's default) for a check that runs * after the fact. The same number is checked again on the message once it * arrives, for a factory that ignores the option. */ maxFrameBytes: number; /** * Operations this client will fold out of a single History message. * * A backstop on {@link maxFrameBytes}, not the bound that does the work. * Folding one operation costs O(document length), so the cost of a History * message is the product of its length and the document's — and a count is a * poor proxy for either. The bound that actually holds is the deadline check * inside the folding loop; this one exists so that raising `maxFrameBytes` * does not silently raise the number of operations one frame may demand. * * Set above what a 1 MiB frame can carry (the smallest useful entry is about * 25 bytes, so ~42 000) on purpose. A tighter count would refuse pads that * work today: Rustpad replays a pad's entire history on connect and never * compacts it, a typing session produces roughly one operation per keystroke, * and a real entry runs 30–60 bytes — so a cap in the low tens of thousands * would turn "slow but fine" into "permanently unusable" for an ordinary * heavily-edited pad, to save an adversary's frame twenty seconds it can only * spend once per tool call anyway. */ maxHistoryOperations: number; maxUsers: number; } export declare const DEFAULT_LIMITS: SessionLimits; export interface UserInfo { name: string; hue: number; } export interface DocumentState { /** Number of operations the server holds; the base for the next edit. */ revision: number; text: string; language: string | undefined; /** Users connected right now, keyed by their socket id (excluding us). */ users: Map; /** * Whether a History message was ever seen on this connection. * * The one thing `text === ''` cannot tell a caller apart. Rustpad sends no * History at all for a pad that has never been written, so silence means * either "empty" or "not here yet" — and {@link RustpadSession.settle} ends * the initial burst after {@link SessionLimits.settleIdleMs} of quiet, which * a slow instance, a restored database or a buffering proxy can outlast. * Everything that treats an empty pad as licence to skip a guard has to know * which of the two it is looking at. */ sawHistory: boolean; } /** * The browser-flavoured surface this module needs from a WebSocket. Both * Node's global implementation and undici's export satisfy it; tests inject a * fake that plays the server side of the protocol. */ export interface WebSocketLike { addEventListener(type: 'open' | 'close' | 'error' | 'message', listener: (event: { data?: unknown; }) => void): void; send(data: string): void; close(): void; } export interface WebSocketOptions { insecureTls: boolean; /** Passed to the implementation as its payload limit; see {@link SessionLimits.maxFrameBytes}. */ maxFrameBytes: number; } export type WebSocketFactory = (url: string, options: WebSocketOptions) => WebSocketLike; /** Default factory: undici's WebSocket on a dispatcher that carries the limits. */ export declare function defaultWebSocketFactory(url: string, options: WebSocketOptions): WebSocketLike; /** Maps the configured http(s) base URL to the ws(s) socket URL for a pad. */ export declare function socketUrl(baseUrl: string, id: string): string; /** * One short-lived connection to a pad: read the current state, optionally send * a single edit or a language change, close. Stateless by design — every tool * call sees the server's truth of that moment, and there is no long-lived * socket to babysit across hub restarts. */ export declare class RustpadSession { private readonly socket; private readonly messages; private readonly limits; readonly state: DocumentState; private identity; private infoSent; private constructor(); static open(config: Config, id: string, factory?: WebSocketFactory, limits?: SessionLimits): Promise; /** Reads until the initial burst (or the aftermath of an edit) goes quiet. */ private settle; /** * Folds one server message into the state. * * `deadline` is the wall-clock limit of whatever wait this message arrived * during, and it is passed in rather than checked by the caller because the * expensive part happens *here*: a single History message can carry tens of * thousands of operations, and the caller's own deadline check is not reached * again until all of them have been applied. */ private handle; private sendClientInfo; /** * Sends one edit based on the current state and waits until the server's * History echo contains an operation attributed to this connection — that * echo is the only acknowledgement the protocol has. Concurrent edits that * arrive first are folded in; the server transforms this operation against * them itself. */ edit(ops: Op[]): Promise; /** * Sets the editor language and waits for the server's broadcast of the same * value, which every connection — including the sender — receives. */ setLanguage(language: string): Promise; close(): void; } /** * Opens a session, runs `fn`, and closes the socket no matter how `fn` ends. */ export declare function withSession(config: Config, id: string, factory: WebSocketFactory | undefined, fn: (session: RustpadSession) => Promise): Promise;