fix(FN-2525): harden remote access auth and tunnel regression coverage

- Add regression tests across CLI, core, dashboard, and engine for remote access auth, settings parity, and serve/TUI callback wiring
- Expand dashboard route and modal coverage for remote settings/auth flows including node environment behaviors
- Redact provider-switch failure details in tunnel process manager to avoid leaking sensitive provider diagnostics
- Update route registration and engine lifecycle tests to lock in remote-access behavior under real execution paths
This commit is contained in:
Fusion
2026-04-26 06:35:46 -07:00
committed by gsxdsm
parent 559e908865
commit f09346a9da
13 changed files with 815 additions and 19 deletions

View File

@@ -263,30 +263,113 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
// ── Remote Access Routes ────────────────────────────────────────────
function toRemoteSettingsPayload(remoteAccess: NonNullable<Awaited<ReturnType<typeof store.getSettings>>["remoteAccess"]>) {
const persistentToken = remoteAccess.tokenStrategy.persistent.token?.trim() ?? "";
return {
remoteEnabled: Boolean(remoteAccess.enabled),
remoteActiveProvider: remoteAccess.activeProvider ?? null,
remoteTailscaleEnabled: Boolean(remoteAccess.providers.tailscale.enabled),
remoteTailscaleHostname: remoteAccess.providers.tailscale.hostname,
remoteTailscaleTargetPort: Number(remoteAccess.providers.tailscale.targetPort ?? 4040),
remoteTailscaleAcceptRoutes: Boolean(remoteAccess.providers.tailscale.acceptRoutes),
remoteCloudflareEnabled: Boolean(remoteAccess.providers.cloudflare.enabled),
remoteCloudflareTunnelName: remoteAccess.providers.cloudflare.tunnelName,
remoteCloudflareTunnelToken: remoteAccess.providers.cloudflare.tunnelToken,
remoteCloudflareIngressUrl: remoteAccess.providers.cloudflare.ingressUrl,
remoteShortLivedEnabled: Boolean(remoteAccess.tokenStrategy.shortLived.enabled),
remoteShortLivedTtlMs: Number(remoteAccess.tokenStrategy.shortLived.ttlMs ?? 900_000),
remoteShortLivedMaxTtlMs: Number(remoteAccess.tokenStrategy.shortLived.maxTtlMs ?? 86_400_000),
remotePersistentToken: persistentToken ? maskRemoteToken(persistentToken) : null,
remoteRememberLastRunning: Boolean(remoteAccess.lifecycle.rememberLastRunning),
remoteWasRunningOnShutdown: Boolean(remoteAccess.lifecycle.wasRunningOnShutdown),
remoteLastStartedProvider: remoteAccess.lifecycle.lastRunningProvider ?? null,
};
}
router.get("/remote/settings", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
const settings = await scopedStore.getSettings();
const remoteAccess = settings.remoteAccess;
const persistentToken = remoteAccess?.tokenStrategy.persistent.token?.trim() ?? "";
res.json({
settings: {
remoteEnabled: Boolean(remoteAccess?.enabled),
remoteActiveProvider: remoteAccess?.activeProvider ?? null,
remoteTailscaleEnabled: Boolean(remoteAccess?.providers.tailscale.enabled),
remoteCloudflareEnabled: Boolean(remoteAccess?.providers.cloudflare.enabled),
remoteShortLivedEnabled: Boolean(remoteAccess?.tokenStrategy.shortLived.enabled),
remoteShortLivedTtlMs: Number(remoteAccess?.tokenStrategy.shortLived.ttlMs ?? 900_000),
remotePersistentToken: persistentToken ? maskRemoteToken(persistentToken) : null,
},
});
if (!remoteAccess) {
throw new ApiError(409, "Remote access is not configured", { code: "REMOTE_ACCESS_DISABLED" });
}
res.json({ settings: toRemoteSettingsPayload(remoteAccess) });
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err, "Failed to load remote settings");
}
});
router.put("/remote/settings", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
const settings = await scopedStore.getSettings();
const remoteAccess = settings.remoteAccess;
if (!remoteAccess) {
throw new ApiError(409, "Remote access is not configured", { code: "REMOTE_ACCESS_DISABLED" });
}
const body = (req.body ?? {}) as Record<string, unknown>;
const nextRemoteAccess = {
...remoteAccess,
enabled: body.remoteEnabled === undefined ? remoteAccess.enabled : Boolean(body.remoteEnabled),
activeProvider: body.remoteActiveProvider === undefined
? remoteAccess.activeProvider
: (body.remoteActiveProvider as "tailscale" | "cloudflare" | null),
providers: {
tailscale: {
...remoteAccess.providers.tailscale,
enabled: body.remoteTailscaleEnabled === undefined ? remoteAccess.providers.tailscale.enabled : Boolean(body.remoteTailscaleEnabled),
hostname: body.remoteTailscaleHostname === undefined ? remoteAccess.providers.tailscale.hostname : String(body.remoteTailscaleHostname ?? ""),
targetPort: body.remoteTailscaleTargetPort === undefined ? remoteAccess.providers.tailscale.targetPort : Number(body.remoteTailscaleTargetPort ?? 4040),
acceptRoutes: body.remoteTailscaleAcceptRoutes === undefined ? remoteAccess.providers.tailscale.acceptRoutes : Boolean(body.remoteTailscaleAcceptRoutes),
},
cloudflare: {
...remoteAccess.providers.cloudflare,
enabled: body.remoteCloudflareEnabled === undefined ? remoteAccess.providers.cloudflare.enabled : Boolean(body.remoteCloudflareEnabled),
tunnelName: body.remoteCloudflareTunnelName === undefined ? remoteAccess.providers.cloudflare.tunnelName : String(body.remoteCloudflareTunnelName ?? ""),
tunnelToken: body.remoteCloudflareTunnelToken === undefined
? remoteAccess.providers.cloudflare.tunnelToken
: (body.remoteCloudflareTunnelToken ? String(body.remoteCloudflareTunnelToken) : null),
ingressUrl: body.remoteCloudflareIngressUrl === undefined ? remoteAccess.providers.cloudflare.ingressUrl : String(body.remoteCloudflareIngressUrl ?? ""),
},
},
tokenStrategy: {
persistent: {
...remoteAccess.tokenStrategy.persistent,
enabled: body.remotePersistentEnabled === undefined ? remoteAccess.tokenStrategy.persistent.enabled : Boolean(body.remotePersistentEnabled),
token: body.remotePersistentToken === undefined
? remoteAccess.tokenStrategy.persistent.token
: (body.remotePersistentToken ? String(body.remotePersistentToken) : null),
},
shortLived: {
...remoteAccess.tokenStrategy.shortLived,
enabled: body.remoteShortLivedEnabled === undefined ? remoteAccess.tokenStrategy.shortLived.enabled : Boolean(body.remoteShortLivedEnabled),
ttlMs: body.remoteShortLivedTtlMs === undefined ? remoteAccess.tokenStrategy.shortLived.ttlMs : Number(body.remoteShortLivedTtlMs ?? 900_000),
maxTtlMs: body.remoteShortLivedMaxTtlMs === undefined ? remoteAccess.tokenStrategy.shortLived.maxTtlMs : Number(body.remoteShortLivedMaxTtlMs ?? 86_400_000),
},
},
lifecycle: {
...remoteAccess.lifecycle,
rememberLastRunning: body.remoteRememberLastRunning === undefined ? remoteAccess.lifecycle.rememberLastRunning : Boolean(body.remoteRememberLastRunning),
wasRunningOnShutdown: body.remoteWasRunningOnShutdown === undefined ? remoteAccess.lifecycle.wasRunningOnShutdown : Boolean(body.remoteWasRunningOnShutdown),
lastRunningProvider: body.remoteLastStartedProvider === undefined
? remoteAccess.lifecycle.lastRunningProvider
: (body.remoteLastStartedProvider as "tailscale" | "cloudflare" | null),
},
};
await scopedStore.updateSettings({ remoteAccess: nextRemoteAccess });
res.json({ settings: toRemoteSettingsPayload(nextRemoteAccess) });
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err, "Failed to update remote settings");
}
});
router.get("/remote/status", async (req, res) => {
try {
const { store: scopedStore, engine } = await getProjectContext(req);
@@ -452,7 +535,10 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
: remoteAccess;
const issued = issueRemoteAuthToken("short-lived", modeSettings);
res.json({ token: issued.token, expiresAt: issued.expiresAt ?? null, ttlMs: modeSettings.tokenStrategy.shortLived.ttlMs });
const effectiveTtlMs = issued.expiresAt
? Math.max(0, Date.parse(issued.expiresAt) - Date.now())
: modeSettings.tokenStrategy.shortLived.ttlMs;
res.json({ token: issued.token, expiresAt: issued.expiresAt ?? null, ttlMs: effectiveTtlMs });
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err, "Failed to generate short-lived token");