@vibecontrols/vibe-plugin-tunnel-cloudflare 2026.601.2 → 2026.814.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Agent-tunnel supervisor.
3
+ *
4
+ * A trycloudflare quick tunnel only resolves while the `cloudflared` process
5
+ * that created it is alive — once that process exits (crash, OOM, network
6
+ * flap, Cloudflare dropping the tunnel) the random `*.trycloudflare.com`
7
+ * hostname goes NXDOMAIN immediately.
8
+ *
9
+ * Before this supervisor existed nothing noticed: the provider kept the dead
10
+ * URL in storage, `getActiveTunnelUrl()` kept handing it out, the agent's
11
+ * periodic tunnel-sync saw "URL unchanged" and stayed quiet, and the control
12
+ * plane kept probing a hostname that no longer existed — the agent showed up
13
+ * as *Degraded* until a human hit "Edit Tunnel" or restarted the daemon.
14
+ *
15
+ * This module owns the "is the agent tunnel actually up, and if not bring it
16
+ * back" decision. It is deliberately dependency-injected and free of any
17
+ * process/storage imports so the whole restart policy is unit-testable without
18
+ * spawning `cloudflared`.
19
+ *
20
+ * Liveness is polled by PID rather than driven purely by a `proc.exited`
21
+ * handler because the agent tunnel is frequently a process this plugin did
22
+ * *not* spawn: the daemon's pre-config bootstrap starts `cloudflared` before
23
+ * finalize and the provider merely **adopts** its PID, so there is no
24
+ * `Subprocess` handle to await. Polling covers spawned and adopted tunnels
25
+ * with one code path; the spawn path additionally nudges the supervisor on
26
+ * exit so recovery starts in milliseconds instead of at the next tick.
27
+ */
28
+ /** How often to check that the agent tunnel's process is still alive. */
29
+ export declare const DEFAULT_SUPERVISOR_INTERVAL_MS = 15000;
30
+ /** First restart delay after a failed restart attempt. */
31
+ export declare const RESTART_BASE_DELAY_MS = 2000;
32
+ /** Ceiling for the exponential restart backoff. */
33
+ export declare const RESTART_MAX_DELAY_MS = 60000;
34
+ /**
35
+ * Delay before restart attempt `attempt` (1-based), exponential and capped.
36
+ * Attempt 1 is immediate — the first restart after a crash should not wait.
37
+ */
38
+ export declare function restartBackoffMs(attempt: number): number;
39
+ /** Outcome of a single supervisor tick — returned for tests and telemetry. */
40
+ export type SupervisorTickResult = "paused" | "alive" | "backoff" | "restarted" | "failed";
41
+ export interface AgentTunnelSupervisorDeps {
42
+ /** PID of the process currently believed to own the agent tunnel. */
43
+ getAgentTunnelPid(): Promise<number | null>;
44
+ /** True when `pid` is still running. */
45
+ isAlive(pid: number): boolean;
46
+ /**
47
+ * Drop the persisted agent-tunnel URL + PID. Called as soon as the tunnel is
48
+ * found dead so nothing (health checks, tunnel-sync, the control plane) can
49
+ * keep handing out a hostname that no longer resolves.
50
+ */
51
+ clearAgentTunnel(): Promise<void>;
52
+ /** Bring a fresh agent tunnel up. Resolves to the new public URL, or null. */
53
+ restartAgentTunnel(): Promise<string | null>;
54
+ /** True while supervision must not act (shutdown, nuke, deliberate stop). */
55
+ isPaused(): boolean;
56
+ log: {
57
+ info(message: string, meta?: Record<string, unknown>): void;
58
+ warn(message: string, meta?: Record<string, unknown>): void;
59
+ };
60
+ /** Injectable clock (tests). Defaults to `Date.now`. */
61
+ now?(): number;
62
+ }
63
+ /**
64
+ * Polls agent-tunnel liveness and restarts it with a capped exponential
65
+ * backoff. Ticks never overlap and never reject — a supervisor that throws
66
+ * would take the daemon's whole self-healing story down with it.
67
+ */
68
+ export declare class AgentTunnelSupervisor {
69
+ private readonly deps;
70
+ private readonly intervalMs;
71
+ private timer;
72
+ private ticking;
73
+ private attempts;
74
+ private nextAttemptAt;
75
+ constructor(deps: AgentTunnelSupervisorDeps, intervalMs?: number);
76
+ private now;
77
+ /** Begin polling. Idempotent. */
78
+ start(): void;
79
+ /** Stop polling and reset the backoff. Idempotent. */
80
+ stop(): void;
81
+ get isRunning(): boolean;
82
+ /**
83
+ * Run one liveness check (+ restart if needed). Exposed so the spawn-side
84
+ * `exited` handler can trigger immediate recovery, and so tests can drive
85
+ * the policy deterministically.
86
+ */
87
+ tick(): Promise<SupervisorTickResult>;
88
+ /** Hold off the next restart for the current attempt's backoff window. */
89
+ private armBackoff;
90
+ }
package/dist/index.d.ts CHANGED
@@ -13,7 +13,169 @@
13
13
  * Migrated to consume `@vibecontrols/plugin-sdk` for the contract,
14
14
  * lifecycle, telemetry, logger and subprocess helpers.
15
15
  */
