@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
TypeScript
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 };