import { checkCloudflareApiReachability, checkCloudflaredAvailable, inspectManagedLocalCloudflareConfig, normalizeCloudflareTunnelHostname, startCloudflareManagedLocalTunnel, startCloudflareManagedRemoteTunnel, startCloudflareQuickTunnel, } from '../../cloudflare-tunnel.js'; import { TUNNEL_INTENT_EPHEMERAL_PUBLIC, TUNNEL_INTENT_PERSISTENT_PUBLIC, TUNNEL_MODE_MANAGED_LOCAL, TUNNEL_MODE_MANAGED_REMOTE, TUNNEL_MODE_QUICK, TUNNEL_PROVIDER_CLOUDFLARE, TunnelServiceError, } from '../types.js'; import { getTunnelDependencyInstallInfo } from '../install-help.js'; export const cloudflareTunnelProviderCapabilities = { provider: TUNNEL_PROVIDER_CLOUDFLARE, defaults: { mode: TUNNEL_MODE_QUICK, optionDefaults: {}, }, modes: [ { key: TUNNEL_MODE_QUICK, label: 'Quick Tunnel', intent: TUNNEL_INTENT_EPHEMERAL_PUBLIC, requires: [], supports: ['sessionTTL'], stability: 'ga', }, { key: TUNNEL_MODE_MANAGED_REMOTE, label: 'Managed Remote Tunnel', intent: TUNNEL_INTENT_PERSISTENT_PUBLIC, requires: ['token', 'hostname'], supports: ['customDomain', 'sessionTTL'], stability: 'ga', }, { key: TUNNEL_MODE_MANAGED_LOCAL, label: 'Managed Local Tunnel', intent: TUNNEL_INTENT_PERSISTENT_PUBLIC, requires: [], supports: ['configFile', 'customDomain', 'sessionTTL'], stability: 'ga', }, ], }; export function createCloudflareTunnelProvider() { const validateTokenShape = (value) => { if (typeof value !== 'string') { return { ok: false, detail: 'Managed remote token is missing.' }; } const trimmed = value.trim(); if (!trimmed) { return { ok: false, detail: 'Managed remote token is missing.' }; } if (/\s/.test(trimmed)) { return { ok: false, detail: 'Managed remote token has whitespace; provide the raw token value.' }; } return { ok: true, detail: 'Managed remote token looks valid.' }; }; const createModeSummary = (checks) => { const failures = checks.filter((entry) => entry.status === 'fail').length; const warnings = checks.filter((entry) => entry.status === 'warn').length; return { ready: failures === 0, failures, warnings, }; }; const describeMode = ({ mode, checks }) => { const summary = createModeSummary(checks); const blockers = checks .filter((entry) => entry.status === 'fail' && entry.id !== 'startup_readiness') .map((entry) => entry.detail || entry.label || entry.id); return { mode, checks, summary, ready: summary.ready, blockers, }; }; return { id: TUNNEL_PROVIDER_CLOUDFLARE, capabilities: cloudflareTunnelProviderCapabilities, checkAvailability: async () => { const result = await checkCloudflaredAvailable(); if (result.available) { return { ...result, ...getTunnelDependencyInstallInfo(TUNNEL_PROVIDER_CLOUDFLARE), }; } const installInfo = getTunnelDependencyInstallInfo(TUNNEL_PROVIDER_CLOUDFLARE); return { ...result, ...installInfo, }; }, diagnose: async (request = {}) => { const dependency = await checkCloudflaredAvailable(); const network = await checkCloudflareApiReachability(); const installInfo = getTunnelDependencyInstallInfo(TUNNEL_PROVIDER_CLOUDFLARE); const providerChecks = [ { id: 'dependency', label: 'cloudflared installed', status: dependency.available ? 'pass' : 'fail', detail: dependency.available ? (dependency.version || dependency.path || 'cloudflared available') : installInfo.message, }, { id: 'network', label: 'Cloudflare API reachable', status: network.reachable ? 'pass' : 'fail', detail: network.reachable ? (network.status ? `HTTP ${network.status}` : 'Reachable') : (network.error || 'Could not reach api.trycloudflare.com'), }, ]; const startupReady = dependency.available && network.reachable; const startupDetail = startupReady ? 'Provider dependency and network checks passed.' : 'Resolve provider checks before starting tunnels.'; const quickChecks = [ { id: 'startup_readiness', label: 'Provider startup readiness', status: startupReady ? 'pass' : 'fail', detail: startupDetail, }, { id: 'quick_mode_prerequisites', label: 'Quick tunnel prerequisites', status: network.reachable ? 'pass' : 'fail', detail: network.reachable ? 'Cloudflare edge is reachable for quick tunnels.' : 'Cloudflare edge is not reachable for quick tunnels.', }, ]; const managedLocalInspection = inspectManagedLocalCloudflareConfig({ configPath: request.configPath, hostname: request.hostname, }); const managedLocalChecks = [ { id: 'startup_readiness', label: 'Provider startup readiness', status: startupReady ? 'pass' : 'fail', detail: startupDetail, }, { id: 'managed_local_config', label: 'Managed local config', status: managedLocalInspection.ok ? 'pass' : 'fail', detail: managedLocalInspection.ok ? `${managedLocalInspection.effectiveConfigPath}${managedLocalInspection.resolvedHostname ? ` (${managedLocalInspection.resolvedHostname})` : ''}` : managedLocalInspection.error, }, ]; const normalizedHost = normalizeCloudflareTunnelHostname(request.hostname); const hostnameMissing = !normalizedHost; const remoteTokenValidation = validateTokenShape(request.token); const tokenMissing = typeof request.token !== 'string' || request.token.trim().length === 0; const hasSavedManagedRemoteProfile = request.hasSavedManagedRemoteProfile === true; const tokenProvided = request.tokenProvided === true; const hostnameProvided = request.hostnameProvided === true; const hasExplicitManagedRemoteInput = tokenProvided || hostnameProvided; const canUseSavedProfileForHostname = !hasExplicitManagedRemoteInput && hostnameMissing && hasSavedManagedRemoteProfile; const canUseSavedProfileForToken = !hasExplicitManagedRemoteInput && tokenMissing && hasSavedManagedRemoteProfile; const savedProfileReadyDetail = 'at least one saved profile present'; const managedRemoteChecks = [ { id: 'startup_readiness', label: 'Provider startup readiness', status: startupReady ? 'pass' : 'fail', detail: startupDetail, }, { id: 'managed_remote_hostname', label: 'Managed remote hostname', status: normalizedHost || canUseSavedProfileForHostname ? 'pass' : 'fail', detail: normalizedHost ? normalizedHost : canUseSavedProfileForHostname ? savedProfileReadyDetail : 'Managed remote hostname is required (use --hostname).', }, { id: 'managed_remote_token', label: 'Managed remote token', status: remoteTokenValidation.ok || canUseSavedProfileForToken ? 'pass' : 'fail', detail: canUseSavedProfileForToken ? savedProfileReadyDetail : remoteTokenValidation.detail, }, ]; const allModes = [ describeMode({ mode: TUNNEL_MODE_QUICK, checks: quickChecks }), describeMode({ mode: TUNNEL_MODE_MANAGED_REMOTE, checks: managedRemoteChecks }), describeMode({ mode: TUNNEL_MODE_MANAGED_LOCAL, checks: managedLocalChecks }), ]; const modeFilter = typeof request.mode === 'string' && request.mode.trim().length > 0 ? request.mode.trim().toLowerCase() : null; const modes = modeFilter ? allModes.filter((entry) => entry.mode === modeFilter) : allModes; return { providerChecks, modes, }; }, start: async (request, context = {}) => { if (request.mode === TUNNEL_MODE_MANAGED_REMOTE) { return startCloudflareManagedRemoteTunnel({ token: request.token, hostname: request.hostname, }); } if (request.mode === TUNNEL_MODE_MANAGED_LOCAL) { return startCloudflareManagedLocalTunnel({ configPath: request.configPath, hostname: request.hostname, }); } if (!context.originUrl) { throw new TunnelServiceError('validation_error', 'originUrl is required for quick tunnel mode'); } return startCloudflareQuickTunnel({ originUrl: context.originUrl, port: context.activePort, }); }, stop: (controller) => { controller?.stop?.(); }, resolvePublicUrl: (controller) => controller?.getPublicUrl?.() ?? null, getMetadata: (controller) => ({ configPath: controller?.getEffectiveConfigPath?.() ?? null, resolvedHostname: controller?.getResolvedHostname?.() ?? null, }), }; }