feat(FN-2519): add remote tunnel manager for settings sync

- Add remote-access contracts, provider adapters, and a tunnel process manager with lifecycle handling
- Wire tunnel manager into ProjectEngine startup/shutdown flow and export new remote-access modules
- Update settings modal UX for remote auth URLs, including wrapping and related UI test coverage
- Document tunnel manager behavior and remote settings sync details in architecture, CLI, and settings docs
This commit is contained in:
Fusion
2026-04-26 02:20:28 -07:00
committed by gsxdsm
parent a9e68b38eb
commit db9f4ba57e
16 changed files with 1234 additions and 28 deletions

View File

@@ -0,0 +1,55 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { getTunnelProviderAdapter, redactTunnelText } from "../remote-access/provider-adapters.js";
describe("remote-access provider adapters", () => {
it("builds redacted command previews and masks sensitive values", () => {
const adapter = getTunnelProviderAdapter("cloudflare");
const command = adapter.buildCommand({
provider: "cloudflare",
executablePath: "cloudflared",
args: ["tunnel", "--token", "very-secret-token"],
tokenEnvVar: "CLOUDFLARED_TOKEN",
env: {
CLOUDFLARED_TOKEN: "very-secret-token",
},
});
expect(command.redactedPreview).toContain("[REDACTED]");
expect(command.redactedPreview).not.toContain("very-secret-token");
expect(redactTunnelText("token=very-secret-token", command.sensitiveValues)).toBe("token=[REDACTED]");
});
it("fails config validation when token env var reference is missing", () => {
const adapter = getTunnelProviderAdapter("tailscale");
expect(() =>
adapter.buildCommand({
provider: "tailscale",
executablePath: "tailscale",
args: ["serve", "status"],
tokenEnvVar: "TS_AUTHKEY",
}),
).toThrow(/invalid_config:missing credential in env var TS_AUTHKEY/);
});
it("validates cloudflare credentialsPath when path is provided", () => {
const tempDir = mkdtempSync(join(tmpdir(), "fn-remote-access-"));
const credentialsPath = join(tempDir, "credentials.json");
writeFileSync(credentialsPath, "{}", "utf8");
try {
const adapter = getTunnelProviderAdapter("cloudflare");
const command = adapter.buildCommand({
provider: "cloudflare",
executablePath: "cloudflared",
args: ["tunnel", "run"],
credentialsPath,
});
expect(command.command).toBe("cloudflared");
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});