Files
fusion/packages/dashboard/app/api/projects/remote.ts
gsxdsm cb57093d03 refactor: domain folder layout (types, API, core, engine) (#2398)
## Summary

Wave 17 organizes Fusion into **domain folders** (stacks on #2397).

### Layout
- **core/types/** — board, task, agents, settings, merge, workflow,
mesh, …
- **core/src/** — agents, ai, async-stores, workflows, tasks, config,
db, …
- **dashboard/app/api/** — client, tasks, agents, git, missions,
planning, …
- **engine/src/** — agents, auth, execution, merge, missions, overseer,
worktree, …

Root keepers retained for large entrypoints (`store.ts`, `executor.ts`,
`merger.ts`, …).

Public barrels (`@fusion/core`, `@fusion/engine`, `app/api.ts` → legacy)
stay stable.

## Test plan
- [x] `@fusion/core` typecheck
- [x] `@fusion/engine` typecheck (pre-existing playwright-core noise
only)
- [ ] CI merge gate

**Stack:** #2394 → #2397 → **this PR**
2026-08-03 00:20:53 -07:00

152 lines
5.5 KiB
TypeScript

/**
* FNXC:CodeOrganization 2026-07-16-20:00:
* Remote access / tunnel client API peeled from legacy.ts.
*/
import { api } from "../client/client.js";
import { withProjectId } from "../client/health.js";
export interface RemoteSettings {
remoteActiveProvider: "tailscale" | "cloudflare" | null;
remoteTailscaleEnabled: boolean;
remoteTailscaleHostname: string;
remoteTailscaleTargetPort: number;
remoteTailscaleAcceptRoutes: boolean;
remoteCloudflareEnabled: boolean;
remoteCloudflareQuickTunnel: boolean;
remoteCloudflareTunnelName: string;
remoteCloudflareTunnelToken: string | null;
remoteCloudflareIngressUrl: string;
remotePersistentToken: string | null;
remoteShortLivedEnabled: boolean;
remoteShortLivedTtlMs: number;
remoteShortLivedMaxTtlMs: number;
remoteRememberLastRunning: boolean;
remoteWasRunningOnShutdown: boolean;
remoteLastStartedProvider: "tailscale" | "cloudflare" | null;
}
export interface RemoteStatus {
provider: "tailscale" | "cloudflare" | null;
state: "stopped" | "starting" | "running" | "stopping" | "failed";
url: string | null;
lastError: string | null;
lastErrorCode?: string | null;
cloudflaredAvailable?: boolean | null;
externalTunnel?: {
provider: "tailscale" | "cloudflare";
url: string | null;
} | null;
restore?: {
outcome: "applied" | "skipped" | "failed";
reason: string;
at: string;
provider: "tailscale" | "cloudflare" | null;
message?: string;
};
}
export function fetchRemoteSettings(projectId?: string): Promise<{ settings: RemoteSettings }> {
return api<{ settings: RemoteSettings }>(withProjectId("/remote/settings", projectId));
}
export function updateRemoteSettings(
settings: Partial<RemoteSettings>,
projectId?: string,
): Promise<{ settings: RemoteSettings }> {
return api<{ settings: RemoteSettings }>(withProjectId("/remote/settings", projectId), {
method: "PUT",
body: JSON.stringify(settings),
});
}
export function fetchRemoteStatus(projectId?: string): Promise<RemoteStatus> {
return api<RemoteStatus>(withProjectId("/remote/status", projectId));
}
export function installCloudflared(projectId?: string): Promise<{ success: boolean; command: string; error?: string }> {
return api(withProjectId("/remote/install-cloudflared", projectId), {
method: "POST",
});
}
export function activateRemoteProvider(provider: "tailscale" | "cloudflare", projectId?: string): Promise<{ activeProvider: "tailscale" | "cloudflare" }> {
return api<{ activeProvider: "tailscale" | "cloudflare" }>(withProjectId("/remote/provider/activate", projectId), {
method: "POST",
body: JSON.stringify({ provider }),
});
}
export function startRemoteTunnel(projectId?: string): Promise<{ state: "starting" | "running"; provider: string }> {
return api<{ state: "starting" | "running"; provider: string }>(withProjectId("/remote/tunnel/start", projectId), {
method: "POST",
});
}
export function stopRemoteTunnel(projectId?: string): Promise<{ state: "stopped"; provider: string | null }> {
return api<{ state: "stopped"; provider: string | null }>(withProjectId("/remote/tunnel/stop", projectId), {
method: "POST",
});
}
export function killExternalTunnel(projectId?: string): Promise<{ ok: boolean }> {
return api<{ ok: boolean }>(withProjectId("/remote/tunnel/kill-external", projectId), {
method: "POST",
});
}
export function regenerateRemotePersistentToken(projectId?: string): Promise<{ token: string; maskedToken: string }> {
return api<{ token: string; maskedToken: string }>(withProjectId("/remote/token/persistent/regenerate", projectId), {
method: "POST",
});
}
export function generateShortLivedRemoteToken(ttlMs: number, projectId?: string): Promise<{ token: string; expiresAt: string; ttlMs: number }> {
return api<{ token: string; expiresAt: string; ttlMs: number }>(withProjectId("/remote/token/short-lived/generate", projectId), {
method: "POST",
body: JSON.stringify({ ttlMs }),
});
}
type RemoteAuthTokenType = "persistent" | "short-lived";
type RemoteLinkRequestOptions = {
projectId?: string;
tokenType?: RemoteAuthTokenType;
ttlMs?: number;
};
function buildRemoteAuthQuery(
format: "text" | "image/svg" | null,
tokenType: RemoteAuthTokenType,
ttlMs?: number,
): string {
const params = new URLSearchParams();
if (format) {
params.set("format", format);
}
params.set("tokenType", tokenType);
if (tokenType === "short-lived" && typeof ttlMs === "number" && Number.isFinite(ttlMs)) {
params.set("ttlMs", String(ttlMs));
}
const query = params.toString();
return query ? `?${query}` : "";
}
export function fetchRemoteUrl(
options: RemoteLinkRequestOptions = {},
): Promise<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null }> {
const { projectId, tokenType = "persistent", ttlMs } = options;
const query = buildRemoteAuthQuery(null, tokenType, ttlMs);
return api<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null }>(withProjectId(`/remote/url${query}`, projectId));
}
export function fetchRemoteQr(
format: "text" | "image/svg" = "text",
options: RemoteLinkRequestOptions = {},
): Promise<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null; format: "text" | "image/svg"; data?: string }> {
const { projectId, tokenType = "persistent", ttlMs } = options;
const query = buildRemoteAuthQuery(format, tokenType, ttlMs);
return api<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null; format: "text" | "image/svg"; data?: string }>(withProjectId(`/remote/qr${query}`, projectId));
}