refactor(web): rewire attach.ts to same-origin broker endpoint (NAP-241)
ci / rust (push) Failing after 8m45s

Browser now opens ws://{host}/api/attach/:id directly — maestro-web
reverse-proxies it to the home daemon's broker, which short-circuits to
the local PTY (NAP-243) or, once NAP-247 lands, brokers to a peer over T2.

Drops the old getConnectInfo + daemon_url path: the browser no longer
knows or cares which daemon owns a session. ConnectInfo type and the
helper are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 18:23:21 +00:00
co-authored by Claude Opus 4.7
parent bc3237287f
commit 464bea2fae
3 changed files with 12 additions and 44 deletions
+1 -16
View File
@@ -1,11 +1,4 @@
import type {
ApiError,
ConnectInfo,
CreateSessionRequest,
DaemonInfo,
Session,
Uuid,
} from "./types.js";
import type { ApiError, CreateSessionRequest, DaemonInfo, Session, Uuid } from "./types.js";
async function jsonOrError<T>(res: Response): Promise<T> {
if (res.ok) {
@@ -50,14 +43,6 @@ export async function killSession(id: Uuid): Promise<void> {
);
}
export async function getConnectInfo(id: Uuid): Promise<ConnectInfo> {
return jsonOrError(
await fetch(`/api/sessions/${encodeURIComponent(id)}/connect-info`, {
credentials: "same-origin",
}),
);
}
export function eventsWsUrl(): string {
const proto = location.protocol === "https:" ? "wss:" : "ws:";
return `${proto}//${location.host}/api/events`;
+11 -23
View File
@@ -2,7 +2,6 @@ import { FitAddon } from "@xterm/addon-fit";
import { Terminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import { getConnectInfo } from "./api.js";
import type { ClientControl, Uuid } from "./types.js";
interface AttachHandle {
@@ -13,16 +12,15 @@ function statusMessage(host: HTMLElement, kind: "info" | "error", text: string):
host.innerHTML = `<div class="attach-status status-${kind}">${text}</div>`;
}
function streamUrl(daemonUrl: string, sessionId: Uuid, jwt: string): string {
let base = daemonUrl;
// Allow operators to give an http(s) URL; convert to ws(s).
if (base.startsWith("http://")) base = "ws://" + base.slice("http://".length);
else if (base.startsWith("https://")) base = "wss://" + base.slice("https://".length);
// Trim trailing slash.
base = base.replace(/\/+$/, "");
const encId = encodeURIComponent(sessionId);
const encJwt = encodeURIComponent(jwt);
return `${base}/sessions/${encId}/stream?token=${encJwt}`;
/**
* Same-origin broker URL. The browser loads from `maestro-web`, which
* reverse-proxies this WebSocket to the co-located daemon's
* `/api/attach/:id` broker endpoint. The daemon short-circuits to the local
* PTY when it owns the session; cross-daemon brokering lands with NAP-247.
*/
function brokerStreamUrl(sessionId: Uuid): string {
const proto = location.protocol === "https:" ? "wss:" : "ws:";
return `${proto}//${location.host}/api/attach/${encodeURIComponent(sessionId)}`;
}
export async function renderAttach(
@@ -51,16 +49,7 @@ export async function renderAttach(
stateEl.dataset.state = kind;
};
statusMessage(body, "info", "Fetching connect info…");
let connect;
try {
connect = await getConnectInfo(sessionId);
} catch (e) {
setState("error", "error");
statusMessage(body, "error", `Failed to get connect info: ${(e as Error).message}`);
return { dispose: () => {} };
}
statusMessage(body, "info", "Connecting…");
body.innerHTML = "";
@@ -91,8 +80,7 @@ export async function renderAttach(
};
tryFit();
const wsUrl = streamUrl(connect.daemon_url, sessionId, connect.jwt);
const ws = new WebSocket(wsUrl);
const ws = new WebSocket(brokerStreamUrl(sessionId));
ws.binaryType = "arraybuffer";
const sendControl = (msg: ClientControl) => {
-5
View File
@@ -24,11 +24,6 @@ export interface DaemonInfo {
daemon_url: string;
}
export interface ConnectInfo {
daemon_url: string;
jwt: string;
}
export interface WorktreeSpec {
base_repo: string;
branch: string;