Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@silver886/mcp-proxy

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@silver886/mcp-proxy - npm Package Compare versions

Comparing version
0.2.0
to
0.2.1
+10
node_modules/cloudflared/lib/cloudflared.js
#!/usr/bin/env node
"use strict";
var import_error = require("./error.js");
var import_index = require("./index.js");
(0, import_index.main)().catch((err) => {
if (err instanceof import_error.UnsupportedError) {
console.error(err.message);
process.exit(1);
}
});
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var constants_exports = {};
__export(constants_exports, {
CLOUDFLARED_VERSION: () => CLOUDFLARED_VERSION,
DEFAULT_CLOUDFLARED_BIN: () => DEFAULT_CLOUDFLARED_BIN,
RELEASE_BASE: () => RELEASE_BASE,
bin: () => bin,
use: () => use
});
module.exports = __toCommonJS(constants_exports);
var import_node_path = __toESM(require("node:path"));
const DEFAULT_CLOUDFLARED_BIN = import_node_path.default.join(
__dirname,
"..",
"bin",
process.platform === "win32" ? "cloudflared.exe" : "cloudflared"
);
let bin = process.env.CLOUDFLARED_BIN || DEFAULT_CLOUDFLARED_BIN;
function use(executable) {
bin = executable;
}
const CLOUDFLARED_VERSION = process.env.CLOUDFLARED_VERSION || "latest";
const RELEASE_BASE = "https://github.com/cloudflare/cloudflared/releases/";
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CLOUDFLARED_VERSION,
DEFAULT_CLOUDFLARED_BIN,
RELEASE_BASE,
bin,
use
});
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var error_exports = {};
__export(error_exports, {
UnsupportedError: () => UnsupportedError
});
module.exports = __toCommonJS(error_exports);
class UnsupportedError extends Error {
constructor(message) {
super(message);
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
UnsupportedError
});
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var handler_exports = {};
__export(handler_exports, {
ConfigHandler: () => ConfigHandler,
ConnectionHandler: () => ConnectionHandler,
TryCloudflareHandler: () => TryCloudflareHandler
});
module.exports = __toCommonJS(handler_exports);
var import_node_stream = require("node:stream");
var import_regex = require("./regex");
class ConnectionHandler {
constructor(tunnel) {
this.connections = [];
this.connected_handler = (output, tunnel) => {
const conn_match = output.match(import_regex.conn_regex);
const ip_match = output.match(import_regex.ip_regex);
const location_match = output.match(import_regex.location_regex);
const index_match = output.match(import_regex.index_regex);
if (conn_match && ip_match && location_match && index_match) {
const connection = {
id: conn_match[1],
ip: ip_match[1],
location: location_match[1]
};
this.connections[Number(index_match[1])] = connection;
tunnel.emit("connected", connection);
}
};
this.disconnected_handler = (output, tunnel) => {
const index_match = output.includes("terminated") ? output.match(import_regex.index_regex) : null;
if (index_match) {
const index = Number(index_match[1]);
if (this.connections[index]) {
tunnel.emit("disconnected", this.connections[index]);
this.connections[index] = void 0;
}
}
};
tunnel.addHandler(this.connected_handler.bind(this));
tunnel.addHandler(this.disconnected_handler.bind(this));
}
}
class TryCloudflareHandler {
constructor(tunnel) {
this.url_handler = (output, tunnel) => {
const url_match = output.match(/https:\/\/([a-z0-9-]+)\.trycloudflare\.com/);
if (url_match) {
tunnel.emit("url", url_match[0]);
}
};
tunnel.addHandler(this.url_handler.bind(this));
}
}
class ConfigHandler extends import_node_stream.EventEmitter {
constructor(tunnel) {
super();
this.config_handler = (output, tunnel) => {
const config_match = output.match(/\bconfig="(.+?)" version=(\d+)/);
if (config_match) {
try {
const config_str = config_match[1].replace(/\\"/g, '"');
const config = JSON.parse(config_str);
const version = parseInt(config_match[2], 10);
this.emit("config", {
config,
version
});
if (config && typeof config === "object" && "ingress" in config && Array.isArray(config.ingress)) {
for (const ingress of config.ingress) {
if ("hostname" in ingress) {
tunnel.emit("url", ingress.hostname);
}
}
}
} catch (error) {
this.emit("error", new Error(`Failed to parse config: ${error}`));
}
}
};
tunnel.addHandler(this.config_handler.bind(this));
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
off(event, listener) {
return super.off(event, listener);
}
emit(event, ...args) {
return super.emit(event, ...args);
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ConfigHandler,
ConnectionHandler,
TryCloudflareHandler
});
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var src_exports = {};
__export(src_exports, {
main: () => main
});
module.exports = __toCommonJS(src_exports);
var import_node_fs = __toESM(require("node:fs"));
var import_node_https = __toESM(require("node:https"));
var import_node_child_process = require("node:child_process");
var import_lib = require("./lib.js");
var import_constants = require("./constants.js");
async function main() {
const args = process.argv.slice(2);
if (args[0] === "bin") {
if (!args[1]) {
console.log(import_lib.bin);
return;
}
if (args[1] === "remove") {
import_node_fs.default.unlinkSync(import_lib.bin);
console.log("Removed cloudflared");
return;
}
if (args[1] === "install") {
const version = args[2] || import_constants.CLOUDFLARED_VERSION;
if (version !== "latest") {
console.log(`Installing cloudflared ${args[2]}`);
console.log(await (0, import_lib.install)(import_lib.bin, args[2]));
} else {
console.log("Installing latest version of cloudflared");
await (0, import_lib.install)(import_lib.bin, version);
}
return;
}
if (args[1] === "list") {
import_node_https.default.get(
{
hostname: "api.github.com",
path: "/repos/cloudflare/cloudflared/releases",
headers: {
"user-agent": "node-cloudflared"
}
},
(res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
const releases = JSON.parse(data);
for (const release of releases) {
console.log(
`${release.tag_name.padEnd(10)} (${release.published_at}) [${release.html_url}]`
);
}
});
}
);
return;
}
if (args[1] === "help" || args[1] === "--help" || args[1] === "-h") {
console.log(`cloudflared bin : Prints the path to the binary`);
console.log(`cloudflared bin remove : Removes the binary`);
console.log(`cloudflared bin install [version] : Installs the binary`);
console.log(`cloudflared bin list : Lists 30 latest releases`);
console.log(`cloudflared bin help : Prints this help message`);
console.log(`Examples:`);
console.log(
`cloudflared bin install : Installs the latest version of cloudflared`
);
console.log(`cloudflared bin install 2023.4.1 : Installs cloudflared 2023.4.1`);
console.log(
`You can find releases at https://github.com/cloudflare/cloudflared/releases`
);
return;
}
}
if (!import_node_fs.default.existsSync(import_lib.bin)) {
console.log("Installed cloudflared to " + await (0, import_lib.install)(import_lib.bin));
}
const sub = (0, import_node_child_process.spawn)(import_lib.bin, args, { stdio: "inherit" });
sub.on("exit", (code) => {
if (typeof code === "number") {
process.exit(code);
} else {
process.exit(1);
}
});
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
for (const signal of signals) {
process.on(signal, () => {
sub.kill(signal);
});
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
main
});
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var install_exports = {};
__export(install_exports, {
install: () => install,
install_linux: () => install_linux,
install_macos: () => install_macos,
install_windows: () => install_windows
});
module.exports = __toCommonJS(install_exports);
var import_node_fs = __toESM(require("node:fs"));
var import_node_path = __toESM(require("node:path"));
var import_node_https = __toESM(require("node:https"));
var import_node_child_process = require("node:child_process");
var import_constants = require("./constants");
var import_error = require("./error");
const LINUX_URL = {
arm64: "cloudflared-linux-arm64",
arm: "cloudflared-linux-arm",
x64: "cloudflared-linux-amd64",
ia32: "cloudflared-linux-386"
};
const MACOS_URL = {
arm64: "cloudflared-darwin-arm64.tgz",
x64: "cloudflared-darwin-amd64.tgz"
};
const WINDOWS_URL = {
x64: "cloudflared-windows-amd64.exe",
ia32: "cloudflared-windows-386.exe"
};
function resolve_base(version) {
if (version === "latest") {
return `${import_constants.RELEASE_BASE}latest/download/`;
}
return `${import_constants.RELEASE_BASE}download/${version}/`;
}
async function install(to, version = import_constants.CLOUDFLARED_VERSION) {
if (process.platform === "linux") {
return install_linux(to, version);
} else if (process.platform === "darwin") {
return install_macos(to, version);
} else if (process.platform === "win32") {
return install_windows(to, version);
} else {
throw new import_error.UnsupportedError("Unsupported platform: " + process.platform);
}
}
async function install_linux(to, version = import_constants.CLOUDFLARED_VERSION) {
const file = LINUX_URL[process.arch];
if (file === void 0) {
throw new import_error.UnsupportedError("Unsupported architecture: " + process.arch);
}
await download(resolve_base(version) + file, to);
import_node_fs.default.chmodSync(to, "755");
return to;
}
async function install_macos(to, version = import_constants.CLOUDFLARED_VERSION) {
let arch = process.arch;
if (version !== "latest" && version_number(version) < 20240802) {
arch = "x64";
}
const file = MACOS_URL[arch];
if (file === void 0) {
throw new import_error.UnsupportedError("Unsupported architecture: " + arch);
}
await download(resolve_base(version) + file, `${to}.tgz`);
process.env.VERBOSE && console.log(`Extracting to ${to}`);
(0, import_node_child_process.execSync)(`tar -xzf ${import_node_path.default.basename(`${to}.tgz`)}`, { cwd: import_node_path.default.dirname(to) });
import_node_fs.default.unlinkSync(`${to}.tgz`);
import_node_fs.default.renameSync(`${import_node_path.default.dirname(to)}/cloudflared`, to);
return to;
}
async function install_windows(to, version = import_constants.CLOUDFLARED_VERSION) {
const file = WINDOWS_URL[process.arch];
if (file === void 0) {
throw new import_error.UnsupportedError("Unsupported architecture: " + process.arch);
}
await download(resolve_base(version) + file, to);
return to;
}
function download(url, to, redirect = 0) {
if (redirect === 0) {
process.env.VERBOSE && console.log(`Downloading ${url} to ${to}`);
} else {
process.env.VERBOSE && console.log(`Redirecting to ${url}`);
}
if (!import_node_fs.default.existsSync(import_node_path.default.dirname(to))) {
import_node_fs.default.mkdirSync(import_node_path.default.dirname(to), { recursive: true });
}
return new Promise((resolve, reject) => {
const request = import_node_https.default.get(url, (res) => {
const redirect_code = [301, 302, 303, 307, 308];
if (redirect_code.includes(res.statusCode) && res.headers.location !== void 0) {
request.destroy();
const redirection = res.headers.location;
resolve(download(redirection, to, redirect + 1));
return;
}
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
const file = import_node_fs.default.createWriteStream(to);
file.on("finish", () => {
file.close(() => resolve(to));
});
file.on("error", (err) => {
import_node_fs.default.unlink(to, () => reject(err));
});
res.pipe(file);
} else {
request.destroy();
reject(new Error(`HTTP response with status code: ${res.statusCode}`));
}
});
request.on("error", (err) => {
reject(err);
});
request.end();
});
}
function version_number(semver) {
const [major, minor, patch] = semver.split(".").map(Number);
return major * 1e4 + minor * 100 + patch;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
install,
install_linux,
install_macos,
install_windows
});
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import { EventEmitter as EventEmitter$1 } from 'node:stream';
declare const DEFAULT_CLOUDFLARED_BIN: string;
/**
* The path to the cloudflared binary.
* If the `CLOUDFLARED_BIN` environment variable is set, it will be used; otherwise, {@link DEFAULT_CLOUDFLARED_BIN} will be used.
* Can be overridden with {@link use}.
*/
declare let bin: string;
/**
* Override the path to the cloudflared binary.
* @param executable - The path to the cloudflared executable.
*/
declare function use(executable: string): void;
declare const CLOUDFLARED_VERSION: string;
declare const RELEASE_BASE = "https://github.com/cloudflare/cloudflared/releases/";
/**
* Install cloudflared to the given path.
* @param to The path to the binary to install.
* @param version The version of cloudflared to install.
* @returns The path to the binary that was installed.
*/
declare function install$1(to: string, version?: string): Promise<string>;
declare function install_linux(to: string, version?: string): Promise<string>;
declare function install_macos(to: string, version?: string): Promise<string>;
declare function install_windows(to: string, version?: string): Promise<string>;
interface Connection {
id: string;
ip: string;
location: string;
}
type TunnelOptions = Record<string, string | number | boolean>;
interface TunnelEvents {
url: (url: string) => void;
connected: (connection: Connection) => void;
disconnected: (connection: Connection) => void;
stdout: (data: string) => void;
stderr: (data: string) => void;
error: (error: Error) => void;
exit: (code: number | null, signal: NodeJS.Signals | null) => void;
}
type OutputHandler = (output: string, tunnel: Tunnel) => void;
declare class Tunnel extends EventEmitter {
private _process;
private outputHandlers;
constructor(options?: TunnelOptions | string[]);
get process(): ChildProcess;
private setupDefaultHandlers;
/**
* Add a custom output handler
* @param handler Function to handle cloudflared output
*/
addHandler(handler: OutputHandler): void;
/**
* Remove a previously added output handler
* @param handler The handler to remove
*/
removeHandler(handler: OutputHandler): void;
private processOutput;
private setupEventHandlers;
private createProcess;
stop: () => boolean;
private _stop;
on<E extends keyof TunnelEvents>(event: E, listener: TunnelEvents[E]): this;
once<E extends keyof TunnelEvents>(event: E, listener: TunnelEvents[E]): this;
off<E extends keyof TunnelEvents>(event: E, listener: TunnelEvents[E]): this;
emit<E extends keyof TunnelEvents>(event: E, ...args: Parameters<TunnelEvents[E]>): boolean;
/**
* Create a quick tunnel without a Cloudflare account.
* @param url The local service URL to connect to. If not provided, the hello world mode will be used.
* @param options The options to pass to cloudflared.
*/
static quick(url?: string, options?: TunnelOptions): Tunnel;
/**
* Create a tunnel with a Cloudflare account.
* @param token The Cloudflare Tunnel token.
* @param options The options to pass to cloudflared.
*/
static withToken(token: string, options?: TunnelOptions): Tunnel;
}
/**
* Create a tunnel.
* @param options The options to pass to cloudflared.
* @returns A Tunnel instance
*/
declare function tunnel(options?: TunnelOptions): Tunnel;
/**
* Build the arguments for the cloudflared command.
* @param options The options to pass to cloudflared.
* @returns The arguments for the cloudflared command.
*/
declare function build_args(options: TunnelOptions): string[];
declare function build_options(options: TunnelOptions): string[];
/**
* Cloudflared launchd identifier.
* @platform macOS
*/
declare const identifier = "com.cloudflare.cloudflared";
/**
* Path of service related files.
* @platform macOS
*/
declare const MACOS_SERVICE_PATH: {
readonly PLIST: string;
readonly OUT: string;
readonly ERR: string;
};
/**
* Cloudflared Service API.
*/
declare const service: {
install: typeof install;
uninstall: typeof uninstall;
exists: typeof exists;
log: typeof log;
err: typeof err;
current: typeof current;
clean: typeof clean;
journal: typeof journal;
};
/**
* Throw when service is already installed.
*/
declare class AlreadyInstalledError extends Error {
constructor();
}
/**
* Throw when service is not installed.
*/
declare class NotInstalledError extends Error {
constructor();
}
/**
* Install Cloudflared service.
* @param token Tunnel service token.
* @platform macOS, linux
*/
declare function install(token?: string): void;
/**
* Uninstall Cloudflared service.
* @platform macOS, linux
*/
declare function uninstall(): void;
/**
* Get stdout log of cloudflared service. (Usually empty)
* @returns stdout log of cloudflared service.
* @platform macOS, linux (sysv)
*/
declare function log(): string;
/**
* Get stderr log of cloudflared service. (cloudflared print all things here)
* @returns stderr log of cloudflared service.
* @platform macOS, linux (sysv)
*/
declare function err(): string;
/**
* Get cloudflared service journal from journalctl.
* @param n The number of entries to return.
* @returns cloudflared service journal.
* @platform linux (systemd)
*/
declare function journal(n?: number): string;
/**
* Get informations of current running cloudflared service.
* @returns informations of current running cloudflared service.
* @platform macOS, linux
*/
declare function current(): {
/** Tunnel ID */
tunnelID: string;
/** Connector ID */
connectorID: string;
/** The connections of the tunnel */
connections: Connection[];
/** Metrics Server Location */
metrics: string;
/** Tunnel Configuration */
config: {
ingress?: {
service: string;
hostname?: string;
}[];
[key: string]: unknown;
};
};
/**
* Clean up service log files.
* @platform macOS
*/
declare function clean(): void;
/**
* Check if cloudflared service is installed.
* @returns true if service is installed, false otherwise.
* @platform macOS, linux
*/
declare function exists(): boolean;
declare class ConnectionHandler {
private connections;
constructor(tunnel: Tunnel);
private connected_handler;
private disconnected_handler;
}
declare class TryCloudflareHandler {
constructor(tunnel: Tunnel);
private url_handler;
}
interface ConfigHandlerEvents<T> {
config: (config: {
config: T;
version: number;
}) => void;
error: (error: Error) => void;
}
interface TunnelConfig {
ingress: Record<string, string>[];
warp_routing: {
enabled: boolean;
};
}
declare class ConfigHandler<T = TunnelConfig> extends EventEmitter$1 {
constructor(tunnel: Tunnel);
private config_handler;
on<E extends keyof ConfigHandlerEvents<T>>(event: E, listener: ConfigHandlerEvents<T>[E]): this;
once<E extends keyof ConfigHandlerEvents<T>>(event: E, listener: ConfigHandlerEvents<T>[E]): this;
off<E extends keyof ConfigHandlerEvents<T>>(event: E, listener: ConfigHandlerEvents<T>[E]): this;
emit<E extends keyof ConfigHandlerEvents<T>>(event: E, ...args: Parameters<ConfigHandlerEvents<T>[E]>): boolean;
}
export { AlreadyInstalledError, CLOUDFLARED_VERSION, ConfigHandler, type ConfigHandlerEvents, type Connection, ConnectionHandler, DEFAULT_CLOUDFLARED_BIN, MACOS_SERVICE_PATH, NotInstalledError, type OutputHandler, RELEASE_BASE, TryCloudflareHandler, Tunnel, type TunnelConfig, type TunnelEvents, type TunnelOptions, bin, build_args, build_options, identifier, install$1 as install, install_linux, install_macos, install_windows, service, tunnel, use };
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var lib_exports = {};
__export(lib_exports, {
AlreadyInstalledError: () => import_service.AlreadyInstalledError,
MACOS_SERVICE_PATH: () => import_service.MACOS_SERVICE_PATH,
NotInstalledError: () => import_service.NotInstalledError,
identifier: () => import_service.identifier,
service: () => import_service.service
});
module.exports = __toCommonJS(lib_exports);
__reExport(lib_exports, require("./constants.js"), module.exports);
__reExport(lib_exports, require("./install.js"), module.exports);
__reExport(lib_exports, require("./tunnel.js"), module.exports);
var import_service = require("./service.js");
__reExport(lib_exports, require("./handler.js"), module.exports);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AlreadyInstalledError,
MACOS_SERVICE_PATH,
NotInstalledError,
identifier,
service,
...require("./constants.js"),
...require("./install.js"),
...require("./tunnel.js"),
...require("./handler.js")
});
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var regex_exports = {};
__export(regex_exports, {
config_regex: () => config_regex,
conn_regex: () => conn_regex,
connectorID_regex: () => connectorID_regex,
disconnect_regex: () => disconnect_regex,
index_regex: () => index_regex,
ip_regex: () => ip_regex,
location_regex: () => location_regex,
metrics_regex: () => metrics_regex,
tunnelID_regex: () => tunnelID_regex
});
module.exports = __toCommonJS(regex_exports);
const conn_regex = /connection[= ]([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})/i;
const ip_regex = /ip=([0-9.]+)/;
const location_regex = /location=([A-Za-z0-9]+)/;
const index_regex = /connIndex=(\d)/;
const disconnect_regex = /Unregistered tunnel connection connIndex=(\d)/i;
const tunnelID_regex = /tunnelID=([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})/i;
const connectorID_regex = /Connector ID: ([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})/i;
const metrics_regex = /metrics server on ([0-9.:]+\/metrics)/;
const config_regex = /config="(.+[^\\])"/;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
config_regex,
conn_regex,
connectorID_regex,
disconnect_regex,
index_regex,
ip_regex,
location_regex,
metrics_regex,
tunnelID_regex
});
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var service_exports = {};
__export(service_exports, {
AlreadyInstalledError: () => AlreadyInstalledError,
LINUX_SERVICE_PATH: () => LINUX_SERVICE_PATH,
MACOS_SERVICE_PATH: () => MACOS_SERVICE_PATH,
NotInstalledError: () => NotInstalledError,
clean: () => clean,
current: () => current,
err: () => err,
exists: () => exists,
identifier: () => identifier,
install: () => install,
journal: () => journal,
log: () => log,
service: () => service,
service_name: () => service_name,
uninstall: () => uninstall
});
module.exports = __toCommonJS(service_exports);
var import_node_os = __toESM(require("node:os"));
var import_node_fs = __toESM(require("node:fs"));
var import_node_child_process = require("node:child_process");
var import_constants = require("./constants.js");
var import_regex = require("./regex.js");
const identifier = "com.cloudflare.cloudflared";
const service_name = "cloudflared.service";
const MACOS_SERVICE_PATH = {
PLIST: is_root() ? `/Library/LaunchDaemons/${identifier}.plist` : `${import_node_os.default.homedir()}/Library/LaunchAgents/${identifier}.plist`,
OUT: is_root() ? `/Library/Logs/${identifier}.out.log` : `${import_node_os.default.homedir()}/Library/Logs/${identifier}.out.log`,
ERR: is_root() ? `/Library/Logs/${identifier}.err.log` : `${import_node_os.default.homedir()}/Library/Logs/${identifier}.err.log`
};
const LINUX_SERVICE_PATH = {
SYSTEMD: `/etc/systemd/system/${service_name}`,
SERVICE: "/etc/init.d/cloudflared",
SERVICE_OUT: "/var/log/cloudflared.log",
SERVICE_ERR: "/var/log/cloudflared.err"
};
const service = { install, uninstall, exists, log, err, current, clean, journal };
class AlreadyInstalledError extends Error {
constructor() {
super("service is already installed");
}
}
class NotInstalledError extends Error {
constructor() {
super("service is not installed");
}
}
function install(token) {
if (!["darwin", "linux"].includes(process.platform)) {
throw new Error(`Not Implemented on platform ${process.platform}`);
}
if (exists()) {
throw new AlreadyInstalledError();
}
const args = ["service", "install"];
if (token) {
args.push(token);
}
const result = (0, import_node_child_process.spawnSync)(import_constants.bin, args);
if (result.status !== 0) {
throw new Error(`service install failed: ${result.stderr.toString()}`);
}
}
function uninstall() {
if (!["darwin", "linux"].includes(process.platform)) {
throw new Error(`Not Implemented on platform ${process.platform}`);
}
if (!exists()) {
throw new NotInstalledError();
}
const result = (0, import_node_child_process.spawnSync)(import_constants.bin, ["service", "uninstall"]);
if (result.status !== 0) {
throw new Error(`service uninstall failed: ${result.stderr.toString()}`);
}
if (process.platform === "darwin") {
import_node_fs.default.rmSync(MACOS_SERVICE_PATH.OUT);
import_node_fs.default.rmSync(MACOS_SERVICE_PATH.ERR);
} else if (process.platform === "linux" && !is_systemd()) {
import_node_fs.default.rmSync(LINUX_SERVICE_PATH.SERVICE_OUT);
import_node_fs.default.rmSync(LINUX_SERVICE_PATH.SERVICE_ERR);
}
}
function log() {
if (!exists()) {
throw new NotInstalledError();
}
if (process.platform === "darwin") {
return import_node_fs.default.readFileSync(MACOS_SERVICE_PATH.OUT, "utf8");
}
if (process.platform === "linux" && !is_systemd()) {
return import_node_fs.default.readFileSync(LINUX_SERVICE_PATH.SERVICE_OUT, "utf8");
}
throw new Error(`Not Implemented on platform ${process.platform}`);
}
function err() {
if (!exists()) {
throw new NotInstalledError();
}
if (process.platform === "darwin") {
return import_node_fs.default.readFileSync(MACOS_SERVICE_PATH.ERR, "utf8");
}
if (process.platform === "linux" && !is_systemd()) {
return import_node_fs.default.readFileSync(LINUX_SERVICE_PATH.SERVICE_ERR, "utf8");
}
throw new Error(`Not Implemented on platform ${process.platform}`);
}
function journal(n = 300) {
if (process.platform === "linux" && is_systemd()) {
const args = ["-u", service_name, "-o", "cat", "-n", n.toString()];
return (0, import_node_child_process.spawnSync)("journalctl", args).stdout.toString();
}
throw new Error(`Not Implemented on platform ${process.platform}`);
}
function current() {
var _a, _b, _c, _d;
if (!["darwin", "linux"].includes(process.platform)) {
throw new Error(`Not Implemented on platform ${process.platform}`);
}
if (!exists()) {
throw new NotInstalledError();
}
const log2 = is_systemd() ? journal() : err();
let tunnelID = "";
let connectorID = "";
const connections = [];
let metrics = "";
let config = {};
for (const line of log2.split("\n")) {
try {
if (line.match(import_regex.tunnelID_regex)) {
tunnelID = ((_a = line.match(import_regex.tunnelID_regex)) == null ? void 0 : _a[1]) ?? "";
} else if (line.match(import_regex.connectorID_regex)) {
connectorID = ((_b = line.match(import_regex.connectorID_regex)) == null ? void 0 : _b[1]) ?? "";
} else if (line.match(import_regex.conn_regex) && line.match(import_regex.location_regex) && line.match(import_regex.ip_regex) && line.match(import_regex.index_regex)) {
const [, id] = line.match(import_regex.conn_regex) ?? [];
const [, location] = line.match(import_regex.location_regex) ?? [];
const [, ip] = line.match(import_regex.ip_regex) ?? [];
const [, idx] = line.match(import_regex.index_regex) ?? [];
connections[parseInt(idx)] = { id, ip, location };
} else if (line.match(import_regex.disconnect_regex)) {
const [, idx] = line.match(import_regex.disconnect_regex) ?? [];
if (parseInt(idx) in connections) {
connections[parseInt(idx)] = { id: "", ip: "", location: "" };
}
} else if (line.match(import_regex.metrics_regex)) {
metrics = ((_c = line.match(import_regex.metrics_regex)) == null ? void 0 : _c[1]) ?? "";
} else if (line.match(import_regex.config_regex)) {
config = JSON.parse(((_d = line.match(import_regex.config_regex)) == null ? void 0 : _d[1].replace(/\\/g, "")) ?? "{}");
}
} catch (err2) {
if (process.env.VERBOSE) {
console.error("log parsing failed", err2);
}
}
}
return { tunnelID, connectorID, connections, metrics, config };
}
function clean() {
if (process.platform !== "darwin") {
throw new Error(`Not Implemented on platform ${process.platform}`);
}
if (exists()) {
throw new AlreadyInstalledError();
}
import_node_fs.default.rmSync(MACOS_SERVICE_PATH.OUT, { force: true });
import_node_fs.default.rmSync(MACOS_SERVICE_PATH.ERR, { force: true });
}
function exists() {
if (process.platform === "darwin") {
return import_node_fs.default.existsSync(MACOS_SERVICE_PATH.PLIST);
} else if (process.platform === "linux") {
return is_systemd() ? import_node_fs.default.existsSync(LINUX_SERVICE_PATH.SYSTEMD) : import_node_fs.default.existsSync(LINUX_SERVICE_PATH.SERVICE);
}
throw new Error(`Not Implemented on platform ${process.platform}`);
}
function is_root() {
var _a;
return ((_a = process.getuid) == null ? void 0 : _a.call(process)) === 0;
}
function is_systemd() {
return process.platform === "linux" && import_node_fs.default.existsSync("/run/systemd/system");
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AlreadyInstalledError,
LINUX_SERVICE_PATH,
MACOS_SERVICE_PATH,
NotInstalledError,
clean,
current,
err,
exists,
identifier,
install,
journal,
log,
service,
service_name,
uninstall
});
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var tunnel_exports = {};
__export(tunnel_exports, {
Tunnel: () => Tunnel,
build_args: () => build_args,
build_options: () => build_options,
tunnel: () => tunnel
});
module.exports = __toCommonJS(tunnel_exports);
var import_node_child_process = require("node:child_process");
var import_node_events = require("node:events");
var import_constants = require("./constants.js");
var import_handler = require("./handler.js");
class Tunnel extends import_node_events.EventEmitter {
constructor(options = ["tunnel", "--hello-world"]) {
super();
this.outputHandlers = [];
this.stop = this._stop.bind(this);
this.setupDefaultHandlers();
const args = Array.isArray(options) ? options : build_args(options);
this._process = this.createProcess(args);
this.setupEventHandlers();
}
get process() {
return this._process;
}
setupDefaultHandlers() {
new import_handler.ConnectionHandler(this);
new import_handler.TryCloudflareHandler(this);
}
/**
* Add a custom output handler
* @param handler Function to handle cloudflared output
*/
addHandler(handler) {
this.outputHandlers.push(handler);
}
/**
* Remove a previously added output handler
* @param handler The handler to remove
*/
removeHandler(handler) {
const index = this.outputHandlers.indexOf(handler);
if (index !== -1) {
this.outputHandlers.splice(index, 1);
}
}
processOutput(output) {
for (const handler of this.outputHandlers) {
try {
handler(output, this);
} catch (error) {
this.emit("error", error instanceof Error ? error : new Error(String(error)));
}
}
}
setupEventHandlers() {
this.on("stdout", (output) => {
this.processOutput(output);
});
this.on("stderr", (output) => {
this.processOutput(output);
});
}
createProcess(args) {
var _a, _b;
const child = (0, import_node_child_process.spawn)(import_constants.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
child.on("error", (error) => this.emit("error", error));
child.on("exit", (code, signal) => this.emit("exit", code, signal));
if (process.env.VERBOSE) {
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
}
(_a = child.stdout) == null ? void 0 : _a.on("data", (data) => this.emit("stdout", data.toString()));
(_b = child.stderr) == null ? void 0 : _b.on("data", (data) => this.emit("stderr", data.toString()));
return child;
}
_stop() {
return this.process.kill("SIGINT");
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
off(event, listener) {
return super.off(event, listener);
}
emit(event, ...args) {
return super.emit(event, ...args);
}
/**
* Create a quick tunnel without a Cloudflare account.
* @param url The local service URL to connect to. If not provided, the hello world mode will be used.
* @param options The options to pass to cloudflared.
*/
static quick(url, options = {}) {
const args = ["tunnel"];
if (url) {
args.push("--url", url);
} else {
args.push("--hello-world");
}
args.push(...build_options(options));
return new Tunnel(args);
}
/**
* Create a tunnel with a Cloudflare account.
* @param token The Cloudflare Tunnel token.
* @param options The options to pass to cloudflared.
*/
static withToken(token, options = {}) {
options["--token"] = token;
return new Tunnel(build_args(options));
}
}
function tunnel(options = {}) {
return new Tunnel(options);
}
function build_args(options) {
const args = "--hello-world" in options ? ["tunnel"] : ["tunnel", "run"];
args.push(...build_options(options));
return args;
}
function build_options(options) {
const opts = [];
for (const [key, value] of Object.entries(options)) {
if (typeof value === "string") {
opts.push(`${key}`, value);
} else if (typeof value === "number") {
opts.push(`${key}`, value.toString());
} else if (typeof value === "boolean") {
if (value === true) {
opts.push(`${key}`);
}
}
}
return opts;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Tunnel,
build_args,
build_options,
tunnel
});
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var types_exports = {};
module.exports = __toCommonJS(types_exports);
MIT License
Copyright (c) 2022 JacobLinCool
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "cloudflared",
"version": "0.7.1",
"description": "Cloudflared in Node. Which allows you to easily create HTTPS tunnels using Cloudflare's cloudflared. It provides a typed API for creating tunnels and managing the cloudflared binary installation.",
"main": "./lib/lib.js",
"types": "./lib/lib.d.ts",
"bin": "./lib/cloudflared.js",
"keywords": [
"cloudflared",
"tunnel",
"macos",
"windows",
"linux",
"cli",
"lib"
],
"author": "JacobLinCool <jacoblincool@gmail.com> (https://github.com/JacobLinCool)",
"license": "MIT",
"files": [
"lib",
"scripts"
],
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.7",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0",
"@types/node": "^22.1.0",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"changeset": "^0.2.6",
"eslint": "^9.8.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"typedoc": "^0.26.5",
"typescript": "^5.5.4",
"vitest": "^2.0.5",
"cloudflared": "0.7.1"
},
"homepage": "https://github.com/JacobLinCool/node-cloudflared#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/JacobLinCool/node-cloudflared.git"
},
"bugs": {
"url": "https://github.com/JacobLinCool/node-cloudflared/issues"
},
"scripts": {
"test": "vitest",
"dev": "tsup --watch",
"build": "tsup",
"docs": "typedoc ./src/lib.ts",
"format": "prettier --write '**/*.{js,ts,jsx,tsx,json,yml,yaml,md,html}' --ignore-path .gitignore",
"lint": "eslint .",
"postinstall": "node scripts/postinstall.mjs && node lib/cloudflared.js bin install latest",
"changeset": "changeset"
}
}
# cloudflared
A Node.js package that allows you to easily create HTTPS tunnels using Cloudflare's `cloudflared` command-line tool. It provides a typed API for creating tunnels and managing the `cloudflared` binary installation.
> This tool will automatically install the [latest version of `cloudflared`](https://github.com/cloudflare/cloudflared/releases/latest) (or `CLOUDFLARED_VERSION` env var if exists) at the first time.
> Then, it just passes down the command to `cloudflared`.
## Installation
You can install this package using your favorite package manager:
### PNPM
```sh
pnpm i -g cloudflared
```
### NPM
```sh
npm i -g cloudflared
```
### Yarn
```sh
yarn global add cloudflared
```
> If `CLOUDFLARED_VERSION` env var is set, it will install the specified version of `cloudflared`, otherwise it will install the latest version.
## CLI Usage
You can use the `cloudflared` command-line tool to create HTTPS tunnels. You can find the usage of `cloudflared` [here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-useful-commands/).
In addition to the standard `cloudflared` commands, this package also provides an extra subcommand: `cloudflared bin`. You can use it to manage the `cloudflared` binary version.
```sh
❯ cloudflared bin --help
cloudflared bin : Prints the path to the binary
cloudflared bin remove : Removes the binary
cloudflared bin install [version] : Installs the binary
cloudflared bin list : Lists 30 latest releases
cloudflared bin help : Prints this help message
Examples:
cloudflared bin install : Installs the latest version of cloudflared
cloudflared bin install 2023.4.1 : Installs cloudflared 2023.4.1
You can find releases at https://github.com/cloudflare/cloudflared/releases
```
## Library Usage
You can also use it as a library in your TypeScript / JavaScript projects.
### Binary Path & Install
You can get the path of the `cloudflared` binary and install it using the `bin` and `install` functions, respectively.
```js
import { bin, install } from "cloudflared";
import fs from "node:fs";
import { spawn } from "node:child_process";
if (!fs.existsSync(bin)) {
// install cloudflared binary
await install(bin);
}
// run cloudflared
spawn(bin, ["--version"], { stdio: "inherit" });
```
- `bin`: The path of the binary.
- `install`: A function that installs the binary to the given path.
### Tunnel
Checkout [`examples/tunnel.js`](examples/tunnel.js).
`Tunnel` is inherited from `EventEmitter`, so you can listen to the events it emits, checkout [`examples/events.mjs`](examples/events.mjs).
```js
import { Tunnel } from "cloudflared";
console.log("Cloudflared Tunnel Example.");
main();
async function main() {
// run: cloudflared tunnel --hello-world
const tunnel = Tunnel.quick();
// show the url
const url = new Promise((resolve) => tunnel.once("url", resolve));
console.log("LINK:", await url);
// wait for connection to be established
const conn = new Promise((resolve) => tunnel.once("connected", resolve));
console.log("CONN:", await conn);
// stop the tunnel after 15 seconds
setTimeout(tunnel.stop, 15_000);
tunnel.on("exit", (code) => {
console.log("tunnel process exited with code", code);
});
}
```
```sh
❯ node examples/tunnel.js
Cloudflared Tunnel Example.
LINK: https://mailto-davis-wilderness-facts.trycloudflare.com
CONN: {
id: 'df1b8330-44ea-4ecb-bb93-8a32400f6d1c',
ip: '198.41.200.193',
location: 'tpe01'
}
tunnel process exited with code 0
```
### Service
Checkout [`examples/service.js`](examples/service.js).
```js
import { service } from "cloudflared";
console.log("Cloudflared Service Example.");
main();
async function main() {
if (service.exists()) {
console.log("Service is running.");
const current = service.current();
for (const { service, hostname } of current.config.ingress) {
console.log(` - ${service} -> ${hostname}`);
}
console.log("metrics server:", current.metrics);
} else {
console.log("Service is not running.");
}
}
```
```sh
❯ node examples/service.js
Cloudflared Service Example.
Service is running.
- http://localhost:12345 -> sub.example.com
- http_status:404 -> undefined
metrics server: 127.0.0.1:49177/metrics
```
NOTICE: On linux, service can only be installed and uninstalled by root.
Run service test on linux: `sudo -E env "PATH=$PATH" pnpm test`
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
if (!fs.existsSync(path.resolve(__dirname, "..", "lib"))) {
execSync("npm run build");
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: !0 });
class ParseError extends Error {
constructor(message, options) {
super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
}
}
const LF = 10, CR = 13, SPACE = 32;
function noop(_arg) {
}
function createParser(callbacks) {
if (typeof callbacks == "function")
throw new TypeError(
"`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?"
);
const { onEvent = noop, onError = noop, onRetry = noop, onComment } = callbacks, pendingFragments = [];
let isFirstChunk = !0, id, data = "", dataLines = 0, eventType;
function feed(chunk) {
if (isFirstChunk && (isFirstChunk = !1, chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191 && (chunk = chunk.slice(3))), pendingFragments.length === 0) {
const trailing2 = processLines(chunk);
trailing2 !== "" && pendingFragments.push(trailing2);
return;
}
if (chunk.indexOf(`
`) === -1 && chunk.indexOf("\r") === -1) {
pendingFragments.push(chunk);
return;
}
pendingFragments.push(chunk);
const input = pendingFragments.join("");
pendingFragments.length = 0;
const trailing = processLines(input);
trailing !== "" && pendingFragments.push(trailing);
}
function processLines(chunk) {
let searchIndex = 0;
if (chunk.indexOf("\r") === -1) {
let lfIndex = chunk.indexOf(`
`, searchIndex);
for (; lfIndex !== -1; ) {
if (searchIndex === lfIndex) {
dataLines > 0 && onEvent({ id, event: eventType, data }), id = void 0, data = "", dataLines = 0, eventType = void 0, searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
`, searchIndex);
continue;
}
const firstCharCode = chunk.charCodeAt(searchIndex);
if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5, value = chunk.slice(valueStart, lfIndex);
if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
onEvent({ id, event: eventType, data: value }), id = void 0, data = "", eventType = void 0, searchIndex = lfIndex + 2, lfIndex = chunk.indexOf(`
`, searchIndex);
continue;
}
data = dataLines === 0 ? value : `${data}
${value}`, dataLines++;
} else isEventPrefix(chunk, searchIndex, firstCharCode) ? eventType = chunk.slice(
chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,
lfIndex
) || void 0 : parseLine(chunk, searchIndex, lfIndex);
searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
`, searchIndex);
}
return chunk.slice(searchIndex);
}
for (; searchIndex < chunk.length; ) {
const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
`, searchIndex);
let lineEnd = -1;
if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = crIndex < lfIndex ? crIndex : lfIndex : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1)
break;
parseLine(chunk, searchIndex, lineEnd), searchIndex = lineEnd + 1, chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF && searchIndex++;
}
return chunk.slice(searchIndex);
}
function parseLine(chunk, start, end) {
if (start === end) {
dispatchEvent();
return;
}
const firstCharCode = chunk.charCodeAt(start);
if (isDataPrefix(chunk, start, firstCharCode)) {
const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5, value2 = chunk.slice(valueStart, end);
data = dataLines === 0 ? value2 : `${data}
${value2}`, dataLines++;
return;
}
if (isEventPrefix(chunk, start, firstCharCode)) {
eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
return;
}
if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
id = value2.includes("\0") ? void 0 : value2;
return;
}
if (firstCharCode === 58) {
if (onComment) {
const line2 = chunk.slice(start, end);
onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
}
return;
}
const line = chunk.slice(start, end), fieldSeparatorIndex = line.indexOf(":");
if (fieldSeparatorIndex === -1) {
processField(line, "", line);
return;
}
const field = line.slice(0, fieldSeparatorIndex), offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset);
processField(field, value, line);
}
function processField(field, value, line) {
switch (field) {
case "event":
eventType = value || void 0;
break;
case "data":
data = dataLines === 0 ? value : `${data}
${value}`, dataLines++;
break;
case "id":
id = value.includes("\0") ? void 0 : value;
break;
case "retry":
/^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(
new ParseError(`Invalid \`retry\` value: "${value}"`, {
type: "invalid-retry",
value,
line
})
);
break;
default:
onError(
new ParseError(
`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`,
{ type: "unknown-field", field, value, line }
)
);
break;
}
}
function dispatchEvent() {
dataLines > 0 && onEvent({
id,
event: eventType,
data
}), id = void 0, data = "", dataLines = 0, eventType = void 0;
}
function reset(options = {}) {
if (options.consume && pendingFragments.length > 0) {
const incompleteLine = pendingFragments.join("");
parseLine(incompleteLine, 0, incompleteLine.length);
}
isFirstChunk = !0, id = void 0, data = "", dataLines = 0, eventType = void 0, pendingFragments.length = 0;
}
return { feed, reset };
}
function isDataPrefix(chunk, i, firstCharCode) {
return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
}
function isEventPrefix(chunk, i, firstCharCode) {
return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
}
exports.ParseError = ParseError;
exports.createParser = createParser;
//# sourceMappingURL=index.cjs.map
{"version":3,"file":"index.cjs","sources":["../src/errors.ts","../src/parse.ts"],"sourcesContent":["/**\n * The type of error that occurred.\n * @public\n */\nexport type ErrorType = 'invalid-retry' | 'unknown-field'\n\n/**\n * Error thrown when encountering an issue during parsing.\n *\n * @public\n */\nexport class ParseError extends Error {\n /**\n * The type of error that occurred.\n */\n type: ErrorType\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the field name.\n */\n field?: string | undefined\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the value of the field.\n */\n value?: string | undefined\n\n /**\n * The line that caused the error, if available.\n */\n line?: string | undefined\n\n constructor(\n message: string,\n options: {type: ErrorType; field?: string; value?: string; line?: string},\n ) {\n super(message)\n this.name = 'ParseError'\n this.type = options.type\n this.field = options.field\n this.value = options.value\n this.line = options.line\n }\n}\n","/**\n * EventSource/Server-Sent Events parser\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html\n */\nimport {ParseError} from './errors.ts'\nimport type {EventSourceParser, ParserCallbacks} from './types.ts'\n\n// ASCII codes used in the hot parsing paths.\nconst LF = 10\nconst CR = 13\nconst SPACE = 32\n\n// oxlint-disable-next-line no-unused-vars\nfunction noop(_arg: unknown) {\n // intentional noop\n}\n\n/**\n * Creates a new EventSource parser.\n *\n * @param callbacks - Callbacks to invoke on different parsing events:\n * - `onEvent` when a new event is parsed\n * - `onError` when an error occurs\n * - `onRetry` when a new reconnection interval has been sent from the server\n * - `onComment` when a comment is encountered in the stream\n *\n * @returns A new EventSource parser, with `parse` and `reset` methods.\n * @public\n */\nexport function createParser(callbacks: ParserCallbacks): EventSourceParser {\n if (typeof callbacks === 'function') {\n throw new TypeError(\n '`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?',\n )\n }\n\n const {onEvent = noop, onError = noop, onRetry = noop, onComment} = callbacks\n\n // Trailing bytes from prior `feed()` calls that did not yet form a complete line.\n // Stored as an array of fragments and only joined when a line terminator arrives.\n // Concatenating per-feed (`prefix + chunk`) is O(N²) when a single SSE line spans\n // many chunks (e.g. a large `data:` payload streamed in tiny slices, or an MCP-style\n // server that emits one giant content block). Buffering as fragments + joining once\n // makes the same workload linear.\n const pendingFragments: string[] = []\n\n let isFirstChunk = true\n let id: string | undefined\n let data = ''\n let dataLines = 0\n let eventType: string | undefined\n\n /**\n * Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do\n * not yet form a complete line are held back and prepended to the next chunk,\n * so callers can pass arbitrary slices of the stream without worrying about\n * line boundaries.\n *\n * Per the SSE spec, a UTF-8 BOM (0xEF 0xBB 0xBF) at the start of the very\n * first chunk is stripped before parsing.\n *\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream\n */\n function feed(chunk: string) {\n if (isFirstChunk) {\n isFirstChunk = false\n // Match and strip UTF-8 BOM from the start of the stream, if present.\n // (Per the spec, this is only valid at the very start of the stream)\n if (\n chunk.charCodeAt(0) === 0xef &&\n chunk.charCodeAt(1) === 0xbb &&\n chunk.charCodeAt(2) === 0xbf\n ) {\n chunk = chunk.slice(3)\n }\n }\n\n // Hot path: no buffered prefix from a prior partial line. Hand the chunk\n // straight to `processLines`, exactly like the original implementation.\n // Zero new work in the common case (every chunk ends with `\\n\\n`).\n if (pendingFragments.length === 0) {\n const trailing = processLines(chunk)\n if (trailing !== '') pendingFragments.push(trailing)\n return\n }\n\n // We have a buffered prefix. If this chunk also has no terminator, append\n // to the buffer without concatenating — that's the O(N²) trap we're\n // avoiding (large single `data:` payload split across many tiny chunks).\n if (chunk.indexOf('\\n') === -1 && chunk.indexOf('\\r') === -1) {\n pendingFragments.push(chunk)\n return\n }\n\n // Terminator arrived. Join the accumulated fragments + this chunk once,\n // process, and buffer any new trailing partial line.\n pendingFragments.push(chunk)\n const input = pendingFragments.join('')\n pendingFragments.length = 0\n const trailing = processLines(input)\n if (trailing !== '') pendingFragments.push(trailing)\n }\n\n /**\n * Splits `chunk` into SSE lines and dispatches each to the appropriate handler.\n * Returns any trailing bytes that did not terminate with a line break, so the\n * caller can prepend them to the next chunk.\n *\n * The SSE spec permits three line terminators: `\\n`, `\\r`, and `\\r\\n`. Real-world\n * streams almost always use plain `\\n`, so we take a fast path when no `\\r` is\n * present in the chunk. The slow path is spec-correct but does more work per line.\n */\n function processLines(chunk: string): string {\n let searchIndex = 0\n\n // Fast path: LF-only chunk (the common case for typical SSE servers).\n // We can scan forward with a single `indexOf('\\n')` per line and inline\n // the hot-path branches for `data:` and `event:` without the CR bookkeeping\n // the slow path needs.\n if (chunk.indexOf('\\r') === -1) {\n let lfIndex = chunk.indexOf('\\n', searchIndex)\n while (lfIndex !== -1) {\n // Blank line: end-of-event marker. Dispatch the accumulated event (if any)\n // and reset the buffered fields. This is hoisted out of `parseLine` because\n // it's the single most common line shape after `data:` lines.\n if (searchIndex === lfIndex) {\n if (dataLines > 0) {\n onEvent({id, event: eventType, data})\n }\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n const firstCharCode = chunk.charCodeAt(searchIndex)\n if (isDataPrefix(chunk, searchIndex, firstCharCode)) {\n // `data:` line — append the value to the event's data buffer.\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart =\n chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5\n const value = chunk.slice(valueStart, lfIndex)\n // Fast path within a fast path: if this is the first data line AND the\n // next char is another LF (i.e. `data:foo\\n\\n`), dispatch immediately\n // without ever writing to the `data` buffer. This is the shape of a\n // typical single-line SSE event (ChatGPT-style streams, etc.) and is\n // hot enough to be worth the duplication.\n if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {\n onEvent({id, event: eventType, data: value})\n id = undefined\n data = ''\n eventType = undefined\n searchIndex = lfIndex + 2\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n // Multi-line data: concatenate with newline separator per spec.\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {\n // `event:` line — set the event type for the next dispatch. Per spec,\n // an empty value resets `event type` to its default (undefined here).\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(\n chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,\n lfIndex,\n ) || undefined\n } else {\n // Everything else: `id:`, `retry:`, comment lines (`:` prefix), unknown\n // fields, or malformed lines. These are rarer and go through the full\n // per-line parser, which handles the SSE field grammar in detail.\n parseLine(chunk, searchIndex, lfIndex)\n }\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n }\n return chunk.slice(searchIndex)\n }\n\n // Slow path: the chunk contains at least one `\\r`, so lines may be terminated\n // by `\\r`, `\\n`, or `\\r\\n`. We locate the next terminator by looking at both\n // the nearest `\\r` and `\\n` and picking whichever comes first.\n while (searchIndex < chunk.length) {\n const crIndex = chunk.indexOf('\\r', searchIndex)\n const lfIndex = chunk.indexOf('\\n', searchIndex)\n\n let lineEnd = -1\n if (crIndex !== -1 && lfIndex !== -1) {\n lineEnd = crIndex < lfIndex ? crIndex : lfIndex\n } else if (crIndex !== -1) {\n // A trailing `\\r` at the very end of the chunk is ambiguous: it could be\n // a bare-CR terminator, or the first half of a `\\r\\n` whose `\\n` arrives\n // in the next chunk. Defer until we see more input.\n if (crIndex === chunk.length - 1) {\n lineEnd = -1\n } else {\n lineEnd = crIndex\n }\n } else if (lfIndex !== -1) {\n lineEnd = lfIndex\n }\n\n if (lineEnd === -1) {\n break\n }\n\n parseLine(chunk, searchIndex, lineEnd)\n searchIndex = lineEnd + 1\n // If we just consumed a `\\r` and the next char is `\\n`, skip it so the\n // pair is treated as a single terminator rather than an empty line.\n if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {\n searchIndex++\n }\n }\n\n return chunk.slice(searchIndex)\n }\n\n function parseLine(chunk: string, start: number, end: number) {\n if (start === end) {\n dispatchEvent()\n return\n }\n\n const firstCharCode = chunk.charCodeAt(start)\n\n if (isDataPrefix(chunk, start, firstCharCode)) {\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5\n const value = chunk.slice(valueStart, end)\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n return\n }\n\n if (isEventPrefix(chunk, start, firstCharCode)) {\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || undefined\n return\n }\n\n // Fast path for \"id:\" — 'i' = 105, 'd' = 100, ':' = 58\n if (\n firstCharCode === 105 &&\n chunk.charCodeAt(start + 1) === 100 &&\n chunk.charCodeAt(start + 2) === 58\n ) {\n // 'id:'.length === 3, 'id: '.length === 4\n const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end)\n id = value.includes('\\0') ? undefined : value\n return\n }\n\n // Comment line — ':' = 58\n if (firstCharCode === 58) {\n if (onComment) {\n const line = chunk.slice(start, end)\n // skip ':' (+1), or ': ' (+2) when a space follows\n onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1))\n }\n return\n }\n\n const line = chunk.slice(start, end)\n const fieldSeparatorIndex = line.indexOf(':')\n if (fieldSeparatorIndex === -1) {\n processField(line, '', line)\n return\n }\n\n const field = line.slice(0, fieldSeparatorIndex)\n // skip ':' (+1), or ': ' (+2) when a space follows\n const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1\n const value = line.slice(fieldSeparatorIndex + offset)\n processField(field, value, line)\n }\n\n function processField(field: string, value: string, line: string) {\n // Field names must be compared literally, with no case folding performed.\n switch (field) {\n case 'event':\n // Set the `event type` buffer to field value\n eventType = value || undefined\n break\n case 'data':\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n break\n case 'id':\n // If the field value does not contain U+0000 NULL, then set the `ID` buffer to\n // the field value. Otherwise, ignore the field.\n id = value.includes('\\0') ? undefined : value\n break\n case 'retry':\n // If the field value consists of only ASCII digits, then interpret the field value as an\n // integer in base ten, and set the event stream's reconnection time to that integer.\n // Otherwise, ignore the field.\n if (/^\\d+$/.test(value)) {\n onRetry(parseInt(value, 10))\n } else {\n onError(\n new ParseError(`Invalid \\`retry\\` value: \"${value}\"`, {\n type: 'invalid-retry',\n value,\n line,\n }),\n )\n }\n break\n default:\n // Otherwise, the field is ignored.\n onError(\n new ParseError(\n `Unknown field \"${field.length > 20 ? `${field.slice(0, 20)}…` : field}\"`,\n {type: 'unknown-field', field, value, line},\n ),\n )\n break\n }\n }\n\n function dispatchEvent() {\n if (dataLines > 0) {\n onEvent({\n id,\n event: eventType,\n data,\n })\n }\n\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n }\n\n function reset(options: {consume?: boolean} = {}) {\n if (options.consume && pendingFragments.length > 0) {\n const incompleteLine = pendingFragments.join('')\n parseLine(incompleteLine, 0, incompleteLine.length)\n }\n\n isFirstChunk = true\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n pendingFragments.length = 0\n }\n\n return {feed, reset}\n}\n\n/**\n * Checks if `chunk` starts with the literal `data:` at index `i`.\n *\n * Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this\n * hand-unrolled char-code comparison is ~20% faster on common event types.\n * The caller passes `firstCharCode` (the code at `i`) so it can be reused\n * across prefix checks.\n *\n * ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58\n */\nfunction isDataPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 100 &&\n chunk.charCodeAt(i + 1) === 97 &&\n chunk.charCodeAt(i + 2) === 116 &&\n chunk.charCodeAt(i + 3) === 97 &&\n chunk.charCodeAt(i + 4) === 58\n )\n}\n\n/**\n * Checks if `chunk` starts with the literal `event:` at index `i`.\n *\n * See {@link isDataPrefix} for why this is hand-unrolled rather than using\n * `String.prototype.startsWith`.\n *\n * ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58\n */\nfunction isEventPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 101 &&\n chunk.charCodeAt(i + 1) === 118 &&\n chunk.charCodeAt(i + 2) === 101 &&\n chunk.charCodeAt(i + 3) === 110 &&\n chunk.charCodeAt(i + 4) === 116 &&\n chunk.charCodeAt(i + 5) === 58\n )\n}\n"],"names":["trailing","value","line"],"mappings":"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"}
/**
* Creates a new EventSource parser.
*
* @param callbacks - Callbacks to invoke on different parsing events:
* - `onEvent` when a new event is parsed
* - `onError` when an error occurs
* - `onRetry` when a new reconnection interval has been sent from the server
* - `onComment` when a comment is encountered in the stream
*
* @returns A new EventSource parser, with `parse` and `reset` methods.
* @public
*/
export declare function createParser(
callbacks: ParserCallbacks,
): EventSourceParser;
/**
* The type of error that occurred.
* @public
*/
export declare type ErrorType = "invalid-retry" | "unknown-field";
/**
* A parsed EventSource message event
*
* @public
*/
export declare interface EventSourceMessage {
/**
* The event type sent from the server. Note that this differs from the browser `EventSource`
* implementation in that browsers will default this to `message`, whereas this parser will
* leave this as `undefined` if not explicitly declared.
*/
event?: string | undefined;
/**
* ID of the message, if any was provided by the server. Can be used by clients to keep the
* last received message ID in sync when reconnecting.
*/
id?: string | undefined;
/**
* The data received for this message
*/
data: string;
}
/**
* EventSource parser instance.
*
* Needs to be reset between reconnections/when switching data source, using the `reset()` method.
*
* @public
*/
export declare interface EventSourceParser {
/**
* Feeds the parser another chunk. The method _does not_ return a parsed message.
* Instead, callbacks passed when creating the parser will be triggered once we see enough data
* for a valid/invalid parsing step (see {@link ParserCallbacks}).
*
* @param chunk - The chunk to parse. Can be a partial, eg in the case of streaming messages.
* @public
*/
feed(chunk: string): void;
/**
* Resets the parser state. This is required when you have a new stream of messages -
* for instance in the case of a client being disconnected and reconnecting.
*
* Previously received, incomplete data will NOT be parsed unless you pass `consume: true`,
* which tells the parser to attempt to consume any incomplete data as if it ended with a newline
* character. This is useful for cases when a server sends a non-EventSource message that you
* want to be able to react to in an `onError` callback.
*
* @public
*/
reset(options?: { consume?: boolean }): void;
}
/**
* Error thrown when encountering an issue during parsing.
*
* @public
*/
export declare class ParseError extends Error {
/**
* The type of error that occurred.
*/
type: ErrorType;
/**
* In the case of an unknown field encountered in the stream, this will be the field name.
*/
field?: string | undefined;
/**
* In the case of an unknown field encountered in the stream, this will be the value of the field.
*/
value?: string | undefined;
/**
* The line that caused the error, if available.
*/
line?: string | undefined;
constructor(
message: string,
options: {
type: ErrorType;
field?: string;
value?: string;
line?: string;
},
);
}
/**
* Callbacks that can be passed to the parser to handle different types of parsed messages
* and errors.
*
* @public
*/
export declare interface ParserCallbacks {
/**
* Callback for when a new event/message is parsed from the stream.
* This is the main callback that clients will use to handle incoming messages.
*
* @param event - The parsed event/message
*/
onEvent?: ((event: EventSourceMessage) => void) | undefined;
/**
* Callback for when the server sends a new reconnection interval through the `retry` field.
*
* @param retry - The number of milliseconds to wait before reconnecting.
*/
onRetry?: ((retry: number) => void) | undefined;
/**
* Callback for when a comment is encountered in the stream.
*
* @param comment - The comment encountered in the stream.
*/
onComment?: ((comment: string) => void) | undefined;
/**
* Callback for when an error occurs during parsing. This is a catch-all for any errors
* that occur during parsing, and can be used to handle them in a custom way. Most clients
* tend to silently ignore any errors and instead retry, but it can be helpful to log/debug.
*
* @param error - The error that occurred during parsing
*/
onError?: ((error: ParseError) => void) | undefined;
}
export {};
/**
* Creates a new EventSource parser.
*
* @param callbacks - Callbacks to invoke on different parsing events:
* - `onEvent` when a new event is parsed
* - `onError` when an error occurs
* - `onRetry` when a new reconnection interval has been sent from the server
* - `onComment` when a comment is encountered in the stream
*
* @returns A new EventSource parser, with `parse` and `reset` methods.
* @public
*/
export declare function createParser(
callbacks: ParserCallbacks,
): EventSourceParser;
/**
* The type of error that occurred.
* @public
*/
export declare type ErrorType = "invalid-retry" | "unknown-field";
/**
* A parsed EventSource message event
*
* @public
*/
export declare interface EventSourceMessage {
/**
* The event type sent from the server. Note that this differs from the browser `EventSource`
* implementation in that browsers will default this to `message`, whereas this parser will
* leave this as `undefined` if not explicitly declared.
*/
event?: string | undefined;
/**
* ID of the message, if any was provided by the server. Can be used by clients to keep the
* last received message ID in sync when reconnecting.
*/
id?: string | undefined;
/**
* The data received for this message
*/
data: string;
}
/**
* EventSource parser instance.
*
* Needs to be reset between reconnections/when switching data source, using the `reset()` method.
*
* @public
*/
export declare interface EventSourceParser {
/**
* Feeds the parser another chunk. The method _does not_ return a parsed message.
* Instead, callbacks passed when creating the parser will be triggered once we see enough data
* for a valid/invalid parsing step (see {@link ParserCallbacks}).
*
* @param chunk - The chunk to parse. Can be a partial, eg in the case of streaming messages.
* @public
*/
feed(chunk: string): void;
/**
* Resets the parser state. This is required when you have a new stream of messages -
* for instance in the case of a client being disconnected and reconnecting.
*
* Previously received, incomplete data will NOT be parsed unless you pass `consume: true`,
* which tells the parser to attempt to consume any incomplete data as if it ended with a newline
* character. This is useful for cases when a server sends a non-EventSource message that you
* want to be able to react to in an `onError` callback.
*
* @public
*/
reset(options?: { consume?: boolean }): void;
}
/**
* Error thrown when encountering an issue during parsing.
*
* @public
*/
export declare class ParseError extends Error {
/**
* The type of error that occurred.
*/
type: ErrorType;
/**
* In the case of an unknown field encountered in the stream, this will be the field name.
*/
field?: string | undefined;
/**
* In the case of an unknown field encountered in the stream, this will be the value of the field.
*/
value?: string | undefined;
/**
* The line that caused the error, if available.
*/
line?: string | undefined;
constructor(
message: string,
options: {
type: ErrorType;
field?: string;
value?: string;
line?: string;
},
);
}
/**
* Callbacks that can be passed to the parser to handle different types of parsed messages
* and errors.
*
* @public
*/
export declare interface ParserCallbacks {
/**
* Callback for when a new event/message is parsed from the stream.
* This is the main callback that clients will use to handle incoming messages.
*
* @param event - The parsed event/message
*/
onEvent?: ((event: EventSourceMessage) => void) | undefined;
/**
* Callback for when the server sends a new reconnection interval through the `retry` field.
*
* @param retry - The number of milliseconds to wait before reconnecting.
*/
onRetry?: ((retry: number) => void) | undefined;
/**
* Callback for when a comment is encountered in the stream.
*
* @param comment - The comment encountered in the stream.
*/
onComment?: ((comment: string) => void) | undefined;
/**
* Callback for when an error occurs during parsing. This is a catch-all for any errors
* that occur during parsing, and can be used to handle them in a custom way. Most clients
* tend to silently ignore any errors and instead retry, but it can be helpful to log/debug.
*
* @param error - The error that occurred during parsing
*/
onError?: ((error: ParseError) => void) | undefined;
}
export {};
class ParseError extends Error {
constructor(message, options) {
super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
}
}
const LF = 10, CR = 13, SPACE = 32;
function noop(_arg) {
}
function createParser(callbacks) {
if (typeof callbacks == "function")
throw new TypeError(
"`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?"
);
const { onEvent = noop, onError = noop, onRetry = noop, onComment } = callbacks, pendingFragments = [];
let isFirstChunk = !0, id, data = "", dataLines = 0, eventType;
function feed(chunk) {
if (isFirstChunk && (isFirstChunk = !1, chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191 && (chunk = chunk.slice(3))), pendingFragments.length === 0) {
const trailing2 = processLines(chunk);
trailing2 !== "" && pendingFragments.push(trailing2);
return;
}
if (chunk.indexOf(`
`) === -1 && chunk.indexOf("\r") === -1) {
pendingFragments.push(chunk);
return;
}
pendingFragments.push(chunk);
const input = pendingFragments.join("");
pendingFragments.length = 0;
const trailing = processLines(input);
trailing !== "" && pendingFragments.push(trailing);
}
function processLines(chunk) {
let searchIndex = 0;
if (chunk.indexOf("\r") === -1) {
let lfIndex = chunk.indexOf(`
`, searchIndex);
for (; lfIndex !== -1; ) {
if (searchIndex === lfIndex) {
dataLines > 0 && onEvent({ id, event: eventType, data }), id = void 0, data = "", dataLines = 0, eventType = void 0, searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
`, searchIndex);
continue;
}
const firstCharCode = chunk.charCodeAt(searchIndex);
if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5, value = chunk.slice(valueStart, lfIndex);
if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
onEvent({ id, event: eventType, data: value }), id = void 0, data = "", eventType = void 0, searchIndex = lfIndex + 2, lfIndex = chunk.indexOf(`
`, searchIndex);
continue;
}
data = dataLines === 0 ? value : `${data}
${value}`, dataLines++;
} else isEventPrefix(chunk, searchIndex, firstCharCode) ? eventType = chunk.slice(
chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,
lfIndex
) || void 0 : parseLine(chunk, searchIndex, lfIndex);
searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
`, searchIndex);
}
return chunk.slice(searchIndex);
}
for (; searchIndex < chunk.length; ) {
const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
`, searchIndex);
let lineEnd = -1;
if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = crIndex < lfIndex ? crIndex : lfIndex : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1)
break;
parseLine(chunk, searchIndex, lineEnd), searchIndex = lineEnd + 1, chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF && searchIndex++;
}
return chunk.slice(searchIndex);
}
function parseLine(chunk, start, end) {
if (start === end) {
dispatchEvent();
return;
}
const firstCharCode = chunk.charCodeAt(start);
if (isDataPrefix(chunk, start, firstCharCode)) {
const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5, value2 = chunk.slice(valueStart, end);
data = dataLines === 0 ? value2 : `${data}
${value2}`, dataLines++;
return;
}
if (isEventPrefix(chunk, start, firstCharCode)) {
eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
return;
}
if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
id = value2.includes("\0") ? void 0 : value2;
return;
}
if (firstCharCode === 58) {
if (onComment) {
const line2 = chunk.slice(start, end);
onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
}
return;
}
const line = chunk.slice(start, end), fieldSeparatorIndex = line.indexOf(":");
if (fieldSeparatorIndex === -1) {
processField(line, "", line);
return;
}
const field = line.slice(0, fieldSeparatorIndex), offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset);
processField(field, value, line);
}
function processField(field, value, line) {
switch (field) {
case "event":
eventType = value || void 0;
break;
case "data":
data = dataLines === 0 ? value : `${data}
${value}`, dataLines++;
break;
case "id":
id = value.includes("\0") ? void 0 : value;
break;
case "retry":
/^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(
new ParseError(`Invalid \`retry\` value: "${value}"`, {
type: "invalid-retry",
value,
line
})
);
break;
default:
onError(
new ParseError(
`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`,
{ type: "unknown-field", field, value, line }
)
);
break;
}
}
function dispatchEvent() {
dataLines > 0 && onEvent({
id,
event: eventType,
data
}), id = void 0, data = "", dataLines = 0, eventType = void 0;
}
function reset(options = {}) {
if (options.consume && pendingFragments.length > 0) {
const incompleteLine = pendingFragments.join("");
parseLine(incompleteLine, 0, incompleteLine.length);
}
isFirstChunk = !0, id = void 0, data = "", dataLines = 0, eventType = void 0, pendingFragments.length = 0;
}
return { feed, reset };
}
function isDataPrefix(chunk, i, firstCharCode) {
return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
}
function isEventPrefix(chunk, i, firstCharCode) {
return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
}
export {
ParseError,
createParser
};
//# sourceMappingURL=index.js.map
{"version":3,"file":"index.js","sources":["../src/errors.ts","../src/parse.ts"],"sourcesContent":["/**\n * The type of error that occurred.\n * @public\n */\nexport type ErrorType = 'invalid-retry' | 'unknown-field'\n\n/**\n * Error thrown when encountering an issue during parsing.\n *\n * @public\n */\nexport class ParseError extends Error {\n /**\n * The type of error that occurred.\n */\n type: ErrorType\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the field name.\n */\n field?: string | undefined\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the value of the field.\n */\n value?: string | undefined\n\n /**\n * The line that caused the error, if available.\n */\n line?: string | undefined\n\n constructor(\n message: string,\n options: {type: ErrorType; field?: string; value?: string; line?: string},\n ) {\n super(message)\n this.name = 'ParseError'\n this.type = options.type\n this.field = options.field\n this.value = options.value\n this.line = options.line\n }\n}\n","/**\n * EventSource/Server-Sent Events parser\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html\n */\nimport {ParseError} from './errors.ts'\nimport type {EventSourceParser, ParserCallbacks} from './types.ts'\n\n// ASCII codes used in the hot parsing paths.\nconst LF = 10\nconst CR = 13\nconst SPACE = 32\n\n// oxlint-disable-next-line no-unused-vars\nfunction noop(_arg: unknown) {\n // intentional noop\n}\n\n/**\n * Creates a new EventSource parser.\n *\n * @param callbacks - Callbacks to invoke on different parsing events:\n * - `onEvent` when a new event is parsed\n * - `onError` when an error occurs\n * - `onRetry` when a new reconnection interval has been sent from the server\n * - `onComment` when a comment is encountered in the stream\n *\n * @returns A new EventSource parser, with `parse` and `reset` methods.\n * @public\n */\nexport function createParser(callbacks: ParserCallbacks): EventSourceParser {\n if (typeof callbacks === 'function') {\n throw new TypeError(\n '`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?',\n )\n }\n\n const {onEvent = noop, onError = noop, onRetry = noop, onComment} = callbacks\n\n // Trailing bytes from prior `feed()` calls that did not yet form a complete line.\n // Stored as an array of fragments and only joined when a line terminator arrives.\n // Concatenating per-feed (`prefix + chunk`) is O(N²) when a single SSE line spans\n // many chunks (e.g. a large `data:` payload streamed in tiny slices, or an MCP-style\n // server that emits one giant content block). Buffering as fragments + joining once\n // makes the same workload linear.\n const pendingFragments: string[] = []\n\n let isFirstChunk = true\n let id: string | undefined\n let data = ''\n let dataLines = 0\n let eventType: string | undefined\n\n /**\n * Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do\n * not yet form a complete line are held back and prepended to the next chunk,\n * so callers can pass arbitrary slices of the stream without worrying about\n * line boundaries.\n *\n * Per the SSE spec, a UTF-8 BOM (0xEF 0xBB 0xBF) at the start of the very\n * first chunk is stripped before parsing.\n *\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream\n */\n function feed(chunk: string) {\n if (isFirstChunk) {\n isFirstChunk = false\n // Match and strip UTF-8 BOM from the start of the stream, if present.\n // (Per the spec, this is only valid at the very start of the stream)\n if (\n chunk.charCodeAt(0) === 0xef &&\n chunk.charCodeAt(1) === 0xbb &&\n chunk.charCodeAt(2) === 0xbf\n ) {\n chunk = chunk.slice(3)\n }\n }\n\n // Hot path: no buffered prefix from a prior partial line. Hand the chunk\n // straight to `processLines`, exactly like the original implementation.\n // Zero new work in the common case (every chunk ends with `\\n\\n`).\n if (pendingFragments.length === 0) {\n const trailing = processLines(chunk)\n if (trailing !== '') pendingFragments.push(trailing)\n return\n }\n\n // We have a buffered prefix. If this chunk also has no terminator, append\n // to the buffer without concatenating — that's the O(N²) trap we're\n // avoiding (large single `data:` payload split across many tiny chunks).\n if (chunk.indexOf('\\n') === -1 && chunk.indexOf('\\r') === -1) {\n pendingFragments.push(chunk)\n return\n }\n\n // Terminator arrived. Join the accumulated fragments + this chunk once,\n // process, and buffer any new trailing partial line.\n pendingFragments.push(chunk)\n const input = pendingFragments.join('')\n pendingFragments.length = 0\n const trailing = processLines(input)\n if (trailing !== '') pendingFragments.push(trailing)\n }\n\n /**\n * Splits `chunk` into SSE lines and dispatches each to the appropriate handler.\n * Returns any trailing bytes that did not terminate with a line break, so the\n * caller can prepend them to the next chunk.\n *\n * The SSE spec permits three line terminators: `\\n`, `\\r`, and `\\r\\n`. Real-world\n * streams almost always use plain `\\n`, so we take a fast path when no `\\r` is\n * present in the chunk. The slow path is spec-correct but does more work per line.\n */\n function processLines(chunk: string): string {\n let searchIndex = 0\n\n // Fast path: LF-only chunk (the common case for typical SSE servers).\n // We can scan forward with a single `indexOf('\\n')` per line and inline\n // the hot-path branches for `data:` and `event:` without the CR bookkeeping\n // the slow path needs.\n if (chunk.indexOf('\\r') === -1) {\n let lfIndex = chunk.indexOf('\\n', searchIndex)\n while (lfIndex !== -1) {\n // Blank line: end-of-event marker. Dispatch the accumulated event (if any)\n // and reset the buffered fields. This is hoisted out of `parseLine` because\n // it's the single most common line shape after `data:` lines.\n if (searchIndex === lfIndex) {\n if (dataLines > 0) {\n onEvent({id, event: eventType, data})\n }\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n const firstCharCode = chunk.charCodeAt(searchIndex)\n if (isDataPrefix(chunk, searchIndex, firstCharCode)) {\n // `data:` line — append the value to the event's data buffer.\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart =\n chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5\n const value = chunk.slice(valueStart, lfIndex)\n // Fast path within a fast path: if this is the first data line AND the\n // next char is another LF (i.e. `data:foo\\n\\n`), dispatch immediately\n // without ever writing to the `data` buffer. This is the shape of a\n // typical single-line SSE event (ChatGPT-style streams, etc.) and is\n // hot enough to be worth the duplication.\n if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {\n onEvent({id, event: eventType, data: value})\n id = undefined\n data = ''\n eventType = undefined\n searchIndex = lfIndex + 2\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n // Multi-line data: concatenate with newline separator per spec.\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {\n // `event:` line — set the event type for the next dispatch. Per spec,\n // an empty value resets `event type` to its default (undefined here).\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(\n chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,\n lfIndex,\n ) || undefined\n } else {\n // Everything else: `id:`, `retry:`, comment lines (`:` prefix), unknown\n // fields, or malformed lines. These are rarer and go through the full\n // per-line parser, which handles the SSE field grammar in detail.\n parseLine(chunk, searchIndex, lfIndex)\n }\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n }\n return chunk.slice(searchIndex)\n }\n\n // Slow path: the chunk contains at least one `\\r`, so lines may be terminated\n // by `\\r`, `\\n`, or `\\r\\n`. We locate the next terminator by looking at both\n // the nearest `\\r` and `\\n` and picking whichever comes first.\n while (searchIndex < chunk.length) {\n const crIndex = chunk.indexOf('\\r', searchIndex)\n const lfIndex = chunk.indexOf('\\n', searchIndex)\n\n let lineEnd = -1\n if (crIndex !== -1 && lfIndex !== -1) {\n lineEnd = crIndex < lfIndex ? crIndex : lfIndex\n } else if (crIndex !== -1) {\n // A trailing `\\r` at the very end of the chunk is ambiguous: it could be\n // a bare-CR terminator, or the first half of a `\\r\\n` whose `\\n` arrives\n // in the next chunk. Defer until we see more input.\n if (crIndex === chunk.length - 1) {\n lineEnd = -1\n } else {\n lineEnd = crIndex\n }\n } else if (lfIndex !== -1) {\n lineEnd = lfIndex\n }\n\n if (lineEnd === -1) {\n break\n }\n\n parseLine(chunk, searchIndex, lineEnd)\n searchIndex = lineEnd + 1\n // If we just consumed a `\\r` and the next char is `\\n`, skip it so the\n // pair is treated as a single terminator rather than an empty line.\n if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {\n searchIndex++\n }\n }\n\n return chunk.slice(searchIndex)\n }\n\n function parseLine(chunk: string, start: number, end: number) {\n if (start === end) {\n dispatchEvent()\n return\n }\n\n const firstCharCode = chunk.charCodeAt(start)\n\n if (isDataPrefix(chunk, start, firstCharCode)) {\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5\n const value = chunk.slice(valueStart, end)\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n return\n }\n\n if (isEventPrefix(chunk, start, firstCharCode)) {\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || undefined\n return\n }\n\n // Fast path for \"id:\" — 'i' = 105, 'd' = 100, ':' = 58\n if (\n firstCharCode === 105 &&\n chunk.charCodeAt(start + 1) === 100 &&\n chunk.charCodeAt(start + 2) === 58\n ) {\n // 'id:'.length === 3, 'id: '.length === 4\n const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end)\n id = value.includes('\\0') ? undefined : value\n return\n }\n\n // Comment line — ':' = 58\n if (firstCharCode === 58) {\n if (onComment) {\n const line = chunk.slice(start, end)\n // skip ':' (+1), or ': ' (+2) when a space follows\n onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1))\n }\n return\n }\n\n const line = chunk.slice(start, end)\n const fieldSeparatorIndex = line.indexOf(':')\n if (fieldSeparatorIndex === -1) {\n processField(line, '', line)\n return\n }\n\n const field = line.slice(0, fieldSeparatorIndex)\n // skip ':' (+1), or ': ' (+2) when a space follows\n const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1\n const value = line.slice(fieldSeparatorIndex + offset)\n processField(field, value, line)\n }\n\n function processField(field: string, value: string, line: string) {\n // Field names must be compared literally, with no case folding performed.\n switch (field) {\n case 'event':\n // Set the `event type` buffer to field value\n eventType = value || undefined\n break\n case 'data':\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n break\n case 'id':\n // If the field value does not contain U+0000 NULL, then set the `ID` buffer to\n // the field value. Otherwise, ignore the field.\n id = value.includes('\\0') ? undefined : value\n break\n case 'retry':\n // If the field value consists of only ASCII digits, then interpret the field value as an\n // integer in base ten, and set the event stream's reconnection time to that integer.\n // Otherwise, ignore the field.\n if (/^\\d+$/.test(value)) {\n onRetry(parseInt(value, 10))\n } else {\n onError(\n new ParseError(`Invalid \\`retry\\` value: \"${value}\"`, {\n type: 'invalid-retry',\n value,\n line,\n }),\n )\n }\n break\n default:\n // Otherwise, the field is ignored.\n onError(\n new ParseError(\n `Unknown field \"${field.length > 20 ? `${field.slice(0, 20)}…` : field}\"`,\n {type: 'unknown-field', field, value, line},\n ),\n )\n break\n }\n }\n\n function dispatchEvent() {\n if (dataLines > 0) {\n onEvent({\n id,\n event: eventType,\n data,\n })\n }\n\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n }\n\n function reset(options: {consume?: boolean} = {}) {\n if (options.consume && pendingFragments.length > 0) {\n const incompleteLine = pendingFragments.join('')\n parseLine(incompleteLine, 0, incompleteLine.length)\n }\n\n isFirstChunk = true\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n pendingFragments.length = 0\n }\n\n return {feed, reset}\n}\n\n/**\n * Checks if `chunk` starts with the literal `data:` at index `i`.\n *\n * Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this\n * hand-unrolled char-code comparison is ~20% faster on common event types.\n * The caller passes `firstCharCode` (the code at `i`) so it can be reused\n * across prefix checks.\n *\n * ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58\n */\nfunction isDataPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 100 &&\n chunk.charCodeAt(i + 1) === 97 &&\n chunk.charCodeAt(i + 2) === 116 &&\n chunk.charCodeAt(i + 3) === 97 &&\n chunk.charCodeAt(i + 4) === 58\n )\n}\n\n/**\n * Checks if `chunk` starts with the literal `event:` at index `i`.\n *\n * See {@link isDataPrefix} for why this is hand-unrolled rather than using\n * `String.prototype.startsWith`.\n *\n * ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58\n */\nfunction isEventPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 101 &&\n chunk.charCodeAt(i + 1) === 118 &&\n chunk.charCodeAt(i + 2) === 101 &&\n chunk.charCodeAt(i + 3) === 110 &&\n chunk.charCodeAt(i + 4) === 116 &&\n chunk.charCodeAt(i + 5) === 58\n )\n}\n"],"names":["trailing","value","line"],"mappings":"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"}
"use strict";
Object.defineProperty(exports, "__esModule", { value: !0 });
var index = require("./index.cjs");
class EventSourceParserStream extends TransformStream {
constructor({ onError, onRetry, onComment } = {}) {
let parser;
super({
start(controller) {
parser = index.createParser({
onEvent: (event) => {
controller.enqueue(event);
},
onError(error) {
onError === "terminate" ? controller.error(error) : typeof onError == "function" && onError(error);
},
onRetry,
onComment
});
},
transform(chunk) {
parser.feed(chunk);
}
});
}
}
exports.ParseError = index.ParseError;
exports.EventSourceParserStream = EventSourceParserStream;
//# sourceMappingURL=stream.cjs.map
{"version":3,"file":"stream.cjs","sources":["../src/stream.ts"],"sourcesContent":["import {createParser} from './parse.ts'\nimport type {EventSourceMessage, EventSourceParser} from './types.ts'\n\n/**\n * Options for the EventSourceParserStream.\n *\n * @public\n */\nexport interface StreamOptions {\n /**\n * Behavior when a parsing error occurs.\n *\n * - A custom function can be provided to handle the error.\n * - `'terminate'` will error the stream and stop parsing.\n * - Any other value will ignore the error and continue parsing.\n *\n * @defaultValue `undefined`\n */\n onError?: ('terminate' | ((error: Error) => void)) | undefined\n\n /**\n * Callback for when a reconnection interval is sent from the server.\n *\n * @param retry - The number of milliseconds to wait before reconnecting.\n */\n onRetry?: ((retry: number) => void) | undefined\n\n /**\n * Callback for when a comment is encountered in the stream.\n *\n * @param comment - The comment encountered in the stream.\n */\n onComment?: ((comment: string) => void) | undefined\n}\n\n/**\n * A TransformStream that ingests a stream of strings and produces a stream of `EventSourceMessage`.\n *\n * @example Basic usage\n * ```\n * const eventStream =\n * response.body\n * .pipeThrough(new TextDecoderStream())\n * .pipeThrough(new EventSourceParserStream())\n * ```\n *\n * @example Terminate stream on parsing errors\n * ```\n * const eventStream =\n * response.body\n * .pipeThrough(new TextDecoderStream())\n * .pipeThrough(new EventSourceParserStream({terminateOnError: true}))\n * ```\n *\n * @public\n */\nexport class EventSourceParserStream extends TransformStream<string, EventSourceMessage> {\n constructor({onError, onRetry, onComment}: StreamOptions = {}) {\n let parser!: EventSourceParser\n\n super({\n start(controller) {\n parser = createParser({\n onEvent: (event) => {\n controller.enqueue(event)\n },\n onError(error) {\n if (onError === 'terminate') {\n controller.error(error)\n } else if (typeof onError === 'function') {\n onError(error)\n }\n\n // Ignore by default\n },\n onRetry,\n onComment,\n })\n },\n transform(chunk) {\n parser.feed(chunk)\n },\n })\n }\n}\n\nexport {type ErrorType, ParseError} from './errors.ts'\nexport type {EventSourceMessage} from './types.ts'\n"],"names":["createParser"],"mappings":";;;AAwDO,MAAM,gCAAgC,gBAA4C;AAAA,EACvF,YAAY,EAAC,SAAS,SAAS,UAAA,IAA4B,CAAA,GAAI;AAC7D,QAAI;AAEJ,UAAM;AAAA,MACJ,MAAM,YAAY;AAChB,iBAASA,MAAAA,aAAa;AAAA,UACpB,SAAS,CAAC,UAAU;AAClB,uBAAW,QAAQ,KAAK;AAAA,UAC1B;AAAA,UACA,QAAQ,OAAO;AACT,wBAAY,cACd,WAAW,MAAM,KAAK,IACb,OAAO,WAAY,cAC5B,QAAQ,KAAK;AAAA,UAIjB;AAAA,UACA;AAAA,UACA;AAAA,QAAA,CACD;AAAA,MACH;AAAA,MACA,UAAU,OAAO;AACf,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IAAA,CACD;AAAA,EACH;AACF;;;"}
/**
* The type of error that occurred.
* @public
*/
export declare type ErrorType = "invalid-retry" | "unknown-field";
/**
* A parsed EventSource message event
*
* @public
*/
export declare interface EventSourceMessage {
/**
* The event type sent from the server. Note that this differs from the browser `EventSource`
* implementation in that browsers will default this to `message`, whereas this parser will
* leave this as `undefined` if not explicitly declared.
*/
event?: string | undefined;
/**
* ID of the message, if any was provided by the server. Can be used by clients to keep the
* last received message ID in sync when reconnecting.
*/
id?: string | undefined;
/**
* The data received for this message
*/
data: string;
}
/**
* A TransformStream that ingests a stream of strings and produces a stream of `EventSourceMessage`.
*
* @example Basic usage
* ```
* const eventStream =
* response.body
* .pipeThrough(new TextDecoderStream())
* .pipeThrough(new EventSourceParserStream())
* ```
*
* @example Terminate stream on parsing errors
* ```
* const eventStream =
* response.body
* .pipeThrough(new TextDecoderStream())
* .pipeThrough(new EventSourceParserStream({terminateOnError: true}))
* ```
*
* @public
*/
export declare class EventSourceParserStream extends TransformStream<
string,
EventSourceMessage
> {
constructor({ onError, onRetry, onComment }?: StreamOptions);
}
/**
* Error thrown when encountering an issue during parsing.
*
* @public
*/
export declare class ParseError extends Error {
/**
* The type of error that occurred.
*/
type: ErrorType;
/**
* In the case of an unknown field encountered in the stream, this will be the field name.
*/
field?: string | undefined;
/**
* In the case of an unknown field encountered in the stream, this will be the value of the field.
*/
value?: string | undefined;
/**
* The line that caused the error, if available.
*/
line?: string | undefined;
constructor(
message: string,
options: {
type: ErrorType;
field?: string;
value?: string;
line?: string;
},
);
}
/**
* Options for the EventSourceParserStream.
*
* @public
*/
export declare interface StreamOptions {
/**
* Behavior when a parsing error occurs.
*
* - A custom function can be provided to handle the error.
* - `'terminate'` will error the stream and stop parsing.
* - Any other value will ignore the error and continue parsing.
*
* @defaultValue `undefined`
*/
onError?: ("terminate" | ((error: Error) => void)) | undefined;
/**
* Callback for when a reconnection interval is sent from the server.
*
* @param retry - The number of milliseconds to wait before reconnecting.
*/
onRetry?: ((retry: number) => void) | undefined;
/**
* Callback for when a comment is encountered in the stream.
*
* @param comment - The comment encountered in the stream.
*/
onComment?: ((comment: string) => void) | undefined;
}
export {};
/**
* The type of error that occurred.
* @public
*/
export declare type ErrorType = "invalid-retry" | "unknown-field";
/**
* A parsed EventSource message event
*
* @public
*/
export declare interface EventSourceMessage {
/**
* The event type sent from the server. Note that this differs from the browser `EventSource`
* implementation in that browsers will default this to `message`, whereas this parser will
* leave this as `undefined` if not explicitly declared.
*/
event?: string | undefined;
/**
* ID of the message, if any was provided by the server. Can be used by clients to keep the
* last received message ID in sync when reconnecting.
*/
id?: string | undefined;
/**
* The data received for this message
*/
data: string;
}
/**
* A TransformStream that ingests a stream of strings and produces a stream of `EventSourceMessage`.
*
* @example Basic usage
* ```
* const eventStream =
* response.body
* .pipeThrough(new TextDecoderStream())
* .pipeThrough(new EventSourceParserStream())
* ```
*
* @example Terminate stream on parsing errors
* ```
* const eventStream =
* response.body
* .pipeThrough(new TextDecoderStream())
* .pipeThrough(new EventSourceParserStream({terminateOnError: true}))
* ```
*
* @public
*/
export declare class EventSourceParserStream extends TransformStream<
string,
EventSourceMessage
> {
constructor({ onError, onRetry, onComment }?: StreamOptions);
}
/**
* Error thrown when encountering an issue during parsing.
*
* @public
*/
export declare class ParseError extends Error {
/**
* The type of error that occurred.
*/
type: ErrorType;
/**
* In the case of an unknown field encountered in the stream, this will be the field name.
*/
field?: string | undefined;
/**
* In the case of an unknown field encountered in the stream, this will be the value of the field.
*/
value?: string | undefined;
/**
* The line that caused the error, if available.
*/
line?: string | undefined;
constructor(
message: string,
options: {
type: ErrorType;
field?: string;
value?: string;
line?: string;
},
);
}
/**
* Options for the EventSourceParserStream.
*
* @public
*/
export declare interface StreamOptions {
/**
* Behavior when a parsing error occurs.
*
* - A custom function can be provided to handle the error.
* - `'terminate'` will error the stream and stop parsing.
* - Any other value will ignore the error and continue parsing.
*
* @defaultValue `undefined`
*/
onError?: ("terminate" | ((error: Error) => void)) | undefined;
/**
* Callback for when a reconnection interval is sent from the server.
*
* @param retry - The number of milliseconds to wait before reconnecting.
*/
onRetry?: ((retry: number) => void) | undefined;
/**
* Callback for when a comment is encountered in the stream.
*
* @param comment - The comment encountered in the stream.
*/
onComment?: ((comment: string) => void) | undefined;
}
export {};
import { createParser } from "./index.js";
import { ParseError } from "./index.js";
class EventSourceParserStream extends TransformStream {
constructor({ onError, onRetry, onComment } = {}) {
let parser;
super({
start(controller) {
parser = createParser({
onEvent: (event) => {
controller.enqueue(event);
},
onError(error) {
onError === "terminate" ? controller.error(error) : typeof onError == "function" && onError(error);
},
onRetry,
onComment
});
},
transform(chunk) {
parser.feed(chunk);
}
});
}
}
export {
EventSourceParserStream,
ParseError
};
//# sourceMappingURL=stream.js.map
{"version":3,"file":"stream.js","sources":["../src/stream.ts"],"sourcesContent":["import {createParser} from './parse.ts'\nimport type {EventSourceMessage, EventSourceParser} from './types.ts'\n\n/**\n * Options for the EventSourceParserStream.\n *\n * @public\n */\nexport interface StreamOptions {\n /**\n * Behavior when a parsing error occurs.\n *\n * - A custom function can be provided to handle the error.\n * - `'terminate'` will error the stream and stop parsing.\n * - Any other value will ignore the error and continue parsing.\n *\n * @defaultValue `undefined`\n */\n onError?: ('terminate' | ((error: Error) => void)) | undefined\n\n /**\n * Callback for when a reconnection interval is sent from the server.\n *\n * @param retry - The number of milliseconds to wait before reconnecting.\n */\n onRetry?: ((retry: number) => void) | undefined\n\n /**\n * Callback for when a comment is encountered in the stream.\n *\n * @param comment - The comment encountered in the stream.\n */\n onComment?: ((comment: string) => void) | undefined\n}\n\n/**\n * A TransformStream that ingests a stream of strings and produces a stream of `EventSourceMessage`.\n *\n * @example Basic usage\n * ```\n * const eventStream =\n * response.body\n * .pipeThrough(new TextDecoderStream())\n * .pipeThrough(new EventSourceParserStream())\n * ```\n *\n * @example Terminate stream on parsing errors\n * ```\n * const eventStream =\n * response.body\n * .pipeThrough(new TextDecoderStream())\n * .pipeThrough(new EventSourceParserStream({terminateOnError: true}))\n * ```\n *\n * @public\n */\nexport class EventSourceParserStream extends TransformStream<string, EventSourceMessage> {\n constructor({onError, onRetry, onComment}: StreamOptions = {}) {\n let parser!: EventSourceParser\n\n super({\n start(controller) {\n parser = createParser({\n onEvent: (event) => {\n controller.enqueue(event)\n },\n onError(error) {\n if (onError === 'terminate') {\n controller.error(error)\n } else if (typeof onError === 'function') {\n onError(error)\n }\n\n // Ignore by default\n },\n onRetry,\n onComment,\n })\n },\n transform(chunk) {\n parser.feed(chunk)\n },\n })\n }\n}\n\nexport {type ErrorType, ParseError} from './errors.ts'\nexport type {EventSourceMessage} from './types.ts'\n"],"names":[],"mappings":";;AAwDO,MAAM,gCAAgC,gBAA4C;AAAA,EACvF,YAAY,EAAC,SAAS,SAAS,UAAA,IAA4B,CAAA,GAAI;AAC7D,QAAI;AAEJ,UAAM;AAAA,MACJ,MAAM,YAAY;AAChB,iBAAS,aAAa;AAAA,UACpB,SAAS,CAAC,UAAU;AAClB,uBAAW,QAAQ,KAAK;AAAA,UAC1B;AAAA,UACA,QAAQ,OAAO;AACT,wBAAY,cACd,WAAW,MAAM,KAAK,IACb,OAAO,WAAY,cAC5B,QAAQ,KAAK;AAAA,UAIjB;AAAA,UACA;AAAA,UACA;AAAA,QAAA,CACD;AAAA,MACH;AAAA,MACA,UAAU,OAAO;AACf,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IAAA,CACD;AAAA,EACH;AACF;"}
MIT License
Copyright (c) 2026 Espen Hovlandsdal <espen@hovlandsdal.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "eventsource-parser",
"version": "3.0.8",
"description": "Streaming, source-agnostic EventSource/Server-Sent Events parser",
"keywords": [
"eventsource",
"server-sent-events",
"sse"
],
"homepage": "https://github.com/rexxars/eventsource-parser#readme",
"bugs": {
"url": "https://github.com/rexxars/eventsource-parser/issues"
},
"license": "MIT",
"author": "Espen Hovlandsdal <espen@hovlandsdal.com>",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/rexxars/eventsource-parser.git"
},
"files": [
"dist",
"!dist/stats.html",
"!dist/index.min.js",
"src",
"stream.js"
],
"type": "module",
"sideEffects": false,
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"source": "./src/index.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
},
"./stream": {
"source": "./src/stream.ts",
"import": "./dist/stream.js",
"require": "./dist/stream.cjs",
"default": "./dist/stream.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "pkg-utils build && pkg-utils --strict",
"clean": "rimraf dist coverage",
"check": "npm run clean && npm run format && npm run lint && npm run build && vitest run",
"format": "oxfmt",
"format:check": "oxfmt --check",
"bench": "node --expose-gc --experimental-strip-types --no-warnings=ExperimentalWarning bench/parse.bench.ts",
"bundle-size": "node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/bundle-size.ts",
"knip": "knip",
"lint": "oxlint && tsc --noEmit",
"posttest": "npm run lint",
"prebuild": "npm run clean",
"prepublishOnly": "npm run build",
"test": "npm run test:node",
"test:bun": "bun test",
"test:deno": "deno run --allow-write --allow-net --allow-run --allow-sys --allow-ffi --allow-env --allow-read npm:vitest",
"test:node": "vitest --reporter=verbose"
},
"devDependencies": {
"@sanity/pkg-utils": "^10.4.15",
"@sanity/semantic-release-preset": "^6.0.0",
"@sanity/tsconfig": "^2.1.0",
"@types/node": "^20.19.0",
"eventsource-encoder": "^1.0.1",
"knip": "^6.4.1",
"mitata": "^1.0.34",
"oxfmt": "^0.45.0",
"oxlint": "^1.60.0",
"rimraf": "^6.1.3",
"rollup-plugin-visualizer": "^6.0.3",
"semantic-release": "^25.0.3",
"terser": "^5.46.1",
"typescript": "^5.9.3",
"vitest": "^4.1.4"
},
"browserslist": [
"node >= 18",
"chrome >= 71",
"safari >= 14.1",
"firefox >= 105",
"edge >= 79"
],
"engines": {
"node": ">=18.0.0"
}
}
# eventsource-parser
[![npm version](https://img.shields.io/npm/v/eventsource-parser.svg?style=flat-square)](https://www.npmjs.com/package/eventsource-parser)[![npm bundle size](https://img.shields.io/bundlephobia/minzip/eventsource-parser?style=flat-square)](https://bundlephobia.com/result?p=eventsource-parser)[![npm weekly downloads](https://img.shields.io/npm/dw/eventsource-parser.svg?style=flat-square)](https://www.npmjs.com/package/eventsource-parser)
A streaming parser for [server-sent events/eventsource](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), without any assumptions about how the actual stream of data is retrieved. It is intended to be a building block for [clients](https://github.com/rexxars/eventsource-client) and polyfills in javascript environments such as browsers, node.js and deno.
If you are looking for a modern client implementation, see [eventsource-client](https://github.com/rexxars/eventsource-client).
You create an instance of the parser, and _feed_ it chunks of data - partial or complete, and the parse emits parsed messages once it receives a complete message. A [TransformStream variant](#stream-usage) is also available for environments that support it (modern browsers, Node 18 and higher).
Other modules in the EventSource family:
- [eventsource-client](https://github.com/rexxars/eventsource-client): modern, feature rich eventsource client for browsers, node.js, bun, deno and other modern JavaScript environments.
- [eventsource-encoder](https://github.com/rexxars/eventsource-encoder): encodes messages in the EventSource/Server-Sent Events format.
- [eventsource](https://github.com/eventsource/eventsource): Node.js polyfill for the WhatWG EventSource API.
> [!NOTE]
> Migrating from eventsource-parser 1.x/2.x? See the [migration guide](./MIGRATE-v3.md).
## Installation
```bash
npm install --save eventsource-parser
```
## Usage
```ts
import {createParser, type EventSourceMessage} from 'eventsource-parser'
function onEvent(event: EventSourceMessage) {
console.log('Received event!')
console.log('id: %s', event.id || '<none>')
console.log('event: %s', event.event || '<none>')
console.log('data: %s', event.data)
}
const parser = createParser({onEvent})
const sseStream = getSomeReadableStream()
for await (const chunk of sseStream) {
parser.feed(chunk)
}
// If you want to re-use the parser for a new stream of events, make sure to reset it!
parser.reset()
console.log('Done!')
```
### Retry intervals
If the server sends a `retry` field in the event stream, the parser will call any `onRetry` callback specified to the `createParser` function:
```ts
const parser = createParser({
onRetry(retryInterval) {
console.log('Server requested retry interval of %dms', retryInterval)
},
onEvent(event) {
// …
},
})
```
### Parse errors
If the parser encounters an error while parsing, it will call any `onError` callback provided to the `createParser` function:
```ts
import {type ParseError} from 'eventsource-parser'
const parser = createParser({
onError(error: ParseError) {
console.error('Error parsing event:', error)
if (error.type === 'invalid-field') {
console.error('Field name:', error.field)
console.error('Field value:', error.value)
console.error('Line:', error.line)
} else if (error.type === 'invalid-retry') {
console.error('Invalid retry interval:', error.value)
}
},
onEvent(event) {
// …
},
})
```
Note that `invalid-field` errors will usually be called for any invalid data - not only data shaped as `field: value`. This is because the EventSource specification says to treat anything prior to a `:` as the field name. Use the `error.line` property to get the full line that caused the error.
> [!NOTE]
> When encountering the end of a stream, calling `.reset({consume: true})` on the parser to flush any remaining data and reset the parser state. This will trigger the `onError` callback if the pending data is not a valid event.
### Comments
The parser will ignore comments (lines starting with `:`) by default. If you want to handle comments, you can provide an `onComment` callback to the `createParser` function:
```ts
const parser = createParser({
onComment(comment) {
console.log('Received comment:', comment)
},
onEvent(event) {
// …
},
})
```
> [!NOTE]
> Leading whitespace is not stripped from comments, eg `: comment` will give ` comment` as the comment value, not `comment` (note the leading space).
## Stream usage
```ts
import {EventSourceParserStream} from 'eventsource-parser/stream'
const eventStream = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream())
```
Note that the TransformStream is exposed under a separate export (`eventsource-parser/stream`), in order to maximize compatibility with environments that do not have the `TransformStream` constructor available.
## License
MIT © [Espen Hovlandsdal](https://espen.codes/)
/**
* The type of error that occurred.
* @public
*/
export type ErrorType = 'invalid-retry' | 'unknown-field'
/**
* Error thrown when encountering an issue during parsing.
*
* @public
*/
export class ParseError extends Error {
/**
* The type of error that occurred.
*/
type: ErrorType
/**
* In the case of an unknown field encountered in the stream, this will be the field name.
*/
field?: string | undefined
/**
* In the case of an unknown field encountered in the stream, this will be the value of the field.
*/
value?: string | undefined
/**
* The line that caused the error, if available.
*/
line?: string | undefined
constructor(
message: string,
options: {type: ErrorType; field?: string; value?: string; line?: string},
) {
super(message)
this.name = 'ParseError'
this.type = options.type
this.field = options.field
this.value = options.value
this.line = options.line
}
}
export {type ErrorType, ParseError} from './errors.ts'
export {createParser} from './parse.ts'
export type {EventSourceMessage, EventSourceParser, ParserCallbacks} from './types.ts'
/**
* EventSource/Server-Sent Events parser
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html
*/
import {ParseError} from './errors.ts'
import type {EventSourceParser, ParserCallbacks} from './types.ts'
// ASCII codes used in the hot parsing paths.
const LF = 10
const CR = 13
const SPACE = 32
// oxlint-disable-next-line no-unused-vars
function noop(_arg: unknown) {
// intentional noop
}
/**
* Creates a new EventSource parser.
*
* @param callbacks - Callbacks to invoke on different parsing events:
* - `onEvent` when a new event is parsed
* - `onError` when an error occurs
* - `onRetry` when a new reconnection interval has been sent from the server
* - `onComment` when a comment is encountered in the stream
*
* @returns A new EventSource parser, with `parse` and `reset` methods.
* @public
*/
export function createParser(callbacks: ParserCallbacks): EventSourceParser {
if (typeof callbacks === 'function') {
throw new TypeError(
'`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?',
)
}
const {onEvent = noop, onError = noop, onRetry = noop, onComment} = callbacks
// Trailing bytes from prior `feed()` calls that did not yet form a complete line.
// Stored as an array of fragments and only joined when a line terminator arrives.
// Concatenating per-feed (`prefix + chunk`) is O(N²) when a single SSE line spans
// many chunks (e.g. a large `data:` payload streamed in tiny slices, or an MCP-style
// server that emits one giant content block). Buffering as fragments + joining once
// makes the same workload linear.
const pendingFragments: string[] = []
let isFirstChunk = true
let id: string | undefined
let data = ''
let dataLines = 0
let eventType: string | undefined
/**
* Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do
* not yet form a complete line are held back and prepended to the next chunk,
* so callers can pass arbitrary slices of the stream without worrying about
* line boundaries.
*
* Per the SSE spec, a UTF-8 BOM (0xEF 0xBB 0xBF) at the start of the very
* first chunk is stripped before parsing.
*
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
*/
function feed(chunk: string) {
if (isFirstChunk) {
isFirstChunk = false
// Match and strip UTF-8 BOM from the start of the stream, if present.
// (Per the spec, this is only valid at the very start of the stream)
if (
chunk.charCodeAt(0) === 0xef &&
chunk.charCodeAt(1) === 0xbb &&
chunk.charCodeAt(2) === 0xbf
) {
chunk = chunk.slice(3)
}
}
// Hot path: no buffered prefix from a prior partial line. Hand the chunk
// straight to `processLines`, exactly like the original implementation.
// Zero new work in the common case (every chunk ends with `\n\n`).
if (pendingFragments.length === 0) {
const trailing = processLines(chunk)
if (trailing !== '') pendingFragments.push(trailing)
return
}
// We have a buffered prefix. If this chunk also has no terminator, append
// to the buffer without concatenating — that's the O(N²) trap we're
// avoiding (large single `data:` payload split across many tiny chunks).
if (chunk.indexOf('\n') === -1 && chunk.indexOf('\r') === -1) {
pendingFragments.push(chunk)
return
}
// Terminator arrived. Join the accumulated fragments + this chunk once,
// process, and buffer any new trailing partial line.
pendingFragments.push(chunk)
const input = pendingFragments.join('')
pendingFragments.length = 0
const trailing = processLines(input)
if (trailing !== '') pendingFragments.push(trailing)
}
/**
* Splits `chunk` into SSE lines and dispatches each to the appropriate handler.
* Returns any trailing bytes that did not terminate with a line break, so the
* caller can prepend them to the next chunk.
*
* The SSE spec permits three line terminators: `\n`, `\r`, and `\r\n`. Real-world
* streams almost always use plain `\n`, so we take a fast path when no `\r` is
* present in the chunk. The slow path is spec-correct but does more work per line.
*/
function processLines(chunk: string): string {
let searchIndex = 0
// Fast path: LF-only chunk (the common case for typical SSE servers).
// We can scan forward with a single `indexOf('\n')` per line and inline
// the hot-path branches for `data:` and `event:` without the CR bookkeeping
// the slow path needs.
if (chunk.indexOf('\r') === -1) {
let lfIndex = chunk.indexOf('\n', searchIndex)
while (lfIndex !== -1) {
// Blank line: end-of-event marker. Dispatch the accumulated event (if any)
// and reset the buffered fields. This is hoisted out of `parseLine` because
// it's the single most common line shape after `data:` lines.
if (searchIndex === lfIndex) {
if (dataLines > 0) {
onEvent({id, event: eventType, data})
}
id = undefined
data = ''
dataLines = 0
eventType = undefined
searchIndex = lfIndex + 1
lfIndex = chunk.indexOf('\n', searchIndex)
continue
}
const firstCharCode = chunk.charCodeAt(searchIndex)
if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
// `data:` line — append the value to the event's data buffer.
// 'data:'.length === 5, 'data: '.length === 6
const valueStart =
chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5
const value = chunk.slice(valueStart, lfIndex)
// Fast path within a fast path: if this is the first data line AND the
// next char is another LF (i.e. `data:foo\n\n`), dispatch immediately
// without ever writing to the `data` buffer. This is the shape of a
// typical single-line SSE event (ChatGPT-style streams, etc.) and is
// hot enough to be worth the duplication.
if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
onEvent({id, event: eventType, data: value})
id = undefined
data = ''
eventType = undefined
searchIndex = lfIndex + 2
lfIndex = chunk.indexOf('\n', searchIndex)
continue
}
// Multi-line data: concatenate with newline separator per spec.
data = dataLines === 0 ? value : `${data}\n${value}`
dataLines++
} else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {
// `event:` line — set the event type for the next dispatch. Per spec,
// an empty value resets `event type` to its default (undefined here).
// 'event:'.length === 6, 'event: '.length === 7
eventType =
chunk.slice(
chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,
lfIndex,
) || undefined
} else {
// Everything else: `id:`, `retry:`, comment lines (`:` prefix), unknown
// fields, or malformed lines. These are rarer and go through the full
// per-line parser, which handles the SSE field grammar in detail.
parseLine(chunk, searchIndex, lfIndex)
}
searchIndex = lfIndex + 1
lfIndex = chunk.indexOf('\n', searchIndex)
}
return chunk.slice(searchIndex)
}
// Slow path: the chunk contains at least one `\r`, so lines may be terminated
// by `\r`, `\n`, or `\r\n`. We locate the next terminator by looking at both
// the nearest `\r` and `\n` and picking whichever comes first.
while (searchIndex < chunk.length) {
const crIndex = chunk.indexOf('\r', searchIndex)
const lfIndex = chunk.indexOf('\n', searchIndex)
let lineEnd = -1
if (crIndex !== -1 && lfIndex !== -1) {
lineEnd = crIndex < lfIndex ? crIndex : lfIndex
} else if (crIndex !== -1) {
// A trailing `\r` at the very end of the chunk is ambiguous: it could be
// a bare-CR terminator, or the first half of a `\r\n` whose `\n` arrives
// in the next chunk. Defer until we see more input.
if (crIndex === chunk.length - 1) {
lineEnd = -1
} else {
lineEnd = crIndex
}
} else if (lfIndex !== -1) {
lineEnd = lfIndex
}
if (lineEnd === -1) {
break
}
parseLine(chunk, searchIndex, lineEnd)
searchIndex = lineEnd + 1
// If we just consumed a `\r` and the next char is `\n`, skip it so the
// pair is treated as a single terminator rather than an empty line.
if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {
searchIndex++
}
}
return chunk.slice(searchIndex)
}
function parseLine(chunk: string, start: number, end: number) {
if (start === end) {
dispatchEvent()
return
}
const firstCharCode = chunk.charCodeAt(start)
if (isDataPrefix(chunk, start, firstCharCode)) {
// 'data:'.length === 5, 'data: '.length === 6
const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5
const value = chunk.slice(valueStart, end)
data = dataLines === 0 ? value : `${data}\n${value}`
dataLines++
return
}
if (isEventPrefix(chunk, start, firstCharCode)) {
// 'event:'.length === 6, 'event: '.length === 7
eventType =
chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || undefined
return
}
// Fast path for "id:" — 'i' = 105, 'd' = 100, ':' = 58
if (
firstCharCode === 105 &&
chunk.charCodeAt(start + 1) === 100 &&
chunk.charCodeAt(start + 2) === 58
) {
// 'id:'.length === 3, 'id: '.length === 4
const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end)
id = value.includes('\0') ? undefined : value
return
}
// Comment line — ':' = 58
if (firstCharCode === 58) {
if (onComment) {
const line = chunk.slice(start, end)
// skip ':' (+1), or ': ' (+2) when a space follows
onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1))
}
return
}
const line = chunk.slice(start, end)
const fieldSeparatorIndex = line.indexOf(':')
if (fieldSeparatorIndex === -1) {
processField(line, '', line)
return
}
const field = line.slice(0, fieldSeparatorIndex)
// skip ':' (+1), or ': ' (+2) when a space follows
const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1
const value = line.slice(fieldSeparatorIndex + offset)
processField(field, value, line)
}
function processField(field: string, value: string, line: string) {
// Field names must be compared literally, with no case folding performed.
switch (field) {
case 'event':
// Set the `event type` buffer to field value
eventType = value || undefined
break
case 'data':
data = dataLines === 0 ? value : `${data}\n${value}`
dataLines++
break
case 'id':
// If the field value does not contain U+0000 NULL, then set the `ID` buffer to
// the field value. Otherwise, ignore the field.
id = value.includes('\0') ? undefined : value
break
case 'retry':
// If the field value consists of only ASCII digits, then interpret the field value as an
// integer in base ten, and set the event stream's reconnection time to that integer.
// Otherwise, ignore the field.
if (/^\d+$/.test(value)) {
onRetry(parseInt(value, 10))
} else {
onError(
new ParseError(`Invalid \`retry\` value: "${value}"`, {
type: 'invalid-retry',
value,
line,
}),
)
}
break
default:
// Otherwise, the field is ignored.
onError(
new ParseError(
`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}…` : field}"`,
{type: 'unknown-field', field, value, line},
),
)
break
}
}
function dispatchEvent() {
if (dataLines > 0) {
onEvent({
id,
event: eventType,
data,
})
}
id = undefined
data = ''
dataLines = 0
eventType = undefined
}
function reset(options: {consume?: boolean} = {}) {
if (options.consume && pendingFragments.length > 0) {
const incompleteLine = pendingFragments.join('')
parseLine(incompleteLine, 0, incompleteLine.length)
}
isFirstChunk = true
id = undefined
data = ''
dataLines = 0
eventType = undefined
pendingFragments.length = 0
}
return {feed, reset}
}
/**
* Checks if `chunk` starts with the literal `data:` at index `i`.
*
* Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this
* hand-unrolled char-code comparison is ~20% faster on common event types.
* The caller passes `firstCharCode` (the code at `i`) so it can be reused
* across prefix checks.
*
* ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58
*/
function isDataPrefix(chunk: string, i: number, firstCharCode: number): boolean {
return (
firstCharCode === 100 &&
chunk.charCodeAt(i + 1) === 97 &&
chunk.charCodeAt(i + 2) === 116 &&
chunk.charCodeAt(i + 3) === 97 &&
chunk.charCodeAt(i + 4) === 58
)
}
/**
* Checks if `chunk` starts with the literal `event:` at index `i`.
*
* See {@link isDataPrefix} for why this is hand-unrolled rather than using
* `String.prototype.startsWith`.
*
* ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58
*/
function isEventPrefix(chunk: string, i: number, firstCharCode: number): boolean {
return (
firstCharCode === 101 &&
chunk.charCodeAt(i + 1) === 118 &&
chunk.charCodeAt(i + 2) === 101 &&
chunk.charCodeAt(i + 3) === 110 &&
chunk.charCodeAt(i + 4) === 116 &&
chunk.charCodeAt(i + 5) === 58
)
}
import {createParser} from './parse.ts'
import type {EventSourceMessage, EventSourceParser} from './types.ts'
/**
* Options for the EventSourceParserStream.
*
* @public
*/
export interface StreamOptions {
/**
* Behavior when a parsing error occurs.
*
* - A custom function can be provided to handle the error.
* - `'terminate'` will error the stream and stop parsing.
* - Any other value will ignore the error and continue parsing.
*
* @defaultValue `undefined`
*/
onError?: ('terminate' | ((error: Error) => void)) | undefined
/**
* Callback for when a reconnection interval is sent from the server.
*
* @param retry - The number of milliseconds to wait before reconnecting.
*/
onRetry?: ((retry: number) => void) | undefined
/**
* Callback for when a comment is encountered in the stream.
*
* @param comment - The comment encountered in the stream.
*/
onComment?: ((comment: string) => void) | undefined
}
/**
* A TransformStream that ingests a stream of strings and produces a stream of `EventSourceMessage`.
*
* @example Basic usage
* ```
* const eventStream =
* response.body
* .pipeThrough(new TextDecoderStream())
* .pipeThrough(new EventSourceParserStream())
* ```
*
* @example Terminate stream on parsing errors
* ```
* const eventStream =
* response.body
* .pipeThrough(new TextDecoderStream())
* .pipeThrough(new EventSourceParserStream({terminateOnError: true}))
* ```
*
* @public
*/
export class EventSourceParserStream extends TransformStream<string, EventSourceMessage> {
constructor({onError, onRetry, onComment}: StreamOptions = {}) {
let parser!: EventSourceParser
super({
start(controller) {
parser = createParser({
onEvent: (event) => {
controller.enqueue(event)
},
onError(error) {
if (onError === 'terminate') {
controller.error(error)
} else if (typeof onError === 'function') {
onError(error)
}
// Ignore by default
},
onRetry,
onComment,
})
},
transform(chunk) {
parser.feed(chunk)
},
})
}
}
export {type ErrorType, ParseError} from './errors.ts'
export type {EventSourceMessage} from './types.ts'
import type {ParseError} from './errors.ts'
/**
* EventSource parser instance.
*
* Needs to be reset between reconnections/when switching data source, using the `reset()` method.
*
* @public
*/
export interface EventSourceParser {
/**
* Feeds the parser another chunk. The method _does not_ return a parsed message.
* Instead, callbacks passed when creating the parser will be triggered once we see enough data
* for a valid/invalid parsing step (see {@link ParserCallbacks}).
*
* @param chunk - The chunk to parse. Can be a partial, eg in the case of streaming messages.
* @public
*/
feed(chunk: string): void
/**
* Resets the parser state. This is required when you have a new stream of messages -
* for instance in the case of a client being disconnected and reconnecting.
*
* Previously received, incomplete data will NOT be parsed unless you pass `consume: true`,
* which tells the parser to attempt to consume any incomplete data as if it ended with a newline
* character. This is useful for cases when a server sends a non-EventSource message that you
* want to be able to react to in an `onError` callback.
*
* @public
*/
reset(options?: {consume?: boolean}): void
}
/**
* A parsed EventSource message event
*
* @public
*/
export interface EventSourceMessage {
/**
* The event type sent from the server. Note that this differs from the browser `EventSource`
* implementation in that browsers will default this to `message`, whereas this parser will
* leave this as `undefined` if not explicitly declared.
*/
event?: string | undefined
/**
* ID of the message, if any was provided by the server. Can be used by clients to keep the
* last received message ID in sync when reconnecting.
*/
id?: string | undefined
/**
* The data received for this message
*/
data: string
}
/**
* Callbacks that can be passed to the parser to handle different types of parsed messages
* and errors.
*
* @public
*/
export interface ParserCallbacks {
/**
* Callback for when a new event/message is parsed from the stream.
* This is the main callback that clients will use to handle incoming messages.
*
* @param event - The parsed event/message
*/
onEvent?: ((event: EventSourceMessage) => void) | undefined
/**
* Callback for when the server sends a new reconnection interval through the `retry` field.
*
* @param retry - The number of milliseconds to wait before reconnecting.
*/
onRetry?: ((retry: number) => void) | undefined
/**
* Callback for when a comment is encountered in the stream.
*
* @param comment - The comment encountered in the stream.
*/
onComment?: ((comment: string) => void) | undefined
/**
* Callback for when an error occurs during parsing. This is a catch-all for any errors
* that occur during parsing, and can be used to handle them in a custom way. Most clients
* tend to silently ignore any errors and instead retry, but it can be helpful to log/debug.
*
* @param error - The error that occurred during parsing
*/
onError?: ((error: ParseError) => void) | undefined
}
/* included for compatibility with react-native without package exports support */
module.exports = require('./dist/stream.cjs')
+2
-1
{
"name": "@silver886/mcp-proxy",
"version": "0.2.0",
"version": "0.2.1",
"description": "MCP proxy bridge: forward MCP requests across network boundaries via Cloudflare tunnel",

@@ -47,2 +47,3 @@ "repository": {

},
"bundleDependencies": true,
"scripts": {

@@ -49,0 +50,0 @@ "build": "tsc"

@@ -216,9 +216,1 @@ # MCP Proxy

| `-32005` | `REQUEST_TIMEOUT` | MCP server didn't respond in time |
## Development
```bash
pnpm install
pnpm run build
pnpm publish --access public --no-git-checks
```