Purpose:
This module starts a cloudflared tunnel that exposes a local http://localhost:<port> service and returns the dynamic trycloudflare.com URL produced by cloudflared. It includes an optional automatic fault-detection-and-update feature that will detect when the tunnel goes down, restart cloudflared, and call user-supplied callbacks.
-
Exported function:
createTunnel()Factory function that returns an isolated tunnel manager instance with{ startCloudflared, killChild }methods. -
startCloudflared(options)starts acloudflaredprocess and resolves aPromisewith the dynamic public URL (thetrycloudflare.comlink) once it appears incloudflaredstdout/stderr. -
Optional behavior: when
autoFaultDetectionAndUpdateistrueand bothsuccessCallbackandfaultCallbackare functions, the module will periodically check the dynamic URL and, on failure, restartcloudflaredand call the callbacks. -
Important runtime requirements:
cloudflaredbinary must be installed and available inPATH.- The module uses Node built-in
child_process,readline, andfs. - Uses closure-scoped state (encapsulated per tunnel instance) and process-level handlers (SIGINT, SIGTERM, exit, etc.).
- Requires Node.js 18+ for native
fetchsupport.
- Install
cloudflaredon the host machine and ensure it can be run by simply callingcloudflaredfrom a shell (i.e., it's inPATH). See the official Cloudflare documentation for installation instructions for your OS. - Use Node.js 18+ (required for native
fetchAPI used in health checks). - Install via npm:
npm install cloudflaredjs - Import this module in your code and call
createTunnel()to get a tunnel instance.
Factory function that creates an isolated tunnel manager instance. Each instance maintains its own child process and configuration in a closure.
Returns:
startCloudflared(function) — starts the tunnelkillChild(function) — stops the tunnel and cleans up
Parameters (single options object):
-
port(number, required) — the local port to forward, e.g.3000. The module will attempt to forwardhttp://localhost:<port>. -
verbose(boolean, defaultfalse) — whentrue, the module appends cloudflared stdout/stderr lines to./cloudflaredjs.<port>.logs.txtand prints additional runtime information to console. -
autoFaultDetectionAndUpdate(boolean, defaultfalse) — whentrue, the module will start automatic monitoring/restart logic to detect a faulty tunnel and attempt to obtain a new link. Note: This is the correct spelling (previously misspelled as "autoFaultDetectionAndUptate"). -
successCallback(function, default() => {}) — a callback that is called with the new public link when a restart produces a new link. IfautoFaultDetectionAndUpdateis enabled, this must be a function. Signature:(url: string) => void. -
faultCallback(function, required if auto-update enabled) — a callback invoked when fault-detection exhausts its retry budget. Required whenautoFaultDetectionAndUpdateis enabled. Signature:() => void. -
delay(number, default8000) — interval in milliseconds between health checks of the tunnel URL whenautoFaultDetectionAndUpdateis enabled. -
afterFaultRetries(number, default10) — number of consecutive failed health checks allowed before giving up and callingfaultCallback. Note: This is the correct spelling (previously misspelled as "afterFaultReties"). The code comparesargs.afterFaultRetries < faultRetriesfor termination.
Returns:
- On success, a
Promisethat resolves to the detected publictrycloudflare.comURL string. - If
autoFaultDetectionAndUpdateis set totruebut eithersuccessCallbackorfaultCallbackare not functions, the function immediately returns a rejectedPromisewith anError.
Behavior detail:
- The module spawns
cloudflared tunnel --url http://localhost:<port>with stdio pipes and parseschild.stdoutandchild.stderrline-by-line usingreadline.createInterface. - It looks for the first HTTP(S) URL matching
/https?:\/\/[^\s)]+/i. When the found URL ends withtrycloudflare.com, it resolves the returnedPromisewith that URL. - Includes a safety check: if no URL is found within the first 11 lines of output, it rejects the promise and kills the child process.
- Logs are written to
./cloudflaredjs.<port>.logs.txtwhenverboseis enabled.
Stops the spawned cloudflared child process and clears all health check intervals.
Behavior:
- Idempotent (safe to call multiple times)
- Clears the
retryIntervalif running - Kills the process using:
taskkill /PID <pid> /T /Fon Windowsprocess.kill(-pid, 'SIGTERM')on Unix (kills process group)- Falls back to
child.kill('SIGTERM')if group kill fails
- Sets internal
childreference tonull
import { createTunnel } from "cloudflaredjs";
const tunnel1 = createTunnel();
(async () => {
try {
const url = await tunnel1.startCloudflared({ port: 3000 });
console.log("Tunnel URL:", url);
// When done:
// tunnel1.killChild();
} catch (err) {
console.error("Failed to start cloudflared tunnel:", err);
}
})();import { createTunnel } from "cloudflaredjs";
const tunnel1 = createTunnel();
function gotNewLink(newLink) {
console.log("New cloudflared link:", newLink);
// e.g. update a remote config, notify client, update DNS, etc.
}
function onFault() {
console.error("cloudflared auto-restart exceeded retry limit.");
// fallback behavior (alert operator, stop server, etc.)
}
tunnel1
.startCloudflared({
port: 5500,
verbose: false,
autoFaultDetectionAndUpdate: true, // correct spelling
successCallback: gotNewLink,
faultCallback: onFault,
delay: 8000,
afterFaultRetries: 10, // correct spelling
})
.then((url) => {
console.log("Initial link:", url);
})
.catch((err) => {
console.error("Error:", err);
});-
Factory pattern:
createTunnel()returns an object with methods. Each instance maintains its own closure-scoped state (child,args,retryInterval,cleaningUp), allowing multiple independent tunnels. -
Process spawn: uses
spawn('cloudflared', ['tunnel', '--url', 'http://localhost:<port>'], { stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' })and parses both stdout and stderr. -
URL extraction: uses regex
/https?:\/\/[^\s)]+/iand further checkstrycloudflare.comwith/trycloudflare\.com$/i. -
Safety mechanism: Rejects the promise if no URL is found in the first 11 lines of output (uses a
countervariable). -
Logging: if
verboseistrue, the module appends every parsed line to./cloudflaredjs.<port>.logs.txtusingfs.appendFile. -
Auto-fault detection (
retryUpdate):- When enabled,
retryUpdate()awaits the initial URL promise. - Sets a repeating
setInterval(stored in closure-scopedretryInterval) that:- Performs
fetch(link)health check. - Tracks
faultRetries(consecutive failures) andoverallRetries(total attempts). - If response status is not
200, it:- Increments
faultRetries - Calls
killChild() - Starts new
cloudflaredviastartCloudflared({ ...args }) - Calls
successCallback(newLink)with the new URL - Recursively calls
retryUpdate()with the new promise
- Increments
- If
faultRetriesexceedsargs.afterFaultRetries:- Clears the interval
- Calls
faultCallback() - Throws an error
- On successful 200 response, resets
faultRetriesto 0
- Performs
- When enabled,
-
Cleanup (
killChild):- Uses a
cleaningUplatch to prevent concurrent kill attempts - Always clears
retryIntervalfirst - Platform-specific process termination:
- Windows:
taskkill /PID <pid> /T /F - Unix:
process.kill(-pid, 'SIGTERM')(process group) - Fallback:
child.kill('SIGTERM')
- Windows:
- Uses a
-
Multiple tunnel instances:
- ✅ The factory pattern now supports multiple concurrent tunnels properly
- Each
createTunnel()call creates an isolated instance - No global state conflicts
-
Cloudflare rate limits:
- Do not create more than 3–4 simultaneous tunnels
- Do not restart tunnels in rapid succession
- Excessive use may result in temporary IP blocking (10–15 minutes)
-
fetchAPI requirement:- Requires Node.js 18+ for native
fetchsupport - For older Node versions, install
node-fetchpolyfill
- Requires Node.js 18+ for native
-
No explicit fetch timeouts:
- The
fetch(link)call has no timeout configuration - Hanging requests could delay health check intervals
- Consider wrapping with
AbortControllerand timeout
- The
-
11-line safety check:
- The module rejects if no URL found in first 11 output lines
- If
cloudflaredis slow to start, this might trigger prematurely - Consider making this configurable
-
Health check behavior:
- Only checks for HTTP 200 status
- Any non-200 response (including redirects, 404, 500) triggers restart
- Consider customizable success criteria
-
Consecutive vs total retries:
afterFaultRetriescounts consecutive failures only- Resets to 0 on any successful check
- Total attempts tracked separately in
overallRetries
-
Log file management:
- Creates
./cloudflaredjs.<port>.logs.txtper tunnel - No automatic rotation or size limits
- Consider implementing log rotation for production use
- Creates
type TunnelInstance = {
startCloudflared: (options: StartOptions) => Promise<string>;
killChild: () => void;
};
type StartOptions = {
port: number;
verbose?: boolean;
autoFaultDetectionAndUpdate?: boolean;
successCallback?: (newLink: string) => void;
faultCallback?: () => void;
delay?: number;
afterFaultRetries?: number;
};
export function createTunnel(): TunnelInstance;- ✅ Fixed: Option name typos corrected (
autoFaultDetectionAndUpdate,afterFaultRetries) - ✅ Fixed:
successCallbacknow defaults to a function, not a string - ✅ Fixed: Instance-based architecture eliminates global state issues
- ✅ Implemented: Counter increments properly for 11-line safety check
Future enhancements:
- Add configurable fetch timeout with
AbortController - Implement log rotation or streaming logger
- Add TypeScript definitions file (
.d.ts) - Add configurable success status codes (not just 200)
- Add graceful shutdown delay before
killChild() - Support custom
cloudflaredbinary path - Add event emitter pattern for tunnel lifecycle events
- Unit tests with mocked
cloudflaredoutput - Integration tests with real
cloudflared(if available in CI)
# cloudflaredjs
Start a `cloudflared` tunnel and obtain the dynamic `trycloudflare.com` link.
## Requirements
- `cloudflared` must be installed and in PATH
- Node.js 18+ (for native fetch)
## Installation
```bash
npm install cloudflaredjs
```import { createTunnel } from "cloudflaredjs";
const tunnel = createTunnel();
const url = await tunnel.startCloudflared({ port: 3000 });
console.log("Tunnel URL:", url);
// Stop when done
tunnel.killChild();const tunnel = createTunnel();
await tunnel.startCloudflared({
port: 3000,
autoFaultDetectionAndUpdate: true,
successCallback: (url) => console.log("New URL:", url),
faultCallback: () => console.error("Tunnel failed permanently"),
delay: 8000,
afterFaultRetries: 10,
});