16
- import type { VibePluginFactory } from "@vibecontrols/plugin-sdk/contract";
16
+ import type { HostServices, VibePluginFactory } from "@vibecontrols/plugin-sdk/contract";
17
+ import type { IssueSessionRequest, TunnelInfo, TunnelProvider, TunnelProviderCapabilities, TunnelSessionInfo } from "./types.js";
18
+ /**
19
+ * Should the agent-tunnel supervisor keep its hands off?
20
+ *
21
+ * Pulled out as a pure rule because the answer has to survive a *race*: a
22
+ * restart takes seconds, and a teardown can begin at any point inside it. The
23
+ * transient `intentionalStops` marker is cleared as soon as `stop()` returns,
24
+ * so an in-flight tick could see it gone and resurrect a tunnel the operator
25
+ * just stopped — hence `agentTunnelDisabled`, which latches until something
26
+ * deliberately starts the agent tunnel again.
27
+ */
28
+ export declare function isAgentSupervisionPaused(state: {
29
+ shuttingDown: boolean;
30
+ agentTunnelDisabled: boolean;
31
+ agentTunnelId: string | null;
32
+ intentionalStops: ReadonlySet<string>;
33
+ }): boolean;
34
+ /**
35
+ * Strip the markers that describe a FAILED attempt (`degraded` +
36
+ * `degradedReason`, set when trycloudflare rate-limits us). They belong to the
37
+ * attempt, not to the tunnel, so they must not survive a successful one.
38
+ */
39
+ export declare function withoutDegradedMarkers(metadata: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
40
+ /**
41
+ * True when a tunnel record describes something the agent is actually
42
+ * reachable through: a real URL served by a live process we know the PID of.
43
+ * The rate-limited placeholder (`https://rate-limited-*`, no PID) is the case
44
+ * this exists to reject — publishing it would advertise a hostname that has
45
+ * never resolved.
46
+ */
47
+ export declare function isUsableAgentTunnel(info: TunnelInfo): boolean;
48
+ export declare class CloudflareTunnelProvider implements TunnelProvider {
49
+ readonly name = "tunnel-cloudflare";
50
+ /** In-memory map of tunnel ID → spawned Subprocess. */
51
+ private readonly processes;
52
+ private readonly storage;
53
+ private readonly log;
54
+ private readonly hostServices;
55
+ /**
56
+ * Agent-tunnel supervision state. The agent tunnel (and only the agent
57
+ * tunnel) is kept alive automatically: it is the machine's single inbound
58
+ * path, so losing it silently is what makes an agent look Degraded.
59
+ */
60
+ private readonly supervisor;
61
+ /** Local port the agent tunnel fronts — needed to rebuild it after a crash. */
62
+ private agentTunnelPort;
63
+ /** Tunnel record id of the agent tunnel, when this provider spawned it. */
64
+ private agentTunnelId;
65
+ /** Set while the provider is tearing down (shutdown / nuke). */
66
+ private shuttingDown;
67
+ /**
68
+ * Latched when the agent tunnel is deliberately stopped or deleted, and
69
+ * cleared only by `startAgentTunnel`. Unlike `intentionalStops` this
70
+ * outlives the stop call itself, so a restart still in flight can't resume
71
+ * afterwards and re-create what the caller just took down.
72
+ */
73
+ private agentTunnelDisabled;
74
+ /** Tunnel ids being stopped on purpose — their exits are not crashes. */
75
+ private readonly intentionalStops;
76
+ /**
77
+ * Per-tunnel start counter. A `start()` that loses the race (its cloudflared
78
+ * died while a newer attempt already took over the record) must not write
79
+ * its failure over the newer attempt's `active` row.
80
+ */
81
+ private readonly startEpoch;
82
+ constructor(hostServices: HostServices);
83
+ /**
84
+ * True while nobody wants the agent tunnel supervised — a teardown
85
+ * (shutdown / nuke / detach) or a deliberate stop of the agent tunnel is in
86
+ * flight. Checked on entry to a tick AND either side of a restart, because
87
+ * building a tunnel takes seconds and a teardown can start mid-flight.
88
+ */
89
+ private isSupervisionPaused;
90
+ /** PID recorded for the agent tunnel, or null when none is persisted. */
91
+ private readAgentTunnelPid;
92
+ /**
93
+ * Forget the agent tunnel's URL + PID. Called the moment the owning process
94
+ * is found dead so no consumer can keep publishing a `*.trycloudflare.com`
95
+ * hostname that has already gone NXDOMAIN.
96
+ */
97
+ private clearAgentTunnelKeys;
98
+ /** Persist the agent tunnel's public URL + owning PID. */
99
+ private persistAgentTunnel;
100
+ /**
101
+ * Rebuild the agent tunnel after its `cloudflared` died. Re-uses the
102
+ * existing tunnel record when we own one (same id, fresh process, new
103
+ * public URL — exactly what `rotate()` does) so a flapping tunnel doesn't
104
+ * accumulate a record per crash; otherwise falls back to a full
105
+ * `startAgentTunnel`, which also covers the adopted-bootstrap case where no
106
+ * record of our own exists.
107
+ *
108
+ * Returns the new public URL, or null when the restart produced nothing
109
+ * usable (unknown port, or a rate-limited placeholder) so the supervisor
110
+ * backs off instead of treating it as recovered.
111
+ */
112
+ private restartAgentTunnel;
113
+ /**
114
+ * Re-use the existing tunnel record when we own one (same id, fresh process,
115
+ * new public URL — exactly what `rotate()` does) so a flapping tunnel doesn't
116
+ * accumulate a record per crash. Falls back to the full start path, which
117
+ * also covers the adopted-bootstrap case where no record of our own exists.
118
+ *
119
+ * Deliberately calls `startAgentTunnelInternal` rather than the public
120
+ * `startAgentTunnel`: the latter clears `shuttingDown`, which would undo a
121
+ * teardown that began while this restart was in flight.
122
+ */
123
+ private buildReplacementTunnel;
124
+ /**
125
+ * React to an agent-tunnel process dying without waiting for the next poll.
126
+ * Fire-and-forget: supervision must never throw into a process-exit handler.
127
+ */
128
+ private onAgentTunnelExited;
129
+ /** Load the persisted tunnel list. */
130
+ private loadTunnels;
131
+ /** Persist the tunnel list. */
132
+ private saveTunnels;
133
+ /** Update a single tunnel record in the persisted list. */
134
+ private upsertTunnel;
135
+ /** Remove a tunnel record from the persisted list. */
136
+ private removeTunnelRecord;
137
+ getCapabilities(): TunnelProviderCapabilities;
138
+ issueSession(req: IssueSessionRequest): Promise<TunnelSessionInfo>;
139
+ /**
140
+ * Start a previously-issued tunnel. Spawns cloudflared and extracts the
141
+ * public URL from its output.
142
+ */
143
+ start(tunnelId: string): Promise<TunnelInfo>;
144
+ rotate(tunnelId: string): Promise<TunnelSessionInfo>;
145
+ attachCustomDomain(_tunnelId: string, _domain: string): Promise<void>;
146
+ detachCustomDomain(_tunnelId: string, _domain: string): Promise<void>;
147
+ listSessions(tunnelId: string): Promise<TunnelSessionInfo[]>;
148
+ stop(tunnelId: string): Promise<void>;
149
+ private stopInternal;
150
+ getStatus(tunnelId: string): Promise<TunnelInfo | null>;
151
+ getActiveTunnelUrl(): Promise<string | null>;
152
+ list(): Promise<TunnelInfo[]>;
153
+ delete(tunnelId: string): Promise<void>;
154
+ /**
155
+ * Detach from all tracked processes without killing them.
156
+ * Used in AGENT_TUNNEL_DETACH mode so the tunnel survives a server restart.
157
+ * Storage is preserved so the next startAgentTunnel can reconnect.
158
+ */
159
+ detachAll(): void;
160
+ healthCheck(): Promise<{
161
+ ok: boolean;
162
+ message?: string;
163
+ }>;
164
+ /**
165
+ * Start the "agent tunnel" — the primary tunnel pointing at the agent's
166
+ * own HTTP port so the agent is reachable from the internet.
167
+ *
168
+ * If AGENT_TUNNEL_DETACH=true and a previous cloudflared process is still
169
+ * alive (e.g. after a watch-mode restart), reuses that process instead of
170
+ * spawning a new one. This keeps the tunnel URL stable across hot-reloads.
171
+ */
172
+ startAgentTunnel(agentPort: number): Promise<TunnelInfo>;
173
+ private startAgentTunnelInternal;
174
+ /**
175
+ * Tear down every tunnel this provider is tracking and clear storage.
176
+ */
177
+ stopAll(): Promise<void>;
178
+ }
17
179
  /**
18
180
  * Plugin contract V2 factory. Builds a fresh VibePlugin (with its own
19
181
  * lifecycle/telemetry instances and providers bag) per call. The
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // @bun
2
2
  import{Elysia as Elysia2}from"elysia";import{Elysia}from"elysia";function createLifecycleHooks(spec){let{name,onInit,onShutdown,onNuke,telemetryEventName,skipPlatforms}=spec;return{onServerStart:async(_app,hostServices)=>{if(skipPlatforms&&skipPlatforms.includes(process.platform)){process.stderr.write(`[${name}] skipping init on unsupported platform '${process.platform}'
3
- `);return}if(onInit)await onInit(hostServices);if(telemetryEventName)hostServices.telemetry?.emit(telemetryEventName,{plugin:name})},onServerStop:async(hostServices)=>{if(onShutdown)await onShutdown(hostServices)},onNuke:async(hostServices,ctx)=>{if(onNuke)return onNuke(hostServices,ctx)}}}var TelemetryEmitter=class{constructor(pluginName,pluginVersion,hostServices){this.pluginName=pluginName,this.pluginVersion=pluginVersion,this.hostServices=hostServices}pluginName;pluginVersion;hostServices;emit(eventName,payload){let target=this.hostServices?.telemetry;if(!target)return;target.emit(eventName,{plugin:this.pluginName,version:this.pluginVersion,timestamp:new Date().toISOString(),...payload??{}})}emitReady(context){this.emit(`${this.pluginName}.ready`,context)}emitError(error,context){this.emit(`${this.pluginName}.error`,{message:error.message,...context??{}})}emitEvent(type,payload){this.emit(type,payload)}},BoundLogger=class{constructor(logger,source){this.logger=logger,this.source=source}logger;source;info(message,meta){this.logger?.info?.(this.source,message,meta)}warn(message,meta){this.logger?.warn?.(this.source,message,meta)}error(message,meta){this.logger?.error?.(this.source,message,meta)}debug(message,meta){this.logger?.debug?.(this.source,message,meta)}};function sleep(ms){return new Promise((resolve)=>setTimeout(resolve,ms))}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}async function gracefulKill(pid,timeoutMs=3000,logger){if(!isProcessAlive(pid))return;try{process.kill(pid,"SIGTERM")}catch(err){logger?.warn?.("subprocess","SIGTERM failed",{pid,message:err instanceof Error?err.message:String(err)});return}let pollMs=50,elapsed=0;while(elapsed<timeoutMs)if(await sleep(pollMs),elapsed+=pollMs,!isProcessAlive(pid))return;try{process.kill(pid,"SIGKILL")}catch(err){logger?.warn?.("subprocess","SIGKILL failed",{pid,message:err instanceof Error?err.message:String(err)})}}var CONTEXT_PROVIDER_TYPE="context",REGISTRY_KEY=Symbol.for("@vibecontrols/plugin-sdk:contextProviders@1");function getRegistry(){let slots=globalThis,existing=slots[REGISTRY_KEY];if(existing)return existing;let created=new Map;return slots[REGISTRY_KEY]=created,created}function registerContextProvider(provider,hostServices){if(!provider||typeof provider.name!=="string"||provider.name.length===0)throw Error("ContextProvider.name is required");if(typeof provider.getContext!=="function")throw Error("ContextProvider.getContext must be a function");getRegistry().set(provider.name,provider),hostServices?.serviceRegistry?.registerProvider?.(CONTEXT_PROVIDER_TYPE,provider,provider.name)}function listContextProviders(){return Array.from(getRegistry().values())}function getContextProvider(name){return getRegistry().get(name)}var ProviderRegistry=class{constructor(hostServices){this.hostServices=hostServices}hostServices;registerContextProvider(provider){registerContextProvider(provider,this.hostServices)}listContextProviders(){return listContextProviders()}getContextProvider(name){return getContextProvider(name)}getServiceRegistry(){return this.hostServices?.serviceRegistry}registerProvider(type,name,provider){this.hostServices?.serviceRegistry?.registerProvider?.(type,provider,name)}getProvider(type,name){let reg=this.hostServices?.serviceRegistry;if(!reg)return;if(name===void 0)return reg.getProvider?.(type);if(reg.getProviderByName)return reg.getProviderByName(type,name);if(!reg.listProvidersForType)return;if(!(reg.listProvidersForType(type)??[]).some((entry)=>typeof entry==="string"?entry===name:entry.pluginName===name))return;return reg.getProvider?.(type)}listProviders(type){return(this.hostServices?.serviceRegistry?.listProvidersForType?.(type)??[]).map((entry)=>typeof entry==="string"?entry:entry.pluginName)}withCliContribution(contribution){let contributors=this.hostServices?.cliContributors;if(!contributors)return;for(let section of contribution.statusSections??[])contributors.addStatusSection?.(section);for(let check of contribution.doctorChecks??[])contributors.addDoctorCheck?.(check)}};import{spawn}from"child_process";import{createHash}from"crypto";import{existsSync,promises,createWriteStream}from"fs";import{homedir,tmpdir}from"os";import*as path from"path";import{Readable}from"stream";import{pipeline}from"stream/promises";function currentPlatform(){let arch=process.arch==="x64"?"x64":"arm64";return`${process.platform==="win32"?"win32":process.platform==="darwin"?"darwin":"linux"}-${arch}`}function toolsCacheRoot(){return path.join(homedir(),".boff","vibecontrols","tools")}function binFileName(name,binaryName){let base=binaryName??name;if(process.platform==="win32"&&!base.toLowerCase().endsWith(".exe"))return`${base}.exe`;return base}function cachedBinaryPath(name,binaryName,cacheRoot){return path.join(cacheRoot??toolsCacheRoot(),name,binFileName(name,binaryName))}function resolveBinary(name,binaryName,cacheRoot){let cached=cachedBinaryPath(name,binaryName,cacheRoot);if(existsSync(cached))return cached;try{let onPath=typeof Bun<"u"&&typeof Bun.which==="function"?Bun.which(binaryName??name,{PATH:process.env.PATH}):null;if(onPath)return onPath}catch{}return null}async function versionOk(binaryPath,spec){if(!spec.versionMatcher)return!0;return new Promise((resolve)=>{let child=spawn(binaryPath,spec.versionArgs??["--version"],{stdio:["ignore","pipe","pipe"]}),out="";child.stdout.on("data",(b)=>out+=b.toString()),child.stderr.on("data",(b)=>out+=b.toString()),child.on("close",()=>{try{resolve(new RegExp(spec.versionMatcher).test(out))}catch{resolve(!1)}}),child.on("error",()=>resolve(!1))})}async function installBinary(spec){let platform=spec.platform??currentPlatform(),binaryName=binFileName(spec.name,spec.binaryName),cached=cachedBinaryPath(spec.name,spec.binaryName,spec.cacheRoot);if(existsSync(cached)&&await versionOk(cached,spec))return cached;try{let onPath=typeof Bun<"u"&&typeof Bun.which==="function"?Bun.which(spec.binaryName??spec.name,{PATH:process.env.PATH}):null;if(onPath&&await versionOk(onPath,spec))return spec.log?.info?.(`[install] using PATH binary ${spec.name} (${onPath})`),onPath}catch{}let download=spec.downloads[platform];if(!download)throw Error(`[install] no download manifest entry for ${spec.name} on ${platform}`);return spec.log?.info?.(`[install] downloading ${spec.name} for ${platform}`),await promises.mkdir(path.dirname(cached),{recursive:!0}),await downloadAndInstall(download,cached,binaryName,spec.name),spec.log?.info?.(`[install] installed ${spec.name} \u2192 ${cached}`),cached}async function downloadAndInstall(d,destBinary,binaryName,toolName){let destDir=path.dirname(destBinary),tmp=path.join(tmpdir(),`vibe-install-${toolName}-${process.pid}`);await promises.mkdir(tmp,{recursive:!0});let archivePath=path.join(tmp,"artifact");try{let res=await fetch(d.url,{redirect:"follow"});if(!res.ok)throw Error(`download failed (${res.status}) for ${d.url}`);if(!res.body)throw Error(`empty response body for ${d.url}`);if(await pipeline(Readable.fromWeb(res.body),createWriteStream(archivePath)),d.sha256){let actual=createHash("sha256").update(await promises.readFile(archivePath)).digest("hex");if(actual!==d.sha256)throw Error(`sha256 mismatch for ${toolName}: expected ${d.sha256}, got ${actual}`)}let archiveType=d.archive??(d.url.endsWith(".tar.gz")||d.url.endsWith(".tgz")?"tar.gz":d.url.endsWith(".zip")?"zip":"raw");if(archiveType==="raw")await promises.copyFile(archivePath,destBinary);else{if(archiveType==="tar.gz")await runProcess("tar",["-xzf",archivePath,"-C",tmp]);else await extractZip(archivePath,tmp);let inner=d.binaryWithinArchive??binaryName;await promises.copyFile(path.join(tmp,inner),destBinary)}if(process.platform!=="win32")await promises.chmod(destBinary,493)}finally{await promises.rm(tmp,{recursive:!0,force:!0})}}async function extractZip(archive,dest){try{await runProcess("unzip",["-q","-o",archive,"-d",dest]);return}catch(err){if(process.platform!=="win32")throw err;await runProcess("tar",["-xf",archive,"-C",dest])}}async function runProcess(cmd,args){return new Promise((resolve,reject)=>{let child=spawn(cmd,args,{stdio:["ignore","ignore","pipe"]}),err="";child.stderr.on("data",(b)=>err+=b.toString()),child.on("close",(code)=>code===0?resolve():reject(Error(`${cmd} exit ${code}: ${err}`))),child.on("error",reject)})}var CLOUDFLARED_BASE="https://github.com/cloudflare/cloudflared/releases/latest/download",CLOUDFLARED_DOWNLOADS={"linux-x64":{url:`${CLOUDFLARED_BASE}/cloudflared-linux-amd64`,archive:"raw"},"linux-arm64":{url:`${CLOUDFLARED_BASE}/cloudflared-linux-arm64`,archive:"raw"},"darwin-x64":{url:`${CLOUDFLARED_BASE}/cloudflared-darwin-amd64.tgz`,archive:"tar.gz",binaryWithinArchive:"cloudflared"},"darwin-arm64":{url:`${CLOUDFLARED_BASE}/cloudflared-darwin-arm64.tgz`,archive:"tar.gz",binaryWithinArchive:"cloudflared"},"win32-x64":{url:`${CLOUDFLARED_BASE}/cloudflared-windows-amd64.exe`,archive:"raw"},"win32-arm64":{url:`${CLOUDFLARED_BASE}/cloudflared-windows-arm64.exe`,archive:"raw"}};function resolveCloudflaredCmd(){return resolveBinary("cloudflared")??(process.platform==="win32"?"cloudflared.exe":"cloudflared")}var PLUGIN_NAME="tunnel-cloudflare",PLUGIN_VERSION="2026.509.3",PROVIDER_NAME="tunnel-cloudflare",STORAGE_NS="tunnel-cloudflare",KEY_TUNNELS="tunnels",KEY_AGENT_URL="agent-tunnel-url",KEY_AGENT_PID="agent-tunnel-pid",TUNNEL_URL_RE=/(https:\/\/(?!api\.)[a-zA-Z0-9-]+\.trycloudflare\.com)/,URL_EXTRACT_TIMEOUT_MS=30000,KILL_GRACE_MS=3000;class CloudflaredRateLimitedError extends Error{constructor(detail){super(`cloudflared rate-limited by trycloudflare.com: ${detail}`);this.name="CloudflaredRateLimitedError"}}async function extractTunnelUrl(proc){let combined="",readStream=async(stream)=>{if(!stream)return;let reader=stream.getReader(),decoder=new TextDecoder;try{while(!0){let{done,value}=await reader.read();if(done)break;combined+=decoder.decode(value,{stream:!0});let match=TUNNEL_URL_RE.exec(combined);if(match)return match[1]}}finally{reader.releaseLock()}return},result=await Promise.race([readStream(proc.stdout),readStream(proc.stderr),new Promise((_,reject)=>setTimeout(()=>reject(Error(`Timed out after ${URL_EXTRACT_TIMEOUT_MS}ms waiting for tunnel URL`)),URL_EXTRACT_TIMEOUT_MS))]);if(!result){if(/429|too many requests|error code: 1015/i.test(combined))throw new CloudflaredRateLimitedError(combined.split(`
4
- `).find((l)=>/429|1015/i.test(l))??combined);throw Error("cloudflared exited before producing a URL")}return result}class CloudflareTunnelProvider{name=PROVIDER_NAME;processes=new Map;storage;log;hostServices;constructor(hostServices){this.hostServices=hostServices,this.storage=hostServices.storage,this.log=new BoundLogger(hostServices.logger,PROVIDER_NAME)}async loadTunnels(){let raw=await this.storage.get(STORAGE_NS,KEY_TUNNELS);if(!raw)return[];try{return JSON.parse(raw)}catch{return this.log.warn("Corrupt tunnel list in storage \u2014 resetting"),[]}}async saveTunnels(tunnels){await this.storage.set(STORAGE_NS,KEY_TUNNELS,JSON.stringify(tunnels))}async upsertTunnel(info){let tunnels=await this.loadTunnels(),idx=tunnels.findIndex((t)=>t.id===info.id);if(idx>=0)tunnels[idx]=info;else tunnels.push(info);await this.saveTunnels(tunnels)}async removeTunnelRecord(tunnelId){let tunnels=await this.loadTunnels();await this.saveTunnels(tunnels.filter((t)=>t.id!==tunnelId))}getCapabilities(){return{provider:PROVIDER_NAME,supportsHttp:!0,supportsHttps:!0,supportsTcp:!1,supportsUdp:!1,supportsCustomDomains:!1,supportsManagedSubdomains:!0,supportsSessionTokens:!1,supportsLiveLogs:!0,supportsUsageMetrics:!1,supportsRotateCredentials:!0,platforms:["darwin","linux","win32"]}}async issueSession(req){let tunnelId=(typeof req.metadata?.backendTunnelId==="string"?req.metadata.backendTunnelId:void 0)??crypto.randomUUID(),sessionId=crypto.randomUUID(),localHost=req.localHost??"127.0.0.1",protocol=req.protocol,now=new Date().toISOString(),info={id:tunnelId,providerName:PROVIDER_NAME,status:"stopped",protocol,localPort:req.localPort,localHost,url:"",sessionId,createdAt:now,metadata:{...req.metadata??{},localUrl:`${protocol}://${localHost}:${req.localPort}`}};await this.upsertTunnel(info);let session={sessionId,tunnelId,provider:PROVIDER_NAME,expiresAt:req.ttlSeconds?new Date(Date.now()+req.ttlSeconds*1000).toISOString():void 0,credentials:{localUrl:info.metadata.localUrl}};return await this.storage.set(STORAGE_NS,`sessions:${tunnelId}`,JSON.stringify([session])),session}async start(tunnelId){let info=(await this.loadTunnels()).find((t)=>t.id===tunnelId);if(!info)throw Error(`Tunnel ${tunnelId} not found (issueSession first)`);if(this.processes.has(tunnelId))return info;let localUrl=info.metadata?.localUrl??`${info.protocol}://${info.localHost}:${info.localPort}`;await this.upsertTunnel({...info,status:"starting",updatedAt:new Date().toISOString()}),this.log.info(`Starting tunnel ${tunnelId} \u2192 ${localUrl}`);let proc=Bun.spawn([resolveCloudflaredCmd(),"tunnel","--url",localUrl],{stdin:"ignore",stdout:"pipe",stderr:"pipe"});this.processes.set(tunnelId,proc),proc.exited.then((code)=>{if(this.processes.has(tunnelId))this.log.warn(`Tunnel ${tunnelId} process exited unexpectedly (code=${code})`),this.processes.delete(tunnelId),this.loadTunnels().then((current)=>{let idx=current.findIndex((t)=>t.id===tunnelId);if(idx>=0)current[idx]={...current[idx],status:"error",updatedAt:new Date().toISOString(),metadata:{...current[idx].metadata,exitCode:code}},this.saveTunnels(current)})});try{let url=await extractTunnelUrl(proc),activeInfo={...info,url,managedHostname:new URL(url).host,status:"active",pid:proc.pid,updatedAt:new Date().toISOString()};return await this.upsertTunnel(activeInfo),this.log.info(`Tunnel ${tunnelId} active at ${url} (PID ${proc.pid})`),activeInfo}catch(err){if(this.processes.delete(tunnelId),isProcessAlive(proc.pid))await gracefulKill(proc.pid,KILL_GRACE_MS);if(err instanceof CloudflaredRateLimitedError){let placeholderHost=`rate-limited-${tunnelId}.trycloudflare.com`,placeholderUrl=`https://${placeholderHost}`,degraded={...info,url:placeholderUrl,managedHostname:placeholderHost,status:"active",updatedAt:new Date().toISOString(),metadata:{...info.metadata,degraded:!0,degradedReason:err.message}};return await this.upsertTunnel(degraded),this.log.warn(`Tunnel ${tunnelId} rate-limited; returning placeholder URL`),degraded}let errorInfo={...info,status:"error",updatedAt:new Date().toISOString(),metadata:{...info.metadata,error:err instanceof Error?err.message:String(err)}};throw await this.upsertTunnel(errorInfo),err}}async rotate(tunnelId){if(!(await this.loadTunnels()).find((t)=>t.id===tunnelId))throw Error(`Tunnel ${tunnelId} not found`);await this.stop(tunnelId),await this.start(tunnelId);let updated=(await this.loadTunnels()).find((t)=>t.id===tunnelId);return{sessionId:updated?.sessionId??crypto.randomUUID(),tunnelId,provider:PROVIDER_NAME,credentials:{localUrl:updated?.metadata?.localUrl}}}async attachCustomDomain(_tunnelId,_domain){throw Error("Custom domains are not supported by the cloudflared quick-tunnel provider. Use the vibetunnels provider or wait for named-tunnel support.")}async detachCustomDomain(_tunnelId,_domain){throw Error("Custom domains are not supported by the cloudflared quick-tunnel provider.")}async listSessions(tunnelId){let raw=await this.storage.get(STORAGE_NS,`sessions:${tunnelId}`);if(!raw)return[];try{return JSON.parse(raw)}catch{return[]}}async stop(tunnelId){this.log.info(`Stopping tunnel ${tunnelId}`);let tunnel=(await this.loadTunnels()).find((t)=>t.id===tunnelId);if(tunnel)tunnel.status="stopping",tunnel.updatedAt=new Date().toISOString(),await this.upsertTunnel(tunnel);let proc=this.processes.get(tunnelId);if(proc)await gracefulKill(proc.pid,KILL_GRACE_MS),this.processes.delete(tunnelId);else if(tunnel?.pid&&isProcessAlive(tunnel.pid))await gracefulKill(tunnel.pid,KILL_GRACE_MS);if(tunnel)tunnel.status="stopped",tunnel.pid=void 0,tunnel.updatedAt=new Date().toISOString(),await this.upsertTunnel(tunnel);let agentPidRaw=await this.storage.get(STORAGE_NS,KEY_AGENT_PID);if(agentPidRaw&&tunnel?.pid&&String(tunnel.pid)===agentPidRaw)await this.storage.delete(STORAGE_NS,KEY_AGENT_URL),await this.storage.delete(STORAGE_NS,KEY_AGENT_PID);this.log.info(`Tunnel ${tunnelId} stopped`)}async getStatus(tunnelId){let tunnel=(await this.loadTunnels()).find((t)=>t.id===tunnelId)??null;if(!tunnel)return null;if(tunnel.status==="active"&&tunnel.pid){if(!isProcessAlive(tunnel.pid))tunnel.status="stopped",tunnel.updatedAt=new Date().toISOString(),await this.upsertTunnel(tunnel),this.processes.delete(tunnelId)}return tunnel}async getActiveTunnelUrl(){let envUrl=process.env.AGENT_TUNNEL_URL;if(envUrl)return envUrl;return this.storage.get(STORAGE_NS,KEY_AGENT_URL)}async list(){return this.loadTunnels()}async delete(tunnelId){this.log.info(`Deleting tunnel ${tunnelId}`);let tunnel=(await this.loadTunnels()).find((t)=>t.id===tunnelId);if(tunnel&&(tunnel.status==="active"||tunnel.status==="starting"))await this.stop(tunnelId);await this.removeTunnelRecord(tunnelId),this.log.info(`Tunnel ${tunnelId} deleted`)}detachAll(){this.log.info("Detaching from all tunnels (processes left running)"),this.processes.clear()}async healthCheck(){try{let result=Bun.spawnSync([resolveCloudflaredCmd(),"--version"],{stdout:"pipe",stderr:"pipe",timeout:1e4});if(result.exitCode!==0)return{ok:!1,message:"cloudflared not available"};return{ok:!0,message:result.stdout.toString().trim()}}catch(err){return{ok:!1,message:err instanceof Error?`cloudflared not available: ${err.message}`:"cloudflared not available"}}}async startAgentTunnel(agentPort){let profileSuffix=(process.env.VIBECONTROLS_PROFILE??"default").replace(/[^A-Za-z0-9_]/g,"_"),envBootstrapPid=process.env[`AGENT_TUNNEL_PID_${profileSuffix}`],envBootstrapUrl=process.env[`AGENT_TUNNEL_URL_${profileSuffix}`];if(envBootstrapPid&&envBootstrapUrl){let pid=parseInt(envBootstrapPid,10);if(!isNaN(pid)&&isProcessAlive(pid)){this.log.info(`Adopting bootstrap cloudflared (pid=${pid}) at ${envBootstrapUrl}`),await this.storage.set(STORAGE_NS,KEY_AGENT_URL,envBootstrapUrl),await this.storage.set(STORAGE_NS,KEY_AGENT_PID,String(pid));let adopted={id:`agent-bootstrap-${pid}`,providerName:"cloudflare",status:"active",protocol:"http",localPort:agentPort,localHost:"127.0.0.1",url:envBootstrapUrl,pid,createdAt:new Date().toISOString(),metadata:{isAgentTunnel:!0,name:"agent",adopted:!0}};return delete process.env[`AGENT_TUNNEL_PID_${profileSuffix}`],delete process.env[`AGENT_TUNNEL_URL_${profileSuffix}`],adopted}}let storedPidStr=await this.storage.get(STORAGE_NS,KEY_AGENT_PID),storedUrl=await this.storage.get(STORAGE_NS,KEY_AGENT_URL);if(storedPidStr&&storedUrl){let storedPid=parseInt(storedPidStr,10);if(!isNaN(storedPid)&&isProcessAlive(storedPid)){this.log.info(`Reusing existing agent tunnel (pid=${storedPid}) at ${storedUrl}`);let existing=(await this.loadTunnels()).find((t)=>t.pid===storedPid&&t.metadata?.isAgentTunnel);if(existing)return existing;return{id:`agent-stored-${storedPid}`,providerName:"cloudflare",status:"active",protocol:"http",localPort:agentPort,localHost:"127.0.0.1",url:storedUrl,pid:storedPid,createdAt:new Date().toISOString(),metadata:{isAgentTunnel:!0,name:"agent",adopted:!0}}}}this.log.info(`Starting agent tunnel on port ${agentPort}`);let session=await this.issueSession({protocol:"http",localPort:agentPort,localHost:"127.0.0.1",metadata:{isAgentTunnel:!0,name:"agent"}}),info=await this.start(session.tunnelId);if(await this.storage.set(STORAGE_NS,KEY_AGENT_URL,info.url),info.pid!==void 0)await this.storage.set(STORAGE_NS,KEY_AGENT_PID,String(info.pid));return this.log.info(`Agent tunnel active at ${info.url}`),info}async stopAll(){this.log.info("Stopping all tunnels");let tunnels=await this.loadTunnels();await Promise.allSettled(tunnels.filter((t)=>t.status==="active"||t.status==="starting").map((t)=>this.stop(t.id)));for(let[id,proc]of this.processes)if(isProcessAlive(proc.pid))this.log.warn(`Killing orphaned process ${proc.pid} (tunnel ${id})`),await gracefulKill(proc.pid,KILL_GRACE_MS);this.processes.clear(),await this.storage.deleteAll(STORAGE_NS),this.log.info("All tunnels stopped and storage cleared")}}var provider=null;function createPrereqsRoutes(){return new Elysia2({prefix:"/prereqs"}).get("/status",()=>{let cf=resolveBinary("cloudflared");return{satisfied:!!cf,missing:cf?[]:[{name:"cloudflared",kind:"binary",requiresSudo:!1,detected:void 0}]}}).post("/install",async()=>{if(resolveBinary("cloudflared"))return{ok:!0,installed:[],pendingSudo:[],errors:[]};try{return await installBinary({name:"cloudflared",downloads:CLOUDFLARED_DOWNLOADS,versionMatcher:"cloudflared version"}),{ok:!0,installed:["cloudflared"],pendingSudo:[],errors:[]}}catch(err){let manual=process.platform==="darwin"?"brew install cloudflared":process.platform==="win32"?"winget install Cloudflare.cloudflared # or download from https://github.com/cloudflare/cloudflared/releases":"see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/";return{ok:!1,installed:[],pendingSudo:[{name:"cloudflared",command:manual,reason:`cloudflared auto-download failed: ${err instanceof Error?err.message:String(err)}`}],errors:[err instanceof Error?err.message:String(err)]}}}).post("/uninstall",()=>({ok:!0}))}var createPlugin=(_ctx)=>{let telemetry=new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION),plugin={capabilities:{storage:"rw",subprocess:!0,telemetry:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"Cloudflare Tunnel provider for remote access",tags:["backend","provider"],apiPrefix:"/api/tunnel-cloudflare",tunnelDomainSuffixes:[".trycloudflare.com"],prerequisites:[{name:"cloudflared",kind:"binary",requiresSudo:!1,description:"Cloudflare tunnel daemon (auto-downloaded to the agent binary cache)"}],providers:{},createRoutes:()=>createPrereqsRoutes(),onServerStart:void 0,onServerStop:void 0},lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"tunnel.provider.ready",onInit:async(hostServices)=>{let log=new BoundLogger(hostServices.logger,PROVIDER_NAME);if(log.info("Plugin initialising"),provider=new CloudflareTunnelProvider(hostServices),plugin.providers={tunnel:provider},new ProviderRegistry(hostServices).registerProvider("tunnel",PROVIDER_NAME,provider),telemetry.emit("tunnel.provider.ready",{provider:"cloudflare"}),process.env.AGENT_TUNNEL==="false"){log.info("AGENT_TUNNEL=false \u2014 provider registered, tunnel managed externally");return}let health=await provider.healthCheck();if(!health.ok){log.warn(`cloudflared is not available \u2014 tunnel features will fail. ${health.message??""}`);return}log.info(health.message??"cloudflared available");try{let baseUrl=hostServices.getAgentBaseUrl?.()??"",parsed=new URL(baseUrl),agentPort=parseInt(parsed.port,10);if(!Number.isFinite(agentPort)||agentPort<=0){log.warn(`Cannot determine agent port from "${baseUrl}" \u2014 skipping agent tunnel`);return}await provider.startAgentTunnel(agentPort)}catch(err){log.error(`Failed to start agent tunnel: ${err instanceof Error?err.message:String(err)}`)}},onShutdown:async()=>{if(!provider)return;if(process.env.AGENT_TUNNEL==="false"||process.env.AGENT_TUNNEL_DETACH==="true"){provider.detachAll(),provider=null;return}await provider.stopAll(),provider=null},onNuke:async(_hostServices,ctx)=>{if(!provider)return{notes:["tunnel provider not initialised"]};if(ctx.dryRun)return{reaped:["cloudflared tunnels + tunnel-cloudflare storage"]};return await provider.stopAll(),provider=null,{reaped:["cloudflared tunnels + tunnel-cloudflare storage"]}}});return plugin.onServerStart=lifecycle.onServerStart,plugin.onServerStop=lifecycle.onServerStop,plugin.onNuke=lifecycle.onNuke,plugin};export{createPlugin};
3
+ `);return}if(onInit)await onInit(hostServices);if(telemetryEventName)hostServices.telemetry?.emit(telemetryEventName,{plugin:name})},onServerStop:async(hostServices)=>{if(onShutdown)await onShutdown(hostServices)},onNuke:async(hostServices,ctx)=>{if(onNuke)return onNuke(hostServices,ctx)}}}var TelemetryEmitter=class{constructor(pluginName,pluginVersion,hostServices){this.pluginName=pluginName,this.pluginVersion=pluginVersion,this.hostServices=hostServices}pluginName;pluginVersion;hostServices;emit(eventName,payload){let target=this.hostServices?.telemetry;if(!target)return;target.emit(eventName,{plugin:this.pluginName,version:this.pluginVersion,timestamp:new Date().toISOString(),...payload??{}})}emitReady(context){this.emit(`${this.pluginName}.ready`,context)}emitError(error,context){this.emit(`${this.pluginName}.error`,{message:error.message,...context??{}})}emitEvent(type,payload){this.emit(type,payload)}},BoundLogger=class{constructor(logger,source){this.logger=logger,this.source=source}logger;source;info(message,meta){this.logger?.info?.(this.source,message,meta)}warn(message,meta){this.logger?.warn?.(this.source,message,meta)}error(message,meta){this.logger?.error?.(this.source,message,meta)}debug(message,meta){this.logger?.debug?.(this.source,message,meta)}};function sleep(ms){return new Promise((resolve)=>setTimeout(resolve,ms))}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}async function gracefulKill(pid,timeoutMs=3000,logger){if(!isProcessAlive(pid))return;try{process.kill(pid,"SIGTERM")}catch(err){logger?.warn?.("subprocess","SIGTERM failed",{pid,message:err instanceof Error?err.message:String(err)});return}let pollMs=50,elapsed=0;while(elapsed<timeoutMs)if(await sleep(pollMs),elapsed+=pollMs,!isProcessAlive(pid))return;try{process.kill(pid,"SIGKILL")}catch(err){logger?.warn?.("subprocess","SIGKILL failed",{pid,message:err instanceof Error?err.message:String(err)})}}var CONTEXT_PROVIDER_TYPE="context",REGISTRY_KEY=Symbol.for("@vibecontrols/plugin-sdk:contextProviders@1");function getRegistry(){let slots=globalThis,existing=slots[REGISTRY_KEY];if(existing)return existing;let created=new Map;return slots[REGISTRY_KEY]=created,created}function registerContextProvider(provider,hostServices){if(!provider||typeof provider.name!=="string"||provider.name.length===0)throw Error("ContextProvider.name is required");if(typeof provider.getContext!=="function")throw Error("ContextProvider.getContext must be a function");getRegistry().set(provider.name,provider),hostServices?.serviceRegistry?.registerProvider?.(CONTEXT_PROVIDER_TYPE,provider,provider.name)}function listContextProviders(){return Array.from(getRegistry().values())}function getContextProvider(name){return getRegistry().get(name)}var ProviderRegistry=class{constructor(hostServices){this.hostServices=hostServices}hostServices;registerContextProvider(provider){registerContextProvider(provider,this.hostServices)}listContextProviders(){return listContextProviders()}getContextProvider(name){return getContextProvider(name)}getServiceRegistry(){return this.hostServices?.serviceRegistry}registerProvider(type,name,provider){this.hostServices?.serviceRegistry?.registerProvider?.(type,provider,name)}getProvider(type,name){let reg=this.hostServices?.serviceRegistry;if(!reg)return;if(name===void 0)return reg.getProvider?.(type);if(reg.getProviderByName)return reg.getProviderByName(type,name);if(!reg.listProvidersForType)return;if(!(reg.listProvidersForType(type)??[]).some((entry)=>typeof entry==="string"?entry===name:entry.pluginName===name))return;return reg.getProvider?.(type)}listProviders(type){return(this.hostServices?.serviceRegistry?.listProvidersForType?.(type)??[]).map((entry)=>typeof entry==="string"?entry:entry.pluginName)}withCliContribution(contribution){let contributors=this.hostServices?.cliContributors;if(!contributors)return;for(let section of contribution.statusSections??[])contributors.addStatusSection?.(section);for(let check of contribution.doctorChecks??[])contributors.addDoctorCheck?.(check)}};import{spawn}from"child_process";import{createHash}from"crypto";import{existsSync,promises,createWriteStream}from"fs";import{homedir,tmpdir}from"os";import*as path from"path";import{Readable}from"stream";import{pipeline}from"stream/promises";function currentPlatform(){let arch=process.arch==="x64"?"x64":"arm64";return`${process.platform==="win32"?"win32":process.platform==="darwin"?"darwin":"linux"}-${arch}`}function toolsCacheRoot(){return path.join(homedir(),".boff","vibecontrols","tools")}function binFileName(name,binaryName){let base=binaryName??name;if(process.platform==="win32"&&!base.toLowerCase().endsWith(".exe"))return`${base}.exe`;return base}function cachedBinaryPath(name,binaryName,cacheRoot){return path.join(cacheRoot??toolsCacheRoot(),name,binFileName(name,binaryName))}function resolveBinary(name,binaryName,cacheRoot){let cached=cachedBinaryPath(name,binaryName,cacheRoot);if(existsSync(cached))return cached;try{let onPath=typeof Bun<"u"&&typeof Bun.which==="function"?Bun.which(binaryName??name,{PATH:process.env.PATH}):null;if(onPath)return onPath}catch{}return null}async function versionOk(binaryPath,spec){if(!spec.versionMatcher)return!0;return new Promise((resolve)=>{let child=spawn(binaryPath,spec.versionArgs??["--version"],{stdio:["ignore","pipe","pipe"]}),out="";child.stdout.on("data",(b)=>out+=b.toString()),child.stderr.on("data",(b)=>out+=b.toString()),child.on("close",()=>{try{resolve(new RegExp(spec.versionMatcher).test(out))}catch{resolve(!1)}}),child.on("error",()=>resolve(!1))})}async function installBinary(spec){let platform=spec.platform??currentPlatform(),binaryName=binFileName(spec.name,spec.binaryName),cached=cachedBinaryPath(spec.name,spec.binaryName,spec.cacheRoot);if(existsSync(cached)&&await versionOk(cached,spec))return cached;try{let onPath=typeof Bun<"u"&&typeof Bun.which==="function"?Bun.which(spec.binaryName??spec.name,{PATH:process.env.PATH}):null;if(onPath&&await versionOk(onPath,spec))return spec.log?.info?.(`[install] using PATH binary ${spec.name} (${onPath})`),onPath}catch{}let download=spec.downloads[platform];if(!download)throw Error(`[install] no download manifest entry for ${spec.name} on ${platform}`);return spec.log?.info?.(`[install] downloading ${spec.name} for ${platform}`),await promises.mkdir(path.dirname(cached),{recursive:!0}),await downloadAndInstall(download,cached,binaryName,spec.name),spec.log?.info?.(`[install] installed ${spec.name} \u2192 ${cached}`),cached}async function downloadAndInstall(d,destBinary,binaryName,toolName){let destDir=path.dirname(destBinary),tmp=path.join(tmpdir(),`vibe-install-${toolName}-${process.pid}`);await promises.mkdir(tmp,{recursive:!0});let archivePath=path.join(tmp,"artifact");try{let res=await fetch(d.url,{redirect:"follow"});if(!res.ok)throw Error(`download failed (${res.status}) for ${d.url}`);if(!res.body)throw Error(`empty response body for ${d.url}`);if(await pipeline(Readable.fromWeb(res.body),createWriteStream(archivePath)),d.sha256){let actual=createHash("sha256").update(await promises.readFile(archivePath)).digest("hex");if(actual!==d.sha256)throw Error(`sha256 mismatch for ${toolName}: expected ${d.sha256}, got ${actual}`)}let archiveType=d.archive??(d.url.endsWith(".tar.gz")||d.url.endsWith(".tgz")?"tar.gz":d.url.endsWith(".zip")?"zip":"raw");if(archiveType==="raw")await promises.copyFile(archivePath,destBinary);else{if(archiveType==="tar.gz")await runProcess("tar",["-xzf",archivePath,"-C",tmp]);else await extractZip(archivePath,tmp);let inner=d.binaryWithinArchive??binaryName;await promises.copyFile(path.join(tmp,inner),destBinary)}if(process.platform!=="win32")await promises.chmod(destBinary,493)}finally{await promises.rm(tmp,{recursive:!0,force:!0})}}async function extractZip(archive,dest){try{await runProcess("unzip",["-q","-o",archive,"-d",dest]);return}catch(err){if(process.platform!=="win32")throw err;await runProcess("tar",["-xf",archive,"-C",dest])}}async function runProcess(cmd,args){return new Promise((resolve,reject)=>{let child=spawn(cmd,args,{stdio:["ignore","ignore","pipe"]}),err="";child.stderr.on("data",(b)=>err+=b.toString()),child.on("close",(code)=>code===0?resolve():reject(Error(`${cmd} exit ${code}: ${err}`))),child.on("error",reject)})}var CLOUDFLARED_BASE="https://github.com/cloudflare/cloudflared/releases/latest/download",CLOUDFLARED_DOWNLOADS={"linux-x64":{url:`${CLOUDFLARED_BASE}/cloudflared-linux-amd64`,archive:"raw"},"linux-arm64":{url:`${CLOUDFLARED_BASE}/cloudflared-linux-arm64`,archive:"raw"},"darwin-x64":{url:`${CLOUDFLARED_BASE}/cloudflared-darwin-amd64.tgz`,archive:"tar.gz",binaryWithinArchive:"cloudflared"},"darwin-arm64":{url:`${CLOUDFLARED_BASE}/cloudflared-darwin-arm64.tgz`,archive:"tar.gz",binaryWithinArchive:"cloudflared"},"win32-x64":{url:`${CLOUDFLARED_BASE}/cloudflared-windows-amd64.exe`,archive:"raw"},"win32-arm64":{url:`${CLOUDFLARED_BASE}/cloudflared-windows-arm64.exe`,archive:"raw"}};function restartBackoffMs(attempt){if(attempt<=1)return 0;let delay=2000*2**(attempt-2);return Math.min(60000,delay)}class AgentTunnelSupervisor{deps;intervalMs;timer=null;ticking=!1;attempts=0;nextAttemptAt=0;constructor(deps,intervalMs=15000){this.deps=deps;this.intervalMs=intervalMs}now(){return this.deps.now?this.deps.now():Date.now()}start(){if(this.timer)return;this.timer=setInterval(()=>{this.tick().catch(()=>{})},this.intervalMs),this.timer.unref?.()}stop(){if(this.timer)clearInterval(this.timer),this.timer=null;this.attempts=0,this.nextAttemptAt=0}get isRunning(){return this.timer!==null}async tick(){if(this.ticking)return"backoff";this.ticking=!0;try{if(this.deps.isPaused())return"paused";let pid;try{if(pid=await this.deps.getAgentTunnelPid(),pid!==null&&this.deps.isAlive(pid))return this.attempts=0,this.nextAttemptAt=0,"alive";await this.deps.clearAgentTunnel()}catch(err){return this.deps.log.warn("Agent tunnel liveness check failed",{error:err instanceof Error?err.message:String(err)}),"failed"}if(this.now()<this.nextAttemptAt)return"backoff";this.attempts+=1,this.deps.log.warn("Agent tunnel is down \u2014 restarting",{pid,attempt:this.attempts});let url=null;try{url=await this.deps.restartAgentTunnel()}catch(err){this.deps.log.warn("Agent tunnel restart threw",{attempt:this.attempts,error:err instanceof Error?err.message:String(err)})}if(this.deps.isPaused())return this.attempts=0,this.nextAttemptAt=0,"paused";if(url)return this.attempts=0,this.nextAttemptAt=0,this.deps.log.info("Agent tunnel restored",{url}),"restarted";return this.armBackoff(),this.deps.log.warn("Agent tunnel restart did not yield a URL",{attempt:this.attempts,retryInMs:restartBackoffMs(this.attempts+1)}),"failed"}finally{this.ticking=!1}}armBackoff(){this.nextAttemptAt=this.now()+restartBackoffMs(this.attempts+1)}}function resolveCloudflaredCmd(){return resolveBinary("cloudflared")??(process.platform==="win32"?"cloudflared.exe":"cloudflared")}var PLUGIN_NAME="tunnel-cloudflare",PLUGIN_VERSION="2026.509.3",PROVIDER_NAME="tunnel-cloudflare",STORAGE_NS="tunnel-cloudflare",KEY_TUNNELS="tunnels",KEY_AGENT_URL="agent-tunnel-url",KEY_AGENT_PID="agent-tunnel-pid",TUNNEL_URL_RE=/(https:\/\/(?!api\.)[a-zA-Z0-9-]+\.trycloudflare\.com)/,URL_EXTRACT_TIMEOUT_MS=30000,KILL_GRACE_MS=3000;class CloudflaredRateLimitedError extends Error{constructor(detail){super(`cloudflared rate-limited by trycloudflare.com: ${detail}`);this.name="CloudflaredRateLimitedError"}}async function extractTunnelUrl(proc){let combined="",readStream=async(stream)=>{if(!stream)return;let reader=stream.getReader(),decoder=new TextDecoder;try{while(!0){let{done,value}=await reader.read();if(done)break;combined+=decoder.decode(value,{stream:!0});let match=TUNNEL_URL_RE.exec(combined);if(match)return match[1]}}finally{reader.releaseLock()}return},result=await Promise.race([readStream(proc.stdout),readStream(proc.stderr),new Promise((_,reject)=>setTimeout(()=>reject(Error(`Timed out after ${URL_EXTRACT_TIMEOUT_MS}ms waiting for tunnel URL`)),URL_EXTRACT_TIMEOUT_MS))]);if(!result){if(/429|too many requests|error code: 1015/i.test(combined))throw new CloudflaredRateLimitedError(combined.split(`
4
+ `).find((l)=>/429|1015/i.test(l))??combined);throw Error("cloudflared exited before producing a URL")}return result}function isAgentSupervisionPaused(state){if(state.shuttingDown||state.agentTunnelDisabled)return!0;return state.agentTunnelId!==null&&state.intentionalStops.has(state.agentTunnelId)}function withoutDegradedMarkers(metadata){if(!metadata)return metadata;let cleaned={...metadata};return delete cleaned.degraded,delete cleaned.degradedReason,cleaned}function isUsableAgentTunnel(info){if(!info.url)return!1;if(info.metadata?.degraded)return!1;return typeof info.pid==="number"&&info.pid>0}class CloudflareTunnelProvider{name=PROVIDER_NAME;processes=new Map;storage;log;hostServices;supervisor;agentTunnelPort=null;agentTunnelId=null;shuttingDown=!1;agentTunnelDisabled=!1;intentionalStops=new Set;startEpoch=new Map;constructor(hostServices){this.hostServices=hostServices,this.storage=hostServices.storage,this.log=new BoundLogger(hostServices.logger,PROVIDER_NAME),this.supervisor=new AgentTunnelSupervisor({getAgentTunnelPid:()=>this.readAgentTunnelPid(),isAlive:(pid)=>isProcessAlive(pid),clearAgentTunnel:()=>this.clearAgentTunnelKeys(),restartAgentTunnel:()=>this.restartAgentTunnel(),isPaused:()=>this.isSupervisionPaused(),log:{info:(message,meta)=>this.log.info(message,meta),warn:(message,meta)=>this.log.warn(message,meta)}})}isSupervisionPaused(){return isAgentSupervisionPaused({shuttingDown:this.shuttingDown,agentTunnelDisabled:this.agentTunnelDisabled,agentTunnelId:this.agentTunnelId,intentionalStops:this.intentionalStops})}async readAgentTunnelPid(){let raw=await this.storage.get(STORAGE_NS,KEY_AGENT_PID);if(!raw)return null;let pid=parseInt(raw,10);return Number.isFinite(pid)&&pid>0?pid:null}async clearAgentTunnelKeys(){await this.storage.delete(STORAGE_NS,KEY_AGENT_URL),await this.storage.delete(STORAGE_NS,KEY_AGENT_PID)}async persistAgentTunnel(info){if(info.url)await this.storage.set(STORAGE_NS,KEY_AGENT_URL,info.url);if(info.pid!==void 0)await this.storage.set(STORAGE_NS,KEY_AGENT_PID,String(info.pid))}async restartAgentTunnel(){let port=this.agentTunnelPort;if(port===null)return null;if(this.isSupervisionPaused())return null;let info=await this.buildReplacementTunnel(port);if(!info)return null;if(this.isSupervisionPaused())return this.log.warn(`Discarding replacement tunnel ${info.id} \u2014 teardown started while it was being built`),await this.stopInternal(info.id).catch(()=>{}),null;if(!isUsableAgentTunnel(info))return this.log.warn(`Replacement tunnel ${info.id} is not usable yet \u2014 backing off`,{url:info.url,degraded:info.metadata?.degraded??!1}),null;return await this.persistAgentTunnel(info),info.url}async buildReplacementTunnel(port){let existingId=this.agentTunnelId;if(existingId){if(this.processes.delete(existingId),(await this.loadTunnels()).some((t)=>t.id===existingId))return this.start(existingId)}let info=await this.startAgentTunnelInternal(port);return this.agentTunnelId=info.id,info}onAgentTunnelExited(){if(this.shuttingDown)return;this.supervisor.tick().catch(()=>{})}async loadTunnels(){let raw=await this.storage.get(STORAGE_NS,KEY_TUNNELS);if(!raw)return[];try{return JSON.parse(raw)}catch{return this.log.warn("Corrupt tunnel list in storage \u2014 resetting"),[]}}async saveTunnels(tunnels){await this.storage.set(STORAGE_NS,KEY_TUNNELS,JSON.stringify(tunnels))}async upsertTunnel(info){let tunnels=await this.loadTunnels(),idx=tunnels.findIndex((t)=>t.id===info.id);if(idx>=0)tunnels[idx]=info;else tunnels.push(info);await this.saveTunnels(tunnels)}async removeTunnelRecord(tunnelId){let tunnels=await this.loadTunnels();await this.saveTunnels(tunnels.filter((t)=>t.id!==tunnelId))}getCapabilities(){return{provider:PROVIDER_NAME,supportsHttp:!0,supportsHttps:!0,supportsTcp:!1,supportsUdp:!1,supportsCustomDomains:!1,supportsManagedSubdomains:!0,supportsSessionTokens:!1,supportsLiveLogs:!0,supportsUsageMetrics:!1,supportsRotateCredentials:!0,platforms:["darwin","linux","win32"]}}async issueSession(req){let tunnelId=(typeof req.metadata?.backendTunnelId==="string"?req.metadata.backendTunnelId:void 0)??crypto.randomUUID(),sessionId=crypto.randomUUID(),localHost=req.localHost??"127.0.0.1",protocol=req.protocol,now=new Date().toISOString(),info={id:tunnelId,providerName:PROVIDER_NAME,status:"stopped",protocol,localPort:req.localPort,localHost,url:"",sessionId,createdAt:now,metadata:{...req.metadata??{},localUrl:`${protocol}://${localHost}:${req.localPort}`}};await this.upsertTunnel(info);let session={sessionId,tunnelId,provider:PROVIDER_NAME,expiresAt:req.ttlSeconds?new Date(Date.now()+req.ttlSeconds*1000).toISOString():void 0,credentials:{localUrl:info.metadata.localUrl}};return await this.storage.set(STORAGE_NS,`sessions:${tunnelId}`,JSON.stringify([session])),session}async start(tunnelId){let info=(await this.loadTunnels()).find((t)=>t.id===tunnelId);if(!info)throw Error(`Tunnel ${tunnelId} not found (issueSession first)`);if(this.processes.has(tunnelId))return info;let localUrl=info.metadata?.localUrl??`${info.protocol}://${info.localHost}:${info.localPort}`;await this.upsertTunnel({...info,status:"starting",updatedAt:new Date().toISOString()}),this.log.info(`Starting tunnel ${tunnelId} \u2192 ${localUrl}`);let epoch=(this.startEpoch.get(tunnelId)??0)+1;this.startEpoch.set(tunnelId,epoch);let isCurrentAttempt=()=>this.startEpoch.get(tunnelId)===epoch,proc=Bun.spawn([resolveCloudflaredCmd(),"tunnel","--url",localUrl],{stdin:"ignore",stdout:"pipe",stderr:"pipe"});this.processes.set(tunnelId,proc);let isAgentTunnel=Boolean(info.metadata?.isAgentTunnel);proc.exited.then(async(code)=>{if(this.shuttingDown||this.intentionalStops.has(tunnelId))return;if(this.processes.get(tunnelId)!==proc||!isCurrentAttempt())return;this.log.warn(`Tunnel ${tunnelId} process exited unexpectedly (code=${code})`),this.processes.delete(tunnelId);try{let current=await this.loadTunnels(),idx=current.findIndex((t)=>t.id===tunnelId);if(idx>=0&&isCurrentAttempt())current[idx]={...current[idx],status:"error",updatedAt:new Date().toISOString(),metadata:{...current[idx].metadata,exitCode:code}},await this.saveTunnels(current)}catch(err){this.log.warn(`Failed to record exit of tunnel ${tunnelId}: ${err instanceof Error?err.message:String(err)}`)}if(isAgentTunnel||tunnelId===this.agentTunnelId)this.onAgentTunnelExited()});try{let url=await extractTunnelUrl(proc),activeInfo={...info,metadata:withoutDegradedMarkers(info.metadata),url,managedHostname:new URL(url).host,status:"active",pid:proc.pid,updatedAt:new Date().toISOString()};return await this.upsertTunnel(activeInfo),this.log.info(`Tunnel ${tunnelId} active at ${url} (PID ${proc.pid})`),activeInfo}catch(err){if(this.processes.get(tunnelId)===proc)this.processes.delete(tunnelId);if(isProcessAlive(proc.pid))await gracefulKill(proc.pid,KILL_GRACE_MS);if(!isCurrentAttempt())throw err;if(err instanceof CloudflaredRateLimitedError){let placeholderHost=`rate-limited-${tunnelId}.trycloudflare.com`,placeholderUrl=`https://${placeholderHost}`,degraded={...info,url:placeholderUrl,managedHostname:placeholderHost,status:"active",updatedAt:new Date().toISOString(),metadata:{...info.metadata,degraded:!0,degradedReason:err.message}};return await this.upsertTunnel(degraded),this.log.warn(`Tunnel ${tunnelId} rate-limited; returning placeholder URL`),degraded}let errorInfo={...info,status:"error",updatedAt:new Date().toISOString(),metadata:{...info.metadata,error:err instanceof Error?err.message:String(err)}};throw await this.upsertTunnel(errorInfo),err}}async rotate(tunnelId){if(!(await this.loadTunnels()).find((t)=>t.id===tunnelId))throw Error(`Tunnel ${tunnelId} not found`);this.intentionalStops.add(tunnelId);try{await this.stopInternal(tunnelId);let rotated=await this.start(tunnelId);if(tunnelId===this.agentTunnelId)await this.persistAgentTunnel(rotated)}finally{this.intentionalStops.delete(tunnelId)}let updated=(await this.loadTunnels()).find((t)=>t.id===tunnelId);return{sessionId:updated?.sessionId??crypto.randomUUID(),tunnelId,provider:PROVIDER_NAME,credentials:{localUrl:updated?.metadata?.localUrl}}}async attachCustomDomain(_tunnelId,_domain){throw Error("Custom domains are not supported by the cloudflared quick-tunnel provider. Use the vibetunnels provider or wait for named-tunnel support.")}async detachCustomDomain(_tunnelId,_domain){throw Error("Custom domains are not supported by the cloudflared quick-tunnel provider.")}async listSessions(tunnelId){let raw=await this.storage.get(STORAGE_NS,`sessions:${tunnelId}`);if(!raw)return[];try{return JSON.parse(raw)}catch{return[]}}async stop(tunnelId){this.log.info(`Stopping tunnel ${tunnelId}`),this.intentionalStops.add(tunnelId);try{if(tunnelId===this.agentTunnelId)this.agentTunnelDisabled=!0,this.supervisor.stop();await this.stopInternal(tunnelId)}finally{this.intentionalStops.delete(tunnelId)}}async stopInternal(tunnelId){let tunnel=(await this.loadTunnels()).find((t)=>t.id===tunnelId);if(tunnel)tunnel.status="stopping",tunnel.updatedAt=new Date().toISOString(),await this.upsertTunnel(tunnel);let proc=this.processes.get(tunnelId);if(proc)await gracefulKill(proc.pid,KILL_GRACE_MS),this.processes.delete(tunnelId);else if(tunnel?.pid&&isProcessAlive(tunnel.pid))await gracefulKill(tunnel.pid,KILL_GRACE_MS);if(tunnel)tunnel.status="stopped",tunnel.pid=void 0,tunnel.updatedAt=new Date().toISOString(),await this.upsertTunnel(tunnel);let agentPidRaw=await this.storage.get(STORAGE_NS,KEY_AGENT_PID);if(agentPidRaw&&tunnel?.pid&&String(tunnel.pid)===agentPidRaw)await this.storage.delete(STORAGE_NS,KEY_AGENT_URL),await this.storage.delete(STORAGE_NS,KEY_AGENT_PID);this.log.info(`Tunnel ${tunnelId} stopped`)}async getStatus(tunnelId){let tunnel=(await this.loadTunnels()).find((t)=>t.id===tunnelId)??null;if(!tunnel)return null;if(tunnel.status==="active"&&tunnel.pid){if(!isProcessAlive(tunnel.pid))tunnel.status="stopped",tunnel.updatedAt=new Date().toISOString(),await this.upsertTunnel(tunnel),this.processes.delete(tunnelId)}return tunnel}async getActiveTunnelUrl(){let envUrl=process.env.AGENT_TUNNEL_URL;if(envUrl)return envUrl;let url=await this.storage.get(STORAGE_NS,KEY_AGENT_URL);if(!url)return null;let pid=await this.readAgentTunnelPid();if(pid!==null&&!isProcessAlive(pid))return null;return url}async list(){return this.loadTunnels()}async delete(tunnelId){this.log.info(`Deleting tunnel ${tunnelId}`);let tunnel=(await this.loadTunnels()).find((t)=>t.id===tunnelId);if(tunnel&&(tunnel.status==="active"||tunnel.status==="starting"))await this.stop(tunnelId);if(await this.removeTunnelRecord(tunnelId),tunnelId===this.agentTunnelId)this.agentTunnelDisabled=!0,this.supervisor.stop(),this.agentTunnelId=null;this.log.info(`Tunnel ${tunnelId} deleted`)}detachAll(){this.log.info("Detaching from all tunnels (processes left running)"),this.shuttingDown=!0,this.supervisor.stop(),this.processes.clear()}async healthCheck(){try{let result=Bun.spawnSync([resolveCloudflaredCmd(),"--version"],{stdout:"pipe",stderr:"pipe",timeout:1e4});if(result.exitCode!==0)return{ok:!1,message:"cloudflared not available"};return{ok:!0,message:result.stdout.toString().trim()}}catch(err){return{ok:!1,message:err instanceof Error?`cloudflared not available: ${err.message}`:"cloudflared not available"}}}async startAgentTunnel(agentPort){this.agentTunnelPort=agentPort,this.shuttingDown=!1,this.agentTunnelDisabled=!1;try{let info=await this.startAgentTunnelInternal(agentPort);return this.agentTunnelId=info.id,info}finally{this.supervisor.start()}}async startAgentTunnelInternal(agentPort){let profileSuffix=(process.env.VIBECONTROLS_PROFILE??"default").replace(/[^A-Za-z0-9_]/g,"_"),envBootstrapPid=process.env[`AGENT_TUNNEL_PID_${profileSuffix}`],envBootstrapUrl=process.env[`AGENT_TUNNEL_URL_${profileSuffix}`];if(envBootstrapPid&&envBootstrapUrl){let pid=parseInt(envBootstrapPid,10);if(!isNaN(pid)&&isProcessAlive(pid)){this.log.info(`Adopting bootstrap cloudflared (pid=${pid}) at ${envBootstrapUrl}`),await this.storage.set(STORAGE_NS,KEY_AGENT_URL,envBootstrapUrl),await this.storage.set(STORAGE_NS,KEY_AGENT_PID,String(pid));let adopted={id:`agent-bootstrap-${pid}`,providerName:"cloudflare",status:"active",protocol:"http",localPort:agentPort,localHost:"127.0.0.1",url:envBootstrapUrl,pid,createdAt:new Date().toISOString(),metadata:{isAgentTunnel:!0,name:"agent",adopted:!0}};return delete process.env[`AGENT_TUNNEL_PID_${profileSuffix}`],delete process.env[`AGENT_TUNNEL_URL_${profileSuffix}`],adopted}}let storedPidStr=await this.storage.get(STORAGE_NS,KEY_AGENT_PID),storedUrl=await this.storage.get(STORAGE_NS,KEY_AGENT_URL);if(storedPidStr&&storedUrl){let storedPid=parseInt(storedPidStr,10);if(!isNaN(storedPid)&&isProcessAlive(storedPid)){this.log.info(`Reusing existing agent tunnel (pid=${storedPid}) at ${storedUrl}`);let existing=(await this.loadTunnels()).find((t)=>t.pid===storedPid&&t.metadata?.isAgentTunnel);if(existing)return existing;return{id:`agent-stored-${storedPid}`,providerName:"cloudflare",status:"active",protocol:"http",localPort:agentPort,localHost:"127.0.0.1",url:storedUrl,pid:storedPid,createdAt:new Date().toISOString(),metadata:{isAgentTunnel:!0,name:"agent",adopted:!0}}}}this.log.info(`Starting agent tunnel on port ${agentPort}`);let session=await this.issueSession({protocol:"http",localPort:agentPort,localHost:"127.0.0.1",metadata:{isAgentTunnel:!0,name:"agent"}});this.agentTunnelId=session.tunnelId;let info=await this.start(session.tunnelId);if(await this.storage.set(STORAGE_NS,KEY_AGENT_URL,info.url),info.pid!==void 0)await this.storage.set(STORAGE_NS,KEY_AGENT_PID,String(info.pid));return this.log.info(`Agent tunnel active at ${info.url}`),info}async stopAll(){this.log.info("Stopping all tunnels"),this.shuttingDown=!0,this.supervisor.stop();let tunnels=await this.loadTunnels();await Promise.allSettled(tunnels.filter((t)=>t.status==="active"||t.status==="starting").map((t)=>this.stop(t.id)));for(let[id,proc]of this.processes)if(isProcessAlive(proc.pid))this.log.warn(`Killing orphaned process ${proc.pid} (tunnel ${id})`),await gracefulKill(proc.pid,KILL_GRACE_MS);this.processes.clear(),await this.storage.deleteAll(STORAGE_NS),this.log.info("All tunnels stopped and storage cleared")}}var provider=null;function createPrereqsRoutes(){return new Elysia2({prefix:"/prereqs"}).get("/status",()=>{let cf=resolveBinary("cloudflared");return{satisfied:!!cf,missing:cf?[]:[{name:"cloudflared",kind:"binary",requiresSudo:!1,detected:void 0}]}}).post("/install",async()=>{if(resolveBinary("cloudflared"))return{ok:!0,installed:[],pendingSudo:[],errors:[]};try{return await installBinary({name:"cloudflared",downloads:CLOUDFLARED_DOWNLOADS,versionMatcher:"cloudflared version"}),{ok:!0,installed:["cloudflared"],pendingSudo:[],errors:[]}}catch(err){let manual=process.platform==="darwin"?"brew install cloudflared":process.platform==="win32"?"winget install Cloudflare.cloudflared # or download from https://github.com/cloudflare/cloudflared/releases":"see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/";return{ok:!1,installed:[],pendingSudo:[{name:"cloudflared",command:manual,reason:`cloudflared auto-download failed: ${err instanceof Error?err.message:String(err)}`}],errors:[err instanceof Error?err.message:String(err)]}}}).post("/uninstall",()=>({ok:!0}))}var createPlugin=(_ctx)=>{let telemetry=new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION),plugin={capabilities:{storage:"rw",subprocess:!0,telemetry:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"Cloudflare Tunnel provider for remote access",tags:["backend","provider"],apiPrefix:"/api/tunnel-cloudflare",tunnelDomainSuffixes:[".trycloudflare.com"],prerequisites:[{name:"cloudflared",kind:"binary",requiresSudo:!1,description:"Cloudflare tunnel daemon (auto-downloaded to the agent binary cache)"}],providers:{},createRoutes:()=>createPrereqsRoutes(),onServerStart:void 0,onServerStop:void 0},lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"tunnel.provider.ready",onInit:async(hostServices)=>{let log=new BoundLogger(hostServices.logger,PROVIDER_NAME);if(log.info("Plugin initialising"),provider=new CloudflareTunnelProvider(hostServices),plugin.providers={tunnel:provider},new ProviderRegistry(hostServices).registerProvider("tunnel",PROVIDER_NAME,provider),telemetry.emit("tunnel.provider.ready",{provider:"cloudflare"}),process.env.AGENT_TUNNEL==="false"){log.info("AGENT_TUNNEL=false \u2014 provider registered, tunnel managed externally");return}let health=await provider.healthCheck();if(!health.ok){log.warn(`cloudflared is not available \u2014 tunnel features will fail. ${health.message??""}`);return}log.info(health.message??"cloudflared available");try{let baseUrl=hostServices.getAgentBaseUrl?.()??"",parsed=new URL(baseUrl),agentPort=parseInt(parsed.port,10);if(!Number.isFinite(agentPort)||agentPort<=0){log.warn(`Cannot determine agent port from "${baseUrl}" \u2014 skipping agent tunnel`);return}await provider.startAgentTunnel(agentPort)}catch(err){log.error(`Failed to start agent tunnel: ${err instanceof Error?err.message:String(err)}`)}},onShutdown:async()=>{if(!provider)return;if(process.env.AGENT_TUNNEL==="false"||process.env.AGENT_TUNNEL_DETACH==="true"){provider.detachAll(),provider=null;return}await provider.stopAll(),provider=null},onNuke:async(_hostServices,ctx)=>{if(!provider)return{notes:["tunnel provider not initialised"]};if(ctx.dryRun)return{reaped:["cloudflared tunnels + tunnel-cloudflare storage"]};return await provider.stopAll(),provider=null,{reaped:["cloudflared tunnels + tunnel-cloudflare storage"]}}});return plugin.onServerStart=lifecycle.onServerStart,plugin.onServerStop=lifecycle.onServerStop,plugin.onNuke=lifecycle.onNuke,plugin};export{withoutDegradedMarkers,isUsableAgentTunnel,isAgentSupervisionPaused,createPlugin,CloudflareTunnelProvider};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecontrols/vibe-plugin-tunnel-cloudflare",
3
- "version": "2026.601.2",
3
+ "version": "2026.814.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "description": "Cloudflare Tunnel provider for VibeControls Agent",
@@ -19,11 +19,12 @@
19
19
  "lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
20
20
  "format": "bunx prettier . --write",
21
21
  "format:check": "bunx prettier . --check",
22
- "type:check": "tsc --noEmit",
22
+ "type:check": "bash scripts/build-limits.sh tsc --noEmit",
23
+ "test": "bun test",
23
24
  "clean": "rimraf dist",
24
25
  "prebuild": "bun run clean",
25
26
  "prepublishOnly": "bun run build",
26
- "sanity": "bun run format:check && bun run lint:sanity && bun run type:check && bun run build"
27
+ "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run format:check && bun run lint:sanity && bun run type:check && bun run test && bun run build'"
27
28
  },
28
29
  "dependencies": {
29
30
  "@vibecontrols/plugin-sdk": "2026.601.1",