Files
openchamber/packages/web/server/lib/tunnels/routes.js
T

611 lines
26 KiB
JavaScript

export const createTunnelRoutesRuntime = (dependencies) => {
const {
crypto,
URL,
tunnelService,
tunnelProviderRegistry,
tunnelAuthController,
readSettingsFromDiskMigrated,
readManagedRemoteTunnelConfigFromDisk,
normalizeTunnelProvider,
normalizeTunnelMode,
normalizeOptionalPath,
normalizeManagedRemoteTunnelHostname,
normalizeTunnelBootstrapTtlMs,
normalizeTunnelSessionTtlMs,
isSupportedTunnelMode,
upsertManagedRemoteTunnelToken,
resolveManagedRemoteTunnelToken,
TUNNEL_MODE_QUICK,
TUNNEL_MODE_MANAGED_LOCAL,
TUNNEL_MODE_MANAGED_REMOTE,
TUNNEL_PROVIDER_CLOUDFLARE,
TunnelServiceError,
getActivePort,
getRuntimeManagedRemoteTunnelHostname,
setRuntimeManagedRemoteTunnelHostname,
getRuntimeManagedRemoteTunnelToken,
setRuntimeManagedRemoteTunnelToken,
getActiveTunnelController,
setActiveTunnelController,
} = dependencies;
const resolveActiveNormalizedTunnelMode = () => {
const mode = tunnelService.resolveActiveMode();
if (mode === TUNNEL_MODE_MANAGED_LOCAL) {
return TUNNEL_MODE_MANAGED_LOCAL;
}
if (mode === TUNNEL_MODE_MANAGED_REMOTE) {
return TUNNEL_MODE_MANAGED_REMOTE;
}
return TUNNEL_MODE_QUICK;
};
const resolveNormalizedTunnelHost = (publicUrl) => {
if (typeof publicUrl !== 'string' || publicUrl.trim().length === 0) {
return null;
}
try {
return new URL(publicUrl).hostname.toLowerCase();
} catch {
return null;
}
};
const resolvePreferredTunnelProvider = async (reqBody = null) => {
if (typeof reqBody?.provider === 'string' && reqBody.provider.trim().length > 0) {
return normalizeTunnelProvider(reqBody.provider);
}
const activeProvider = tunnelService.resolveActiveProvider();
if (activeProvider) {
return normalizeTunnelProvider(activeProvider);
}
const settings = await readSettingsFromDiskMigrated();
return normalizeTunnelProvider(settings?.tunnelProvider);
};
const startTunnelWithNormalizedRequest = async ({
provider,
mode,
intent,
hostname,
token,
configPath,
selectedPresetId,
selectedPresetName,
}) => {
if (provider === TUNNEL_PROVIDER_CLOUDFLARE && mode === TUNNEL_MODE_MANAGED_REMOTE) {
setRuntimeManagedRemoteTunnelHostname(hostname);
setRuntimeManagedRemoteTunnelToken(token);
if (token && hostname) {
await upsertManagedRemoteTunnelToken({
id: selectedPresetId || hostname,
name: selectedPresetName || hostname,
hostname,
token,
});
}
}
const result = await tunnelService.start({
provider,
mode,
intent,
configPath,
token,
hostname,
});
console.log(`Tunnel active (${result.provider}): ${result.publicUrl}`);
return {
publicUrl: result.publicUrl,
mode: result.activeMode,
provider: result.provider,
providerMetadata: result.providerMetadata,
};
};
const createGenericModeChecks = ({ modeKey, requiredFields, doctorRequest, startupReady }) => {
const checks = [
{
id: 'startup_readiness',
label: 'Provider startup readiness',
status: startupReady ? 'pass' : 'fail',
detail: startupReady
? 'Provider dependency checks passed.'
: 'Resolve provider checks before starting tunnels.',
},
];
for (const field of requiredFields) {
const value = doctorRequest?.[field];
const present = typeof value === 'string' ? value.trim().length > 0 : Boolean(value);
checks.push({
id: `requirement_${field}`,
label: `Required: ${field}`,
status: present ? 'pass' : 'fail',
detail: present
? `${field} is configured.`
: `${field} is required for ${modeKey}.`,
});
}
const failures = checks.filter((entry) => entry.status === 'fail').length;
const warnings = checks.filter((entry) => entry.status === 'warn').length;
return {
mode: modeKey,
checks,
summary: {
ready: failures === 0,
failures,
warnings,
},
ready: failures === 0,
blockers: checks
.filter((entry) => entry.status === 'fail' && entry.id !== 'startup_readiness')
.map((entry) => entry.detail || entry.label || entry.id),
};
};
const runTunnelDoctor = async ({ providerId, modeFilter, doctorRequest }) => {
const provider = tunnelProviderRegistry.get(providerId);
if (!provider) {
throw new TunnelServiceError('provider_unsupported', `Unsupported tunnel provider: ${providerId}`);
}
const capabilities = provider.capabilities || {};
const modeKeys = Array.isArray(capabilities.modes)
? capabilities.modes.map((entry) => entry?.key).filter((key) => typeof key === 'string' && key.length > 0)
: [];
if (modeFilter && !modeKeys.includes(modeFilter)) {
throw new TunnelServiceError('mode_unsupported', `Provider '${providerId}' does not support mode '${modeFilter}'`);
}
if (typeof provider.diagnose === 'function') {
const diagnosed = await provider.diagnose({
...doctorRequest,
mode: modeFilter || doctorRequest?.mode,
}, {
capabilities,
});
const providerChecks = Array.isArray(diagnosed?.providerChecks) ? diagnosed.providerChecks : [];
const allModes = Array.isArray(diagnosed?.modes) ? diagnosed.modes : [];
const modes = modeFilter ? allModes.filter((entry) => entry?.mode === modeFilter) : allModes;
return {
ok: true,
provider: providerId,
providerChecks,
modes,
};
}
const availability = await tunnelService.checkAvailability(providerId);
const dependencyAvailable = Boolean(availability?.available);
const providerChecks = [{
id: 'dependency',
label: 'Provider dependency',
status: dependencyAvailable ? 'pass' : 'fail',
detail: dependencyAvailable
? (availability?.version || 'available')
: (availability?.message || 'Required provider dependency is unavailable.'),
}];
const targetModes = (Array.isArray(capabilities.modes) ? capabilities.modes : [])
.filter((entry) => !modeFilter || entry?.key === modeFilter);
const modes = targetModes.map((entry) => createGenericModeChecks({
modeKey: entry.key,
requiredFields: Array.isArray(entry?.requires) ? entry.requires : [],
doctorRequest,
startupReady: dependencyAvailable,
}));
return {
ok: true,
provider: providerId,
providerChecks,
modes,
};
};
const registerRoutes = (app) => {
app.get('/api/openchamber/tunnel/check', async (req, res) => {
try {
const requestedProvider = typeof req?.query?.provider === 'string' && req.query.provider.trim().length > 0
? normalizeTunnelProvider(req.query.provider)
: await resolvePreferredTunnelProvider();
const result = await tunnelService.checkAvailability(requestedProvider);
res.json({
available: result.available,
provider: requestedProvider,
version: result.version || null,
dependency: result.dependency || null,
installCommand: result.installCommand || null,
installUrl: result.installUrl || null,
platform: result.platform || process.platform,
message: result.message || null,
});
} catch (error) {
console.warn('Tunnel dependency check failed:', error);
res.json({ available: false, provider: null, version: null, dependency: null, installCommand: null, installUrl: null, platform: process.platform, message: null });
}
});
const handleTunnelDoctor = async (req, res) => {
try {
const params = req.query || {};
const body = req.body || {};
const providerId = typeof params.provider === 'string' && params.provider.trim().length > 0
? normalizeTunnelProvider(params.provider)
: await resolvePreferredTunnelProvider();
const modeFilter = typeof params.mode === 'string' && params.mode.trim().length > 0
? params.mode.trim().toLowerCase()
: null;
const settings = await readSettingsFromDiskMigrated();
const selectedPresetId = typeof params.managedRemoteTunnelPresetId === 'string'
? params.managedRemoteTunnelPresetId.trim()
: '';
const requestConfigPath = normalizeOptionalPath(params.configPath)
?? normalizeOptionalPath(settings?.managedLocalTunnelConfigPath);
const requestManagedRemoteHostname = normalizeManagedRemoteTunnelHostname(params.managedRemoteTunnelHostname);
const requestTunnelHostname = normalizeManagedRemoteTunnelHostname(params.tunnelHostname);
const requestHostname = normalizeManagedRemoteTunnelHostname(params.hostname);
const hostnameFromSettings = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
const hostname = requestHostname || requestTunnelHostname || requestManagedRemoteHostname || hostnameFromSettings;
const requestManagedRemoteToken = typeof body.managedRemoteTunnelToken === 'string'
? body.managedRemoteTunnelToken.trim()
: '';
const requestTunnelToken = typeof body.tunnelToken === 'string'
? body.tunnelToken.trim()
: '';
const requestToken = typeof body.token === 'string'
? body.token.trim()
: '';
const requestTokenProvided = body.managedRemoteTunnelTokenProvided === true
|| body.tunnelTokenProvided === true
|| body.tokenProvided === true;
const requestHostnameProvided = body.managedRemoteTunnelHostnameProvided === true
|| body.tunnelHostnameProvided === true
|| body.hostnameProvided === true;
const storedManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string'
? settings.managedRemoteTunnelToken.trim()
: '';
const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
const serverHasSavedManagedRemoteProfile = managedRemoteTunnelConfig.tunnels.some((entry) => {
const savedHostname = normalizeManagedRemoteTunnelHostname(entry?.hostname);
const savedToken = typeof entry?.token === 'string' ? entry.token.trim() : '';
return Boolean(savedHostname && savedToken);
});
const cliHasSavedManagedRemoteProfile = params.hasSavedManagedRemoteProfile === '1';
const hasSavedManagedRemoteProfile = serverHasSavedManagedRemoteProfile || cliHasSavedManagedRemoteProfile;
const configManagedRemoteToken = providerId === TUNNEL_PROVIDER_CLOUDFLARE
? await resolveManagedRemoteTunnelToken({ presetId: selectedPresetId, hostname })
: '';
const runtimeHostname = getRuntimeManagedRemoteTunnelHostname();
const runtimeToken = getRuntimeManagedRemoteTunnelToken();
const token = requestToken
|| requestTunnelToken
|| requestManagedRemoteToken
|| ((runtimeHostname && hostname && runtimeHostname === hostname) ? runtimeToken : '')
|| configManagedRemoteToken
|| storedManagedRemoteToken;
const doctorRequest = {
mode: modeFilter,
hostname,
token,
tokenProvided: requestTokenProvided,
hostnameProvided: requestHostnameProvided,
configPath: requestConfigPath,
hasSavedManagedRemoteProfile,
};
const result = await runTunnelDoctor({
providerId,
modeFilter,
doctorRequest,
});
return res.json(result);
} catch (error) {
if (error instanceof TunnelServiceError) {
return res.status(400).json({ ok: false, error: error.message, code: error.code });
}
console.warn('Tunnel doctor failed:', error);
return res.status(500).json({ ok: false, error: 'Failed to run tunnel doctor' });
}
};
app.post('/api/openchamber/tunnel/doctor', handleTunnelDoctor);
app.get('/api/openchamber/tunnel/doctor', handleTunnelDoctor);
app.get('/api/openchamber/tunnel/providers', (_req, res) => {
const providers = tunnelProviderRegistry.listCapabilities();
return res.json({ providers });
});
app.get('/api/openchamber/tunnel/status', async (_req, res) => {
try {
const settings = await readSettingsFromDiskMigrated();
const normalizedMode = normalizeTunnelMode(settings?.tunnelMode);
const managedRemoteHostname = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
const managedRemoteTunnelPresetSummaries = managedRemoteTunnelConfig.tunnels.map((entry) => ({
id: entry.id,
name: entry.name,
hostname: entry.hostname,
}));
const hasStoredManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
const hasManagedRemoteTunnelToken = getRuntimeManagedRemoteTunnelToken().length > 0 || managedRemoteTunnelConfig.tunnels.length > 0 || hasStoredManagedRemoteToken;
const bootstrapTtlMs = settings?.tunnelBootstrapTtlMs === null
? null
: normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs);
const sessionTtlMs = normalizeTunnelSessionTtlMs(settings?.tunnelSessionTtlMs);
const activeSessions = tunnelAuthController.listTunnelSessions();
const activeProvider = tunnelService.resolveActiveProvider();
const provider = activeProvider || normalizeTunnelProvider(settings?.tunnelProvider);
const publicUrl = tunnelService.getPublicUrl();
if (!publicUrl) {
return res.json({
active: false,
url: null,
mode: normalizedMode,
provider,
providerMetadata: null,
hasManagedRemoteTunnelToken,
managedRemoteTunnelHostname: managedRemoteHostname || null,
managedRemoteTunnelPresets: managedRemoteTunnelPresetSummaries,
managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id),
hasBootstrapToken: false,
bootstrapExpiresAt: null,
policy: 'tunnel-gated',
activeTunnelMode: tunnelAuthController.getActiveTunnelMode() || null,
activeSessions,
localPort: getActivePort(),
ttlConfig: {
bootstrapTtlMs,
sessionTtlMs,
},
});
}
const activeNormalizedMode = resolveActiveNormalizedTunnelMode();
const activeTunnelId = tunnelAuthController.getActiveTunnelId();
const activeTunnelHost = tunnelAuthController.getActiveTunnelHost();
const resolvedTunnelHost = resolveNormalizedTunnelHost(publicUrl);
const activeTunnelMode = tunnelAuthController.getActiveTunnelMode();
const needsActiveTunnelSync = !activeTunnelId
|| !activeTunnelHost
|| !resolvedTunnelHost
|| activeTunnelHost !== resolvedTunnelHost
|| activeTunnelMode !== activeNormalizedMode;
if (needsActiveTunnelSync) {
tunnelAuthController.setActiveTunnel({
tunnelId: activeTunnelId || crypto.randomUUID(),
publicUrl,
mode: activeNormalizedMode,
});
}
const bootstrapStatus = tunnelAuthController.getBootstrapStatus();
const providerMetadata = tunnelService.getProviderMetadata();
return res.json({
active: true,
url: publicUrl,
mode: activeNormalizedMode,
provider,
providerMetadata,
hasManagedRemoteTunnelToken,
managedRemoteTunnelHostname: managedRemoteHostname || null,
managedRemoteTunnelPresets: managedRemoteTunnelPresetSummaries,
managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id),
hasBootstrapToken: bootstrapStatus.hasBootstrapToken,
bootstrapExpiresAt: bootstrapStatus.bootstrapExpiresAt,
policy: 'tunnel-gated',
activeTunnelMode: activeNormalizedMode,
activeSessions: tunnelAuthController.listTunnelSessions(),
localPort: getActivePort(),
ttlConfig: {
bootstrapTtlMs,
sessionTtlMs,
},
});
} catch (error) {
return res.status(500).json({ error: 'Failed to get tunnel status' });
}
});
app.put('/api/openchamber/tunnel/managed-remote-token', async (req, res) => {
try {
const presetId = typeof req?.body?.presetId === 'string' ? req.body.presetId.trim() : '';
const presetName = typeof req?.body?.presetName === 'string' ? req.body.presetName.trim() : '';
const managedRemoteTunnelHostname = normalizeManagedRemoteTunnelHostname(req?.body?.managedRemoteTunnelHostname);
const managedRemoteTunnelToken = typeof req?.body?.managedRemoteTunnelToken === 'string' ? req.body.managedRemoteTunnelToken.trim() : '';
if (!presetId || !presetName || !managedRemoteTunnelHostname || !managedRemoteTunnelToken) {
return res.status(400).json({ ok: false, error: 'presetId, presetName, managedRemoteTunnelHostname and managedRemoteTunnelToken are required' });
}
await upsertManagedRemoteTunnelToken({
id: presetId,
name: presetName,
hostname: managedRemoteTunnelHostname,
token: managedRemoteTunnelToken,
});
const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
return res.json({ ok: true, managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id) });
} catch (error) {
return res.status(500).json({ ok: false, error: 'Failed to save managed remote tunnel token' });
}
});
app.post('/api/openchamber/tunnel/start', async (_req, res) => {
try {
const settings = await readSettingsFromDiskMigrated();
if (typeof _req?.body?.provider === 'string' && _req.body.provider.trim().length > 0) {
const rawProvider = _req.body.provider.trim().toLowerCase();
if (!tunnelProviderRegistry.get(rawProvider)) {
return res.status(422).json({ ok: false, error: `Unsupported tunnel provider: ${rawProvider}`, code: 'provider_unsupported' });
}
}
const provider = normalizeTunnelProvider(_req?.body?.provider ?? settings?.tunnelProvider);
const modeInput = _req?.body?.mode ?? settings?.tunnelMode;
const intent = typeof _req?.body?.intent === 'string' ? _req.body.intent.trim().toLowerCase() : undefined;
const mode = typeof modeInput === 'string'
? modeInput.trim().toLowerCase()
: normalizeTunnelMode(modeInput);
if (typeof _req?.body?.mode === 'string' && _req.body.mode.trim().length > 0 && !isSupportedTunnelMode(mode)) {
return res.status(422).json({ ok: false, error: `Unsupported tunnel mode: ${mode}`, code: 'mode_unsupported' });
}
const selectedPresetId = typeof _req?.body?.managedRemoteTunnelPresetId === 'string' ? _req.body.managedRemoteTunnelPresetId.trim() : '';
const selectedPresetName = typeof _req?.body?.managedRemoteTunnelPresetName === 'string' ? _req.body.managedRemoteTunnelPresetName.trim() : '';
const requestConfigPath = normalizeOptionalPath(_req?.body?.configPath)
?? normalizeOptionalPath(settings?.managedLocalTunnelConfigPath);
const requestManagedRemoteHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.managedRemoteTunnelHostname);
const requestTunnelHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.tunnelHostname);
const requestHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.hostname);
const hostnameFromSettings = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
const hostname = requestHostname || requestTunnelHostname || requestManagedRemoteHostname || hostnameFromSettings;
const requestManagedRemoteToken = typeof _req?.body?.managedRemoteTunnelToken === 'string' ? _req.body.managedRemoteTunnelToken.trim() : '';
const requestTunnelToken = typeof _req?.body?.tunnelToken === 'string' ? _req.body.tunnelToken.trim() : '';
const requestToken = typeof _req?.body?.token === 'string' ? _req.body.token.trim() : '';
const storedManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string' ? settings.managedRemoteTunnelToken.trim() : '';
const configManagedRemoteToken = provider === TUNNEL_PROVIDER_CLOUDFLARE
? await resolveManagedRemoteTunnelToken({ presetId: selectedPresetId, hostname })
: '';
const runtimeHostname = getRuntimeManagedRemoteTunnelHostname();
const runtimeToken = getRuntimeManagedRemoteTunnelToken();
const token = requestToken
|| requestTunnelToken
|| requestManagedRemoteToken
|| ((runtimeHostname && hostname && runtimeHostname === hostname) ? runtimeToken : '')
|| configManagedRemoteToken
|| storedManagedRemoteToken;
const requestConnectTtlMs = typeof _req?.body?.connectTtlMs === 'number' && Number.isFinite(_req.body.connectTtlMs)
? normalizeTunnelBootstrapTtlMs(_req.body.connectTtlMs)
: undefined;
const requestSessionTtlMs = typeof _req?.body?.sessionTtlMs === 'number' && Number.isFinite(_req.body.sessionTtlMs)
? normalizeTunnelSessionTtlMs(_req.body.sessionTtlMs)
: undefined;
const bootstrapTtlMs = requestConnectTtlMs ?? (settings?.tunnelBootstrapTtlMs === null
? null
: normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs));
const sessionTtlMs = requestSessionTtlMs ?? normalizeTunnelSessionTtlMs(settings?.tunnelSessionTtlMs);
const previousTunnelId = tunnelAuthController.getActiveTunnelId();
const previousMode = tunnelAuthController.getActiveTunnelMode();
const previousProvider = tunnelService.resolveActiveProvider();
const previousUrl = tunnelService.getPublicUrl();
const { publicUrl, provider: activeProvider, providerMetadata } = await startTunnelWithNormalizedRequest({
provider,
mode,
intent,
hostname,
token,
configPath: requestConfigPath,
selectedPresetId,
selectedPresetName,
});
const replacedTunnel = Boolean(previousTunnelId) && (
previousMode !== mode
|| previousProvider !== activeProvider
|| previousUrl !== publicUrl
);
let revokedBootstrapCount = 0;
let invalidatedSessionCount = 0;
if (replacedTunnel && previousTunnelId) {
const revoked = tunnelAuthController.revokeTunnelArtifacts(previousTunnelId);
revokedBootstrapCount = revoked.revokedBootstrapCount;
invalidatedSessionCount = revoked.invalidatedSessionCount;
}
tunnelAuthController.setActiveTunnel({
tunnelId: replacedTunnel || !previousTunnelId ? crypto.randomUUID() : previousTunnelId,
publicUrl,
mode,
});
const bootstrapToken = tunnelAuthController.issueBootstrapToken({ ttlMs: bootstrapTtlMs });
const connectUrl = `${publicUrl.replace(/\/$/, '')}/connect?t=${encodeURIComponent(bootstrapToken.token)}`;
const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
const isCloudflareProvider = activeProvider === TUNNEL_PROVIDER_CLOUDFLARE;
return res.json({
ok: true,
url: publicUrl,
mode,
provider: activeProvider,
providerMetadata,
managedRemoteTunnelHostname: isCloudflareProvider ? (hostname || null) : null,
managedRemoteTunnelTokenPresetIds: isCloudflareProvider ? managedRemoteTunnelConfig.tunnels.map((entry) => entry.id) : [],
connectUrl,
bootstrapExpiresAt: bootstrapToken.expiresAt,
replacedTunnel,
replaced: replacedTunnel
? {
mode: previousMode,
provider: previousProvider,
url: previousUrl,
}
: null,
revokedBootstrapCount,
invalidatedSessionCount,
policy: 'tunnel-gated',
activeTunnelMode: mode,
activeSessions: tunnelAuthController.listTunnelSessions(),
localPort: getActivePort(),
ttlConfig: {
bootstrapTtlMs,
sessionTtlMs,
},
});
} catch (error) {
console.error('Failed to start tunnel:', error);
setActiveTunnelController(null);
tunnelAuthController.clearActiveTunnel();
if (error instanceof TunnelServiceError) {
const status = error.code === 'missing_dependency'
? 400
: (error.code === 'validation_error' || error.code === 'provider_unsupported' || error.code === 'mode_unsupported'
? 422
: 500);
return res.status(status).json({ ok: false, error: error.message, code: error.code });
}
return res.status(500).json({ ok: false, error: 'Failed to start tunnel', code: 'startup_failed' });
}
});
app.post('/api/openchamber/tunnel/stop', (_req, res) => {
let revokedBootstrapCount = 0;
let invalidatedSessionCount = 0;
const activeTunnelId = tunnelAuthController.getActiveTunnelId();
if (activeTunnelId) {
const revoked = tunnelAuthController.revokeTunnelArtifacts(activeTunnelId);
revokedBootstrapCount = revoked.revokedBootstrapCount;
invalidatedSessionCount = revoked.invalidatedSessionCount;
}
if (getActiveTunnelController()) {
console.log('Stopping active tunnel (user requested)...');
tunnelService.stop();
}
tunnelAuthController.clearActiveTunnel();
res.json({ ok: true, revokedBootstrapCount, invalidatedSessionCount });
});
};
return {
registerRoutes,
startTunnelWithNormalizedRequest,
};
};