267 lines
9.0 KiB
JavaScript
267 lines
9.0 KiB
JavaScript
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,
|
|
}),
|
|
};
|
|
}
|