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

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.
package/dist/index.d.ts CHANGED
@@ -170,7 +170,22 @@ export declare class CloudflareTunnelProvider implements TunnelProvider {
170
170
  * spawning a new one. This keeps the tunnel URL stable across hot-reloads.
171
171
  */
172
172
  startAgentTunnel(agentPort: number): Promise<TunnelInfo>;
173
- private startAgentTunnelInternal;
173
+ /** `protected` so tests can stand in for the spawn without running cloudflared. */
174
+ protected startAgentTunnelInternal(agentPort: number): Promise<TunnelInfo>;
175
+ /**
176
+ * Stop supervising WITHOUT touching any running process.
177
+ *
178
+ * `cloudflared` processes are global OS resources, but a daemon boot
179
+ * initialises this plugin twice: once when it loads, and again after the
180
+ * plugin manager refresh-installs `@latest` and re-inits it. Each init built
181
+ * a provider with its own supervisor, so both polled the same agent tunnel,
182
+ * both saw it die, and both rebuilt it — two `cloudflared` processes, one of
183
+ * them instantly orphaned (observed live on 2026-08-14). The outgoing
184
+ * instance stands down here so exactly one supervisor owns the fleet.
185
+ */
186
+ standDownSupervision(): void;
187
+ /** Whether this instance is the one currently watching the agent tunnel. */
188
+ get isSupervising(): boolean;
174
189
  /**
175
190
  * Tear down every tunnel this provider is tracking and clear storage.
176
191
  */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // @bun
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 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};
2
+ import{createRequire}from"module";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 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";function resolvePluginVersion(){try{let pkg=createRequire(import.meta.url)("../package.json");if(typeof pkg.version==="string"&&pkg.version)return pkg.version}catch{}return"0.0.0-unknown"}var PLUGIN_VERSION=resolvePluginVersion(),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{if(!this.shuttingDown)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}standDownSupervision(){this.shuttingDown=!0,this.supervisor.stop()}get isSupervising(){return this.supervisor.isRunning}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?.standDownSupervision(),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.814.1",
3
+ "version": "2026.814.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "description": "Cloudflare Tunnel provider for VibeControls Agent",