UNPKG

@undercroft/pulse-frame

Version:

Low-level WebSocket framing library implementing RFC 6455. Supports payload masking, control frames, and binary/text handling.

341 lines (327 loc) 11.7 kB
type Bit = 0 | 1; /** * Enum of all valid WebSocket frame opcodes, defined by RFC 6455 §5.2. */ declare enum PulseFrameOpcode { /** Continuation frame for fragmented messages */ CONTINUATION = 0, /** Text message frame */ TEXT = 1, /** Binary message frame */ BINARY = 2, /** Connection close frame */ CLOSE = 8, /** Ping frame */ PING = 9, /** Pong frame */ PONG = 10 } /** * Represents a parsed WebSocket frame header. * Contains control flags, opcode, payload length, and optional masking information. */ type PulseFrameHeader = { /** Final frame indicator (FIN bit) */ fin: Bit; /** Reserved bit 1 (RSV1) */ rsv1: Bit; /** Reserved bit 2 (RSV2) */ rsv2: Bit; /** Reserved bit 3 (RSV3) */ rsv3: Bit; /** Indicates whether the payload is masked (MASK bit) */ mask: Bit; /** The opcode defining the frame type (e.g., text, binary, ping) */ opcode: PulseFrameOpcode; /** Payload length (7-bit base or extended if necessary) */ length: number; /** Length of the extension data prefix, if any */ extensionDataLength: number; /** The 4-byte masking key, present only if `mask` is 1 */ maskingKey?: Buffer; }; /** * Represents a single WebSocket frame, parsed or constructed manually. * * A frame is the fundamental unit of data transmission in the WebSocket protocol. * * @see [RFC 6455, Section 5.2 - Base Framing Protocol](https://www.rfc-editor.org/rfc/rfc6455#section-5.2) */ declare class PulseFrame { private readonly header; private readonly payload; private readonly extensionData?; private readonly applicationData?; private readonly masked; constructor(header: PulseFrameHeader, payloadData: Buffer, extensionData?: Buffer, applicationData?: Buffer); /** * Only used internally after parsing to set application-level decoded payload. */ setApplicationData(data: Buffer): void; /** * Returns the decoded payload (application data if available). */ getPayload(): Buffer; /** * Returns the payload as a UTF-8 string, if any. */ getPayloadString(): string; /** * Returns the parsed extension data. */ getExtensionData(): Buffer | undefined; /** * Returns the raw parsed header. */ getHeader(): PulseFrameHeader; /** Whether the payload was masked. */ isMasked(): boolean; /** Whether this is the final frame in a sequence. */ isFinal(): boolean; /** Returns the frame's opcode. */ getOpcode(): PulseFrameOpcode; /** Whether this frame contains text data. */ isText(): boolean; /** Whether this frame contains binary data. */ isBinary(): boolean; /** Whether this frame continues a fragmented message. */ isContinuation(): boolean; /** Whether this frame is a control frame. */ isControl(): boolean; /** Whether this frame is a ping. */ isPing(): boolean; /** Whether this frame is a pong. */ isPong(): boolean; /** Whether this frame is a connection close. */ isClose(): boolean; /** Whether this is a text or binary data frame. */ isData(): boolean; /** * Converts this frame to a plain object for logging or inspection. */ toJSON(): Record<string, unknown>; /** * Returns a short string representation of the frame. */ toString(): string; } /** * @spec RFC 6455 §5.2 — Base Framing Protocol * * Options for constructing a new WebSocket frame in `createFrame`. */ type PulseFrameBuildOptions = { /** * Final fragment bit. Indicates that this is the final frame of a message. * @default 1 */ fin?: Bit; /** * Opcode for the frame, defining its type (e.g., TEXT, BINARY, CLOSE, PING). */ opcode: PulseFrameOpcode; /** * Reserved bit 1. Used for negotiated extensions (e.g., permessage-deflate). * @default 0 */ rsv1?: Bit; /** * Reserved bit 2. Used for negotiated extensions. * @default 0 */ rsv2?: Bit; /** * Reserved bit 3. Used for negotiated extensions. * @default 0 */ rsv3?: Bit; /** * Indicates if the payload should be masked (typically true for client frames). * @default 0 */ mask?: Bit; /** * Optional 4-byte masking key if `mask` is set to 1. */ maskingKey?: Buffer; /** * The raw payload buffer to send (application + extension data). */ payloadData?: Buffer; /** * The number of bytes in the payload reserved for extensions. * @default 0 */ extensionDataLength?: number; /** * Optional extension data buffer, separate from application payload. */ extensionData?: Buffer; }; /** * Creates a complete `PulseFrame` instance for transmission over a WebSocket connection. * * This function builds the frame header and payload according to * [RFC 6455 §5.2 - Base Framing Protocol](https://datatracker.ietf.org/doc/html/rfc6455#section-5.2), * ensuring correct encoding of all fields and masking (if required). * * ### RFC 6455 Compliance: * * - **FIN / RSV1-3 bits**: Set based on options; used for fragmentation and extensions. * - **Opcode**: Determines the type of frame (text, binary, close, ping, pong). * - **Masking**: * - Applied if `mask` is `1` and a `maskingKey` is provided. * - Required for clients; servers **MUST NOT** mask frames ([RFC §5.1]). * - **Payload length**: * - Calculated after masking is applied. * - Determines how many length bytes will be encoded later in serialization. * * This function does **not** serialize the frame; it prepares the internal representation. * For binary transmission, use `frame.toBuffer()`. * * @param options - Frame metadata and payload data. * @returns A fully prepared {@link PulseFrame} instance with a correct header and (optionally masked) payload. * * @example * ```ts * const frame = createFrame({ * fin: 1, * opcode: PulseFrameOpcode.TEXT, * mask: 1, * maskingKey: createRandomBytes(4), * payloadData: Buffer.from("Hello"), * }); * socket.write(frame.toBuffer()); * ``` * * @see RFC 6455 §5.2 - Base Framing Protocol * @see RFC 6455 §5.1 - Client-to-Server Masking */ declare function createFrame(options: PulseFrameBuildOptions): PulseFrame; /** * Indicates which reserved bits (RSV1, RSV2, RSV3) are supported by negotiated extensions. * If a bit is set to true, it means an extension has explicitly enabled it. */ type PulseFrameExtensionSupport = { rsv1?: boolean; rsv2?: boolean; rsv3?: boolean; }; /** * Options for parsing or validating a WebSocket frame. * These are typically passed during buffer decoding. */ type PulseFrameOptions = { /** Length of the extension data to strip from the start of the payload */ extensionLength?: number; /** Negotiated extension bit support (RSV1/RSV2/RSV3) */ extensionSupport: PulseFrameExtensionSupport; }; /** * Parses a WebSocket frame from a raw buffer according to [RFC 6455 §5](https://datatracker.ietf.org/doc/html/rfc6455#section-5). * * This function: * - Validates header size and structure * - Extracts masking key, extended payload length, and payload data * - Validates RSV bits, opcodes, and control frame rules * - Unmasks payload if required * - Separates extension data from application data * * Supports optional extension handling via {@link PulseFrameOptions}. * * @param buffer - The raw incoming buffer containing a WebSocket frame. * @param options - Optional framing behavior configuration, including extension support and extension data length. * @returns A parsed and validated {@link PulseFrame} object. * * @throws {PulseFrameError} If the frame is incomplete, invalid, or violates protocol constraints. * * @example * ```ts * const frame = createFrameFromBuffer(buffer, { * extensionLength: 8, * extensionSupport: { rsv1: true } * }); * ``` * * @see RFC 6455 §5 - Data Framing * @see RFC 6455 §5.5 - Control Frames * @see RFC 6455 §7 - Closing Handshake */ declare function createFrameFromBuffer(buffer: Buffer, options?: PulseFrameOptions): PulseFrame; /** * Creates a WebSocket control frame according to [RFC 6455 §5.5](https://datatracker.ietf.org/doc/html/rfc6455#section-5.5). * * This utility is used to generate Ping, Pong, and Close frames with the correct header flags: * - FIN bit is always set (`fin = 1`) * - RSV1/RSV2/RSV3 are unset (`0`) * - Control frames must not be fragmented * - Payload must be ≤ 125 bytes * * @param payload - Optional binary payload to include (e.g., Pong echo, Close code+reason). * @param opcode - The control frame opcode (`PING`, `PONG`, `CLOSE`). * @returns A fully constructed {@link PulseFrame} representing the control frame. * * @throws {PulseFrameError} If the payload exceeds the allowed limit for control frames. * * @example * ```ts * const pingFrame = createFrameFromControlCode(Buffer.from('ping'), PulseFrameOpcode.PING); * const closeFrame = createFrameFromControlCode(undefined, PulseFrameOpcode.CLOSE); * ``` * * @see RFC 6455 §5.5 - Control Frames */ declare function createFrameFromControlCode(payload: Buffer | undefined, opcode: PulseFrameOpcode): PulseFrame; /** * Creates a WebSocket text frame with the given UTF-8 string. * * The string is encoded as UTF-8 and inserted directly into the payload. * This is a convenience wrapper for quickly sending simple text messages. * * @param text - The UTF-8 string to send as the frame payload. * @returns A complete PulseFrame representing a text frame. * * @see RFC 6455 §5.6 - Data Frames (Text) */ declare function createFrameFromText(text: string): PulseFrame; /** * Converts a `PulseFrame` object into a raw WebSocket frame buffer. * * This function serializes a fully constructed frame into a binary format * suitable for transmission over a socket, following the WebSocket framing * rules defined in [RFC 6455 §5.2](https://datatracker.ietf.org/doc/html/rfc6455#section-5.2). * * It handles: * - Base header byte construction (FIN, RSV1–3, opcode, MASK). * - Extended payload length encoding if necessary (16-bit or 64-bit). * - Optional payload masking (XOR with masking key). * - Concatenation of header and final payload buffer. * * @param frame - The `PulseFrame` instance to serialize. * @returns A `Buffer` containing the fully encoded WebSocket frame. * * @see RFC 6455 §5.2 - Base Framing Protocol */ declare function frameToBuffer(frame: PulseFrame): Buffer; /** * Represents a protocol error encountered while parsing or constructing * a WebSocket frame. * * Used to signal issues such as: * - Invalid opcodes * - Malformed payloads * - Invalid close codes * - Incomplete or truncated frames * * @extends Error */ declare class PulseFrameError extends Error { readonly message: string; /** * Creates a new PulseFrameError with a specific error message. * * @param message - A descriptive explanation of the framing error. */ constructor(message: string); } export { PulseFrame, type PulseFrameBuildOptions, PulseFrameError, type PulseFrameExtensionSupport, type PulseFrameHeader, PulseFrameOpcode, type PulseFrameOptions, createFrame, createFrameFromBuffer, createFrameFromControlCode, createFrameFromText, frameToBuffer };