@lenne.tech/cli
Version:
lenne.Tech CLI: lt
130 lines (129 loc) • 5.65 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TRYCLOUDFLARE_URL_PATTERN = void 0;
exports.buildQuickTunnelArgs = buildQuickTunnelArgs;
exports.cloudflaredAvailable = cloudflaredAvailable;
exports.extractTrycloudflareUrl = extractTrycloudflareUrl;
exports.spawnQuickTunnel = spawnQuickTunnel;
/**
* Cloudflare Tunnel integration for `lt dev tunnel`.
*
* Quick tunnels (no Cloudflare account, ephemeral URL) are the only
* mode supported here. Named tunnels would require multi-step Cloudflare
* setup (auth, DNS routing) which belongs in a separate command and is
* intentionally out of scope for this lib.
*
* The Caddy upstream stays unchanged: cloudflared connects to Caddy's
* HTTPS endpoint and rewrites the `Host` header to the configured
* `*.localhost` so Caddy's per-project block matches. Without that
* rewrite Cloudflare's edge would forward the random `*.trycloudflare.com`
* hostname which Caddy doesn't know.
*/
const child_process_1 = require("child_process");
/**
* Match the trycloudflare URL anywhere in cloudflared's log output.
*
* cloudflared prints it in an ASCII-box on stderr (Linux/macOS) — exported
* here so tests can assert the exact pattern without spawning the binary.
*/
exports.TRYCLOUDFLARE_URL_PATTERN = /https:\/\/[a-z0-9][a-z0-9-]*\.trycloudflare\.com/i;
/**
* Build the argv list for `cloudflared tunnel --url ...`. Pure helper.
*
* Why each flag:
* --url : the local upstream (Caddy's HTTPS endpoint)
* --http-host-header: tells cloudflared to rewrite Host before forwarding,
* so Caddy's vhost match works for the public URL
* --no-tls-verify : Caddy serves a locally-signed cert that cloudflared
* cannot validate from outside the local trust store;
* disabling the check is safe because the upstream
* hop never leaves localhost
*/
function buildQuickTunnelArgs(opts) {
return [
'tunnel',
'--no-autoupdate',
'--no-tls-verify',
'--http-host-header',
opts.hostHeader,
'--url',
opts.upstreamUrl,
];
}
/** Detect whether `cloudflared` is on PATH. */
function cloudflaredAvailable() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve) => {
var _a;
const child = (0, child_process_1.spawn)('cloudflared', ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (stdout += String(b)));
child.on('error', () => resolve({ installed: false }));
child.on('close', (code) => {
if (code !== 0)
return resolve({ installed: false });
const match = stdout.match(/version\s+(\S+)/i);
resolve({ binary: 'cloudflared', installed: true, version: match === null || match === void 0 ? void 0 : match[1] });
});
});
});
}
/**
* Extract the trycloudflare URL from a chunk of cloudflared output.
* Returns the first match (cloudflared logs the URL exactly once).
*/
function extractTrycloudflareUrl(output) {
const match = output.match(exports.TRYCLOUDFLARE_URL_PATTERN);
return match ? match[0] : null;
}
/**
* Spawn a quick tunnel and resolve the public URL once cloudflared logs it.
*
* The returned `publicUrl` promise rejects if the child exits before
* surfacing a URL (timeout: ~30s, then cloudflared usually emits an
* error message and exits). The caller is expected to keep the
* process alive (foreground command) and `child.kill()` on Ctrl-C.
*/
function spawnQuickTunnel(opts) {
const args = buildQuickTunnelArgs(opts);
const child = (0, child_process_1.spawn)('cloudflared', args, { stdio: ['ignore', 'pipe', 'pipe'] });
const publicUrl = new Promise((resolve, reject) => {
var _a, _b;
let buffer = '';
let settled = false;
const onChunk = (chunk) => {
if (settled)
return;
buffer += String(chunk);
const url = extractTrycloudflareUrl(buffer);
if (url) {
settled = true;
resolve(url);
}
};
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', onChunk);
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', onChunk);
child.on('error', (err) => {
if (settled)
return;
settled = true;
reject(err);
});
child.on('close', (code) => {
if (settled)
return;
settled = true;
reject(new Error(`cloudflared exited (code ${code}) before publishing a tunnel URL.\n${buffer.slice(-500)}`));
});
});
return { child, publicUrl };
}