tmux-ide 2.9.0-beta.21 → 2.9.0-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +46791 -43589
- package/package.json +2 -2
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
- package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
- package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
- package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
- package/packages/contracts/src/actions-contract.ts +12 -0
- package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
- package/packages/contracts/src/daemon-wire.ts +10 -5
- package/packages/contracts/src/fleet-client-state.ts +3 -0
- package/packages/contracts/src/fleet-lifecycle.ts +5 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/interaction-receipts.ts +39 -0
- package/packages/contracts/src/issue-error.ts +1 -1
- package/packages/contracts/src/multiplexer-verbs.ts +25 -1
- package/packages/contracts/src/pane-stream.ts +51 -8
- package/packages/contracts/src/tmux-server-scope.ts +172 -0
- package/packages/contracts/src/window-links.ts +76 -0
- package/packages/contracts/src/workspace-multiplexer.ts +54 -0
- package/packages/core/src/interaction-receipts.ts +4 -0
- package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
- package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
- package/packages/daemon/dist/command-center/actions/registry.js +13 -1
- package/packages/daemon/dist/command-center/discovery.js +2 -9
- package/packages/daemon/dist/command-center/server.js +21 -1
- package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
- package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
- package/packages/daemon/dist/lib/daemon-embed.js +198 -30
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
- package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
- package/packages/daemon/dist/lib/headless-daemon.js +10 -1
- package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
- package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
- package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
- package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
- package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
- package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
- package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
- package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
- package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
- package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
- package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
- package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
- package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
- package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
- package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
- package/packages/daemon/dist/lib/workspace-registry.js +4 -0
- package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
- package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
- package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
- package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
- package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
- package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
- package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
- package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
- package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
- package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
- package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
- package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
- package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
- package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
- package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
- package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
- package/packages/daemon/src/command-center/actions/errors.ts +4 -0
- package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
- package/packages/daemon/src/command-center/actions/registry.ts +14 -0
- package/packages/daemon/src/command-center/discovery.ts +2 -10
- package/packages/daemon/src/command-center/server.ts +29 -1
- package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
- package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
- package/packages/daemon/src/lib/daemon-embed.ts +241 -28
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
- package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
- package/packages/daemon/src/lib/headless-daemon.ts +18 -1
- package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
- package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
- package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
- package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
- package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
- package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
- package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
- package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
- package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
- package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
- package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
- package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
- package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
- package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
- package/packages/daemon/src/lib/workspace-registry.ts +5 -0
- package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
- package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
- package/packages/daemon/src/terminal/mirror/events.ts +2 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
- package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
- package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
- package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
- package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
- package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
- package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
- package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
- package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
- package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
- package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
- package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
- package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
- package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
- package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
- package/packages/daemon-client/package.json +3 -1
- package/packages/daemon-client/src/index.ts +2 -0
- package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
- package/packages/daemon-client/src/pane-stream-client.ts +26 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
- package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
- package/packages/daemon-client/src/tmux-server-client.ts +287 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
PANE_STREAM_PROTOCOL_VERSION,
|
|
4
|
+
PANE_STREAM_REDEEM_PATH,
|
|
5
|
+
PANE_STREAM_WEBSOCKET_SUBPROTOCOL,
|
|
6
|
+
tmuxServerPaneStreamPath,
|
|
7
|
+
} from "@tmux-ide/contracts";
|
|
8
|
+
import {
|
|
9
|
+
createTmuxServerClient,
|
|
10
|
+
listTmuxServers,
|
|
11
|
+
registerTmuxServer,
|
|
12
|
+
removeTmuxServer,
|
|
13
|
+
} from "./tmux-server-client.ts";
|
|
14
|
+
const scope = {
|
|
15
|
+
serverId: `tmux-server.${"a".repeat(32)}`,
|
|
16
|
+
generation: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
17
|
+
};
|
|
18
|
+
const other = { ...scope, serverId: `tmux-server.${"b".repeat(32)}` };
|
|
19
|
+
const requestId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
|
20
|
+
const base = {
|
|
21
|
+
baseUrl: "http://127.0.0.1:4000",
|
|
22
|
+
ownerToken: "owner",
|
|
23
|
+
hostClientId: "client",
|
|
24
|
+
origin: "http://localhost:3000",
|
|
25
|
+
};
|
|
26
|
+
const sessions = (server = scope) => ({
|
|
27
|
+
version: 1,
|
|
28
|
+
server,
|
|
29
|
+
sessions: [
|
|
30
|
+
{
|
|
31
|
+
liveSessionId: `live-session.${"a".repeat(20)}`,
|
|
32
|
+
sessionName: "main",
|
|
33
|
+
workspaceName: "alias",
|
|
34
|
+
paneCount: 1,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
const stream = {
|
|
39
|
+
protocolVersion: PANE_STREAM_PROTOCOL_VERSION,
|
|
40
|
+
workspaceName: "main",
|
|
41
|
+
panes: ["pane.one"],
|
|
42
|
+
viewerMode: "interactive" as const,
|
|
43
|
+
};
|
|
44
|
+
const issued = () => ({
|
|
45
|
+
status: "issued",
|
|
46
|
+
descriptor: {
|
|
47
|
+
protocolVersion: PANE_STREAM_PROTOCOL_VERSION,
|
|
48
|
+
webSocketUrl: `ws://127.0.0.1:4000${tmuxServerPaneStreamPath(scope)}`,
|
|
49
|
+
subprotocol: PANE_STREAM_WEBSOCKET_SUBPROTOCOL,
|
|
50
|
+
redemptionTicket: `ps2_${"a".repeat(43)}`,
|
|
51
|
+
daemonInstanceId: scope.generation,
|
|
52
|
+
requestId,
|
|
53
|
+
expiresAt: Date.now() + 10000,
|
|
54
|
+
panes: ["pane.one"],
|
|
55
|
+
effectiveViewerMode: "interactive",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("explicit tmux server SDK client", () => {
|
|
60
|
+
it("validates versioned server listing without fallback", async () => {
|
|
61
|
+
const fetcher = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
62
|
+
expect(String(url)).toBe(`${base.baseUrl}/api/v1/tmux-servers`);
|
|
63
|
+
expect(init?.headers).toMatchObject({ Authorization: "Bearer owner" });
|
|
64
|
+
return Response.json({
|
|
65
|
+
version: 1,
|
|
66
|
+
servers: [{ ...scope, state: "online", label: "default" }],
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
expect(
|
|
70
|
+
(await listTmuxServers({ ...base, fetch: fetcher as typeof fetch })).servers,
|
|
71
|
+
).toHaveLength(1);
|
|
72
|
+
await expect(
|
|
73
|
+
listTmuxServers({
|
|
74
|
+
...base,
|
|
75
|
+
fetch: mock(async () => Response.json({ version: 2, servers: [] })) as typeof fetch,
|
|
76
|
+
}),
|
|
77
|
+
).rejects.toThrow();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("binds copied inputs and cache keys to an immutable exact scope", async () => {
|
|
81
|
+
const input = { ...scope };
|
|
82
|
+
const fetcher = mock(async (url: string | URL | Request) => {
|
|
83
|
+
expect(String(url)).toContain(`/${scope.serverId}/${scope.generation}/sessions`);
|
|
84
|
+
return Response.json(sessions());
|
|
85
|
+
});
|
|
86
|
+
const client = createTmuxServerClient({ ...base, fetch: fetcher as typeof fetch }, input);
|
|
87
|
+
input.serverId = other.serverId;
|
|
88
|
+
expect((await client.sessions()).server).toEqual(scope);
|
|
89
|
+
expect(Object.isFrozen(client.scope)).toBe(true);
|
|
90
|
+
expect(client.cacheKey("pane", "%0")).not.toBe(
|
|
91
|
+
createTmuxServerClient(base, other).cacheKey("pane", "%0"),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("rejects scope mismatches and stale HTTP refusal without retries", async () => {
|
|
96
|
+
const fetcher = mock(async () => Response.json(sessions(other)));
|
|
97
|
+
await expect(
|
|
98
|
+
createTmuxServerClient({ ...base, fetch: fetcher as typeof fetch }, scope).sessions(),
|
|
99
|
+
).rejects.toMatchObject({ code: "scope-mismatch" });
|
|
100
|
+
const refused = mock(async () =>
|
|
101
|
+
Response.json({ error: { code: "stale-generation" } }, { status: 409 }),
|
|
102
|
+
);
|
|
103
|
+
await expect(
|
|
104
|
+
createTmuxServerClient({ ...base, fetch: refused as typeof fetch }, scope).sessions(),
|
|
105
|
+
).rejects.toMatchObject({ status: 409 });
|
|
106
|
+
expect(refused).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("drops an old selection's late response after disposal", async () => {
|
|
110
|
+
let resolve!: (response: Response) => void;
|
|
111
|
+
const pending = new Promise<Response>((done) => {
|
|
112
|
+
resolve = done;
|
|
113
|
+
});
|
|
114
|
+
const fetcher = mock(async () => pending);
|
|
115
|
+
const client = createTmuxServerClient({ ...base, fetch: fetcher as typeof fetch }, scope);
|
|
116
|
+
const result = client.sessions();
|
|
117
|
+
client.dispose();
|
|
118
|
+
resolve(Response.json(sessions()));
|
|
119
|
+
await expect(result).rejects.toMatchObject({ code: "disposed" });
|
|
120
|
+
await expect(client.sessions()).rejects.toMatchObject({ code: "disposed" });
|
|
121
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("uses explicit owner authority for passive fleet close without a controller principal", async () => {
|
|
125
|
+
const fetcher = mock(async (_url: RequestInfo | URL, init?: RequestInit) => {
|
|
126
|
+
expect(new Headers(init?.headers).has("X-Tmux-Ide-Host-Client-Id")).toBe(false);
|
|
127
|
+
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer owner");
|
|
128
|
+
return Response.json({
|
|
129
|
+
operationId: requestId,
|
|
130
|
+
daemonInstanceId: scope.generation,
|
|
131
|
+
workspaceName: "same",
|
|
132
|
+
verb: "workspace.session.kill",
|
|
133
|
+
outcome: "applied",
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
await createTmuxServerClient({ ...base, fetch: fetcher as typeof fetch }, scope).mutate(
|
|
137
|
+
requestId,
|
|
138
|
+
{
|
|
139
|
+
verb: "workspace.session.kill",
|
|
140
|
+
workspaceName: "same",
|
|
141
|
+
fleetTarget: {
|
|
142
|
+
daemonInstanceId: scope.generation,
|
|
143
|
+
liveSessionId: `live-session.${"a".repeat(20)}`,
|
|
144
|
+
sessionName: "same",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("sends owner-scoped mutations and verifies generation and operation echoes", async () => {
|
|
152
|
+
const fetcher = mock(async (_url: string | URL | Request, init?: RequestInit) => {
|
|
153
|
+
const body = JSON.parse(String(init?.body));
|
|
154
|
+
expect(body.expectedDaemonInstanceId).toBe(scope.generation);
|
|
155
|
+
return Response.json({
|
|
156
|
+
operationId: body.operationId,
|
|
157
|
+
daemonInstanceId: scope.generation,
|
|
158
|
+
outcome: "applied",
|
|
159
|
+
workspaceName: "main",
|
|
160
|
+
verb: "workspace.pane.select",
|
|
161
|
+
semanticPaneId: "pane.one",
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
const client = createTmuxServerClient({ ...base, fetch: fetcher as typeof fetch }, scope);
|
|
165
|
+
const result = await client.mutate(requestId, {
|
|
166
|
+
verb: "workspace.pane.select",
|
|
167
|
+
workspaceName: "main",
|
|
168
|
+
semanticPaneId: "pane.one",
|
|
169
|
+
});
|
|
170
|
+
expect(result.daemonInstanceId).toBe(scope.generation);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("sends exact issue headers and validates scoped redemption", async () => {
|
|
174
|
+
const fetcher = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
175
|
+
expect(String(url)).toEndWith(`/${scope.serverId}/${scope.generation}/pane-streams/issue`);
|
|
176
|
+
expect(init?.headers).toMatchObject({
|
|
177
|
+
Authorization: "Bearer owner",
|
|
178
|
+
Origin: base.origin,
|
|
179
|
+
"X-Tmux-Ide-Request-Id": requestId,
|
|
180
|
+
"X-Tmux-Ide-Expected-Daemon-Instance-Id": scope.generation,
|
|
181
|
+
"X-Tmux-Ide-Host-Client-Id": "client",
|
|
182
|
+
});
|
|
183
|
+
return Response.json(issued());
|
|
184
|
+
});
|
|
185
|
+
expect(
|
|
186
|
+
(
|
|
187
|
+
await createTmuxServerClient(
|
|
188
|
+
{ ...base, fetch: fetcher as typeof fetch },
|
|
189
|
+
scope,
|
|
190
|
+
).issuePaneStream(requestId, stream)
|
|
191
|
+
).status,
|
|
192
|
+
).toBe("issued");
|
|
193
|
+
const wrong = issued();
|
|
194
|
+
wrong.descriptor.webSocketUrl = `ws://127.0.0.1:4000${tmuxServerPaneStreamPath(other)}`;
|
|
195
|
+
await expect(
|
|
196
|
+
createTmuxServerClient(
|
|
197
|
+
{ ...base, fetch: mock(async () => Response.json(wrong)) as typeof fetch },
|
|
198
|
+
scope,
|
|
199
|
+
).issuePaneStream(requestId, stream),
|
|
200
|
+
).rejects.toMatchObject({ code: "scope-mismatch" });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("rejects inventories from the wrong workspace", async () => {
|
|
204
|
+
const fetcher = mock(async () =>
|
|
205
|
+
Response.json({
|
|
206
|
+
version: 1,
|
|
207
|
+
server: scope,
|
|
208
|
+
resource: {
|
|
209
|
+
workspaceName: "other",
|
|
210
|
+
workspaceId: "workspace.0123456789abcdefabcd",
|
|
211
|
+
sessionId: "session.0123456789abcdefabcd",
|
|
212
|
+
resourceRevision: 0,
|
|
213
|
+
semanticPaneIds: ["pane.one"],
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
await expect(
|
|
218
|
+
createTmuxServerClient({ ...base, fetch: fetcher as typeof fetch }, scope).inventory("main"),
|
|
219
|
+
).rejects.toMatchObject({ code: "scope-mismatch" });
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("server registration and adopted default", () => {
|
|
224
|
+
it("passes remote selector intent unchanged and removes by opaque registration", async () => {
|
|
225
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
226
|
+
const fetcher = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
227
|
+
calls.push({ url: String(url), init });
|
|
228
|
+
return Response.json(
|
|
229
|
+
init?.method === "DELETE"
|
|
230
|
+
? { removed: true }
|
|
231
|
+
: { ...scope, label: "remote", state: "online" },
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
const options = { ...base, fetch: fetcher as typeof fetch };
|
|
235
|
+
await registerTmuxServer(options, {
|
|
236
|
+
label: "remote",
|
|
237
|
+
selector: { kind: "path", path: "/remote/socket" },
|
|
238
|
+
});
|
|
239
|
+
expect(JSON.parse(String(calls[0]?.init?.body)).selector).toEqual({
|
|
240
|
+
kind: "path",
|
|
241
|
+
path: "/remote/socket",
|
|
242
|
+
});
|
|
243
|
+
await removeTmuxServer(options, scope.serverId);
|
|
244
|
+
expect(calls[1]?.init?.method).toBe("DELETE");
|
|
245
|
+
expect(calls[1]?.url).toEndWith(`/${scope.serverId}`);
|
|
246
|
+
await expect(removeTmuxServer(options, "../default")).rejects.toThrow();
|
|
247
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("accepts default redemption only with an exact generation and request echo", async () => {
|
|
251
|
+
const response = issued();
|
|
252
|
+
response.descriptor.webSocketUrl = `ws://127.0.0.1:4000${PANE_STREAM_REDEEM_PATH}`;
|
|
253
|
+
const fetcher = mock(async () => Response.json(response));
|
|
254
|
+
const client = createTmuxServerClient({ ...base, fetch: fetcher as typeof fetch }, scope);
|
|
255
|
+
expect((await client.issuePaneStream(requestId, stream)).status).toBe("issued");
|
|
256
|
+
response.descriptor.daemonInstanceId = requestId;
|
|
257
|
+
await expect(client.issuePaneStream(requestId, stream)).rejects.toMatchObject({
|
|
258
|
+
code: "scope-mismatch",
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("preserves authoritative workspace aliases and unavailable collisions", async () => {
|
|
263
|
+
const response = sessions();
|
|
264
|
+
const client = createTmuxServerClient(
|
|
265
|
+
{ ...base, fetch: mock(async () => Response.json(response)) as typeof fetch },
|
|
266
|
+
scope,
|
|
267
|
+
);
|
|
268
|
+
expect((await client.sessions()).sessions[0]?.workspaceName).toBe("alias");
|
|
269
|
+
const unavailable = {
|
|
270
|
+
...response,
|
|
271
|
+
sessions: response.sessions.map((session) => ({ ...session, workspaceName: null })),
|
|
272
|
+
};
|
|
273
|
+
const otherClient = createTmuxServerClient(
|
|
274
|
+
{ ...base, fetch: mock(async () => Response.json(unavailable)) as typeof fetch },
|
|
275
|
+
scope,
|
|
276
|
+
);
|
|
277
|
+
expect((await otherClient.sessions()).sessions[0]?.workspaceName).toBeNull();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TMUX_SERVERS_API_PATH,
|
|
3
|
+
PANE_STREAM_REDEEM_PATH,
|
|
4
|
+
TmuxServerIdSchemaZ,
|
|
5
|
+
TmuxServerDescriptorSchemaZ,
|
|
6
|
+
TmuxServerRegistrationRequestSchemaZ,
|
|
7
|
+
type TmuxServerRegistrationRequest,
|
|
8
|
+
TmuxServerScopeSchemaZ,
|
|
9
|
+
TmuxServersResourceSchemaZ,
|
|
10
|
+
TmuxServerSessionsResourceSchemaZ,
|
|
11
|
+
TerminalRuntimeInventoryProjectionV1SchemaZ,
|
|
12
|
+
WorkspaceMultiplexerMutationRequestSchemaZ,
|
|
13
|
+
WorkspacePaneCreateMutationRequestSchemaZ,
|
|
14
|
+
WorkspacePaneCreateMutationResultSchemaZ,
|
|
15
|
+
type WorkspacePaneCreateArguments,
|
|
16
|
+
WorkspaceSessionCreateArgumentsSchemaZ,
|
|
17
|
+
WorkspaceSessionCreateResultSchemaZ,
|
|
18
|
+
type WorkspaceSessionCreateArguments,
|
|
19
|
+
WorkspaceMultiplexerMutationResultSchemaZ,
|
|
20
|
+
PaneStreamIssueMutationRequestSchemaZ,
|
|
21
|
+
PaneStreamIssueResultSchemaZ,
|
|
22
|
+
tmuxServerPaneStreamPath,
|
|
23
|
+
tmuxServerScopedResourceKey,
|
|
24
|
+
type TmuxServerScope,
|
|
25
|
+
type WorkspaceMultiplexerIntent,
|
|
26
|
+
type PaneStreamLeaseRequest,
|
|
27
|
+
} from "@tmux-ide/contracts";
|
|
28
|
+
|
|
29
|
+
export interface TmuxServerClientOptions {
|
|
30
|
+
readonly baseUrl: string;
|
|
31
|
+
readonly ownerToken: string;
|
|
32
|
+
readonly hostClientId: string;
|
|
33
|
+
readonly origin: string;
|
|
34
|
+
readonly fetch?: typeof fetch;
|
|
35
|
+
readonly timeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export class TmuxServerClientError extends Error {
|
|
38
|
+
constructor(
|
|
39
|
+
readonly code: "disposed" | "scope-mismatch" | "request-failed",
|
|
40
|
+
readonly status?: number,
|
|
41
|
+
) {
|
|
42
|
+
super(`tmux server client ${code}`);
|
|
43
|
+
this.name = "TmuxServerClientError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function request(
|
|
47
|
+
options: TmuxServerClientOptions,
|
|
48
|
+
path: string,
|
|
49
|
+
body?: unknown,
|
|
50
|
+
headers?: Record<string, string>,
|
|
51
|
+
method?: "DELETE",
|
|
52
|
+
) {
|
|
53
|
+
const response = await (options.fetch ?? fetch)(new URL(path, options.baseUrl), {
|
|
54
|
+
method: method ?? (body === undefined ? "GET" : "POST"),
|
|
55
|
+
headers: {
|
|
56
|
+
Authorization: `Bearer ${options.ownerToken}`,
|
|
57
|
+
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
58
|
+
...headers,
|
|
59
|
+
},
|
|
60
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
61
|
+
redirect: "error",
|
|
62
|
+
cache: "no-store",
|
|
63
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 5_000),
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) throw new TmuxServerClientError("request-failed", response.status);
|
|
66
|
+
return (await response.json()) as unknown;
|
|
67
|
+
}
|
|
68
|
+
export async function listTmuxServers(options: TmuxServerClientOptions) {
|
|
69
|
+
return TmuxServersResourceSchemaZ.parse(await request(options, TMUX_SERVERS_API_PATH));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function registerTmuxServer(
|
|
73
|
+
options: TmuxServerClientOptions,
|
|
74
|
+
registration: TmuxServerRegistrationRequest,
|
|
75
|
+
) {
|
|
76
|
+
return TmuxServerDescriptorSchemaZ.parse(
|
|
77
|
+
await request(
|
|
78
|
+
options,
|
|
79
|
+
TMUX_SERVERS_API_PATH,
|
|
80
|
+
TmuxServerRegistrationRequestSchemaZ.parse(registration),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
export async function removeTmuxServer(
|
|
85
|
+
options: TmuxServerClientOptions,
|
|
86
|
+
serverId: string,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const response = await request(
|
|
89
|
+
options,
|
|
90
|
+
`${TMUX_SERVERS_API_PATH}/${TmuxServerIdSchemaZ.parse(serverId)}`,
|
|
91
|
+
undefined,
|
|
92
|
+
undefined,
|
|
93
|
+
"DELETE",
|
|
94
|
+
);
|
|
95
|
+
if (
|
|
96
|
+
!response ||
|
|
97
|
+
typeof response !== "object" ||
|
|
98
|
+
!("removed" in response) ||
|
|
99
|
+
response.removed !== true ||
|
|
100
|
+
Object.keys(response).length !== 1
|
|
101
|
+
)
|
|
102
|
+
throw new TypeError("Invalid server removal response");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Bind once. Selection changes dispose this client and create another; no implicit fallback. */
|
|
106
|
+
export function createTmuxServerClient(
|
|
107
|
+
optionsInput: TmuxServerClientOptions,
|
|
108
|
+
scopeInput: TmuxServerScope,
|
|
109
|
+
) {
|
|
110
|
+
const options = Object.freeze({ ...optionsInput });
|
|
111
|
+
const scope = Object.freeze(TmuxServerScopeSchemaZ.parse(scopeInput));
|
|
112
|
+
const base = `${TMUX_SERVERS_API_PATH}/${scope.serverId}/${scope.generation}`;
|
|
113
|
+
let disposed = false;
|
|
114
|
+
function assertCurrent() {
|
|
115
|
+
if (disposed) throw new TmuxServerClientError("disposed");
|
|
116
|
+
}
|
|
117
|
+
function assertScope(candidate: TmuxServerScope) {
|
|
118
|
+
if (candidate.serverId !== scope.serverId || candidate.generation !== scope.generation)
|
|
119
|
+
throw new TmuxServerClientError("scope-mismatch");
|
|
120
|
+
}
|
|
121
|
+
async function scopedRequest(suffix: string, body?: unknown, headers?: Record<string, string>) {
|
|
122
|
+
assertCurrent();
|
|
123
|
+
const result = await request(options, `${base}/${suffix}`, body, headers);
|
|
124
|
+
assertCurrent();
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
scope,
|
|
129
|
+
dispose() {
|
|
130
|
+
disposed = true;
|
|
131
|
+
},
|
|
132
|
+
cacheKey(kind: string, resourceId: string) {
|
|
133
|
+
return tmuxServerScopedResourceKey(scope, kind, resourceId);
|
|
134
|
+
},
|
|
135
|
+
async sessions() {
|
|
136
|
+
const response = TmuxServerSessionsResourceSchemaZ.parse(await scopedRequest("sessions"));
|
|
137
|
+
assertScope(response.server);
|
|
138
|
+
return response;
|
|
139
|
+
},
|
|
140
|
+
async openSession(liveSessionId: string) {
|
|
141
|
+
const raw = await scopedRequest(`sessions/${encodeURIComponent(liveSessionId)}/open`, {});
|
|
142
|
+
if (
|
|
143
|
+
!raw ||
|
|
144
|
+
typeof raw !== "object" ||
|
|
145
|
+
!("version" in raw) ||
|
|
146
|
+
raw.version !== 1 ||
|
|
147
|
+
!("server" in raw) ||
|
|
148
|
+
!("workspaceName" in raw) ||
|
|
149
|
+
typeof raw.workspaceName !== "string" ||
|
|
150
|
+
!raw.workspaceName ||
|
|
151
|
+
!("liveSessionId" in raw) ||
|
|
152
|
+
raw.liveSessionId !== liveSessionId ||
|
|
153
|
+
Object.keys(raw).length !== 4
|
|
154
|
+
)
|
|
155
|
+
throw new TypeError("Invalid scoped session-open response");
|
|
156
|
+
assertScope(TmuxServerScopeSchemaZ.parse(raw.server));
|
|
157
|
+
return {
|
|
158
|
+
version: 1 as const,
|
|
159
|
+
server: scope,
|
|
160
|
+
workspaceName: raw.workspaceName,
|
|
161
|
+
liveSessionId,
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
async inventory(workspaceName: string) {
|
|
165
|
+
const raw = await scopedRequest(`inventory/${encodeURIComponent(workspaceName)}`);
|
|
166
|
+
if (
|
|
167
|
+
!raw ||
|
|
168
|
+
typeof raw !== "object" ||
|
|
169
|
+
!("version" in raw) ||
|
|
170
|
+
raw.version !== 1 ||
|
|
171
|
+
!("server" in raw) ||
|
|
172
|
+
!("resource" in raw) ||
|
|
173
|
+
Object.keys(raw).length !== 3
|
|
174
|
+
)
|
|
175
|
+
throw new TypeError("Invalid scoped inventory response");
|
|
176
|
+
assertScope(TmuxServerScopeSchemaZ.parse(raw.server));
|
|
177
|
+
const resource = TerminalRuntimeInventoryProjectionV1SchemaZ.parse(raw.resource);
|
|
178
|
+
if (resource.workspaceName !== workspaceName)
|
|
179
|
+
throw new TmuxServerClientError("scope-mismatch");
|
|
180
|
+
return { version: 1 as const, server: scope, resource };
|
|
181
|
+
},
|
|
182
|
+
async createSession(operationId: string, intent: WorkspaceSessionCreateArguments) {
|
|
183
|
+
const safeIntent = WorkspaceSessionCreateArgumentsSchemaZ.parse(intent);
|
|
184
|
+
if (
|
|
185
|
+
safeIntent.expectedDaemonInstanceId &&
|
|
186
|
+
safeIntent.expectedDaemonInstanceId !== scope.generation
|
|
187
|
+
)
|
|
188
|
+
throw new TmuxServerClientError("scope-mismatch");
|
|
189
|
+
const result = WorkspaceSessionCreateResultSchemaZ.parse(
|
|
190
|
+
await scopedRequest("sessions/create", {
|
|
191
|
+
operationId,
|
|
192
|
+
expectedDaemonInstanceId: scope.generation,
|
|
193
|
+
intent: safeIntent,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
if (result.daemonInstanceId !== scope.generation || result.operationId !== operationId)
|
|
197
|
+
throw new TmuxServerClientError("scope-mismatch");
|
|
198
|
+
return result;
|
|
199
|
+
},
|
|
200
|
+
async createPane(
|
|
201
|
+
operationId: string,
|
|
202
|
+
intent: WorkspacePaneCreateArguments,
|
|
203
|
+
liveSessionId: string,
|
|
204
|
+
) {
|
|
205
|
+
const body = WorkspacePaneCreateMutationRequestSchemaZ.parse({
|
|
206
|
+
operationId,
|
|
207
|
+
expectedDaemonInstanceId: scope.generation,
|
|
208
|
+
intent,
|
|
209
|
+
});
|
|
210
|
+
const result = WorkspacePaneCreateMutationResultSchemaZ.parse(
|
|
211
|
+
await scopedRequest(
|
|
212
|
+
`panes/create?liveSessionId=${encodeURIComponent(liveSessionId)}`,
|
|
213
|
+
body,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
if (
|
|
217
|
+
result.daemonInstanceId !== scope.generation ||
|
|
218
|
+
result.operationId !== operationId ||
|
|
219
|
+
result.resource.workspaceName !== intent.workspaceName
|
|
220
|
+
)
|
|
221
|
+
throw new TmuxServerClientError("scope-mismatch");
|
|
222
|
+
return result;
|
|
223
|
+
},
|
|
224
|
+
async mutate(operationId: string, intent: WorkspaceMultiplexerIntent, liveSessionId?: string) {
|
|
225
|
+
const body = WorkspaceMultiplexerMutationRequestSchemaZ.parse({
|
|
226
|
+
operationId,
|
|
227
|
+
expectedDaemonInstanceId: scope.generation,
|
|
228
|
+
intent,
|
|
229
|
+
});
|
|
230
|
+
const result = WorkspaceMultiplexerMutationResultSchemaZ.parse(
|
|
231
|
+
await scopedRequest(
|
|
232
|
+
`mutations${liveSessionId ? `?liveSessionId=${encodeURIComponent(liveSessionId)}` : ""}`,
|
|
233
|
+
body,
|
|
234
|
+
intent.verb === "workspace.session.kill" && intent.fleetTarget
|
|
235
|
+
? undefined
|
|
236
|
+
: { "X-Tmux-Ide-Host-Client-Id": options.hostClientId },
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
if (
|
|
240
|
+
result.daemonInstanceId !== scope.generation ||
|
|
241
|
+
result.operationId !== operationId ||
|
|
242
|
+
result.verb !== intent.verb ||
|
|
243
|
+
result.workspaceName !== intent.workspaceName
|
|
244
|
+
)
|
|
245
|
+
throw new TmuxServerClientError("scope-mismatch");
|
|
246
|
+
return result;
|
|
247
|
+
},
|
|
248
|
+
async issuePaneStream(
|
|
249
|
+
requestId: string,
|
|
250
|
+
stream: PaneStreamLeaseRequest,
|
|
251
|
+
liveSessionId?: string,
|
|
252
|
+
) {
|
|
253
|
+
const body = PaneStreamIssueMutationRequestSchemaZ.parse({
|
|
254
|
+
requestId,
|
|
255
|
+
expectedDaemonInstanceId: scope.generation,
|
|
256
|
+
stream,
|
|
257
|
+
});
|
|
258
|
+
const result = PaneStreamIssueResultSchemaZ.parse(
|
|
259
|
+
await scopedRequest(
|
|
260
|
+
`pane-streams/issue${liveSessionId ? `?liveSessionId=${encodeURIComponent(liveSessionId)}` : ""}`,
|
|
261
|
+
body,
|
|
262
|
+
{
|
|
263
|
+
Origin: options.origin,
|
|
264
|
+
"X-Tmux-Ide-Request-Id": requestId,
|
|
265
|
+
"X-Tmux-Ide-Expected-Daemon-Instance-Id": scope.generation,
|
|
266
|
+
"X-Tmux-Ide-Host-Client-Id": options.hostClientId,
|
|
267
|
+
},
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
if (
|
|
271
|
+
result.status === "issued" &&
|
|
272
|
+
(result.descriptor.daemonInstanceId !== scope.generation ||
|
|
273
|
+
result.descriptor.requestId !== requestId ||
|
|
274
|
+
// The adopted default owner retains its original redemption address.
|
|
275
|
+
// Its exact generation and request are still proven by this bound issue response.
|
|
276
|
+
![tmuxServerPaneStreamPath(scope), PANE_STREAM_REDEEM_PATH].includes(
|
|
277
|
+
new URL(result.descriptor.webSocketUrl).pathname,
|
|
278
|
+
) ||
|
|
279
|
+
JSON.stringify([...result.descriptor.panes].sort()) !==
|
|
280
|
+
JSON.stringify([...stream.panes].sort()))
|
|
281
|
+
)
|
|
282
|
+
throw new TmuxServerClientError("scope-mismatch");
|
|
283
|
+
return result;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
export type TmuxServerClient = ReturnType<typeof createTmuxServerClient>;
|