@juspay/shooter 1.19.0 → 1.20.0
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/build/client/_app/immutable/assets/11.F10lvwyh.css +1 -0
- package/build/client/_app/immutable/assets/11.F10lvwyh.css.br +0 -0
- package/build/client/_app/immutable/assets/11.F10lvwyh.css.gz +0 -0
- package/build/client/_app/immutable/chunks/C_YNQL8b.js +3 -0
- package/build/client/_app/immutable/chunks/C_YNQL8b.js.br +0 -0
- package/build/client/_app/immutable/chunks/C_YNQL8b.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DA4Zt9Me.js → DIZ3Qst5.js} +1 -1
- package/build/client/_app/immutable/chunks/DIZ3Qst5.js.br +0 -0
- package/build/client/_app/immutable/chunks/{DA4Zt9Me.js.gz → DIZ3Qst5.js.gz} +0 -0
- package/build/client/_app/immutable/chunks/{DCDL_9ys.js → DT4H19pV.js} +1 -1
- package/build/client/_app/immutable/chunks/DT4H19pV.js.br +0 -0
- package/build/client/_app/immutable/chunks/DT4H19pV.js.gz +0 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js +6 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js.br +0 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.D4TXlu7A.js → app.Bd-DfeJi.js} +2 -2
- package/build/client/_app/immutable/entry/app.Bd-DfeJi.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Bd-DfeJi.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.evvp4tX7.js +1 -0
- package/build/client/_app/immutable/entry/start.evvp4tX7.js.br +2 -0
- package/build/client/_app/immutable/entry/start.evvp4tX7.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.1zylwAPT.js → 0.Bl-1LQWM.js} +1 -1
- package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.BVnLUSs-.js → 1.DT4dq6Ay.js} +1 -1
- package/build/client/_app/immutable/nodes/1.DT4dq6Ay.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.DT4dq6Ay.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{10.D1wl2wPX.js → 10.CF7RGXpe.js} +1 -1
- package/build/client/_app/immutable/nodes/10.CF7RGXpe.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.CF7RGXpe.js.gz +0 -0
- package/build/client/_app/immutable/nodes/11.BV_G7yLI.js +2 -0
- package/build/client/_app/immutable/nodes/11.BV_G7yLI.js.br +0 -0
- package/build/client/_app/immutable/nodes/11.BV_G7yLI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.D1Mm0DUX.js → 2.DcRhsjYp.js} +1 -1
- package/build/client/_app/immutable/nodes/2.DcRhsjYp.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DcRhsjYp.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.Wfz3TcJd.js → 3.0MMe3oxR.js} +1 -1
- package/build/client/_app/immutable/nodes/3.0MMe3oxR.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.0MMe3oxR.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.DtZAEPXb.js → 6.ComiWlV6.js} +1 -1
- package/build/client/_app/immutable/nodes/6.ComiWlV6.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.ComiWlV6.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.MfBRh32I.js → 7.vkPx1kVP.js} +1 -1
- package/build/client/_app/immutable/nodes/7.vkPx1kVP.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.vkPx1kVP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.DVE6LnOC.js → 8.Bmr3sWbS.js} +1 -1
- package/build/client/_app/immutable/nodes/8.Bmr3sWbS.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Bmr3sWbS.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.BCel5OqI.js → 9.CAJucyeI.js} +1 -1
- package/build/client/_app/immutable/nodes/9.CAJucyeI.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.CAJucyeI.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-DJqyZZTr.js → 0-DDGB6CRT.js} +2 -2
- package/build/server/chunks/{0-DJqyZZTr.js.map → 0-DDGB6CRT.js.map} +1 -1
- package/build/server/chunks/{1-2YUVen1F.js → 1-DEjonQXD.js} +2 -2
- package/build/server/chunks/{1-2YUVen1F.js.map → 1-DEjonQXD.js.map} +1 -1
- package/build/server/chunks/{10-D1X7LB3v.js → 10-BK1kiiiw.js} +2 -2
- package/build/server/chunks/{10-D1X7LB3v.js.map → 10-BK1kiiiw.js.map} +1 -1
- package/build/server/chunks/{11-qXSPdF5j.js → 11-CJPjkEF3.js} +4 -4
- package/build/server/chunks/11-CJPjkEF3.js.map +1 -0
- package/build/server/chunks/{2-BD7kj1mt.js → 2-RLnhlWh5.js} +2 -2
- package/build/server/chunks/{2-BD7kj1mt.js.map → 2-RLnhlWh5.js.map} +1 -1
- package/build/server/chunks/{3-oNjv-BhZ.js → 3-Dd4pJBqZ.js} +2 -2
- package/build/server/chunks/{3-oNjv-BhZ.js.map → 3-Dd4pJBqZ.js.map} +1 -1
- package/build/server/chunks/{6-DRJGUqHG.js → 6-DdRMnKNa.js} +2 -2
- package/build/server/chunks/{6-DRJGUqHG.js.map → 6-DdRMnKNa.js.map} +1 -1
- package/build/server/chunks/{7-_giJiu0L.js → 7-vLOMMetm.js} +2 -2
- package/build/server/chunks/{7-_giJiu0L.js.map → 7-vLOMMetm.js.map} +1 -1
- package/build/server/chunks/{8-zvWAVNT5.js → 8-rJyiQLFs.js} +2 -2
- package/build/server/chunks/{8-zvWAVNT5.js.map → 8-rJyiQLFs.js.map} +1 -1
- package/build/server/chunks/{9-DVyDL445.js → 9-CVSNNYED.js} +2 -2
- package/build/server/chunks/{9-DVyDL445.js.map → 9-CVSNNYED.js.map} +1 -1
- package/build/server/chunks/Banner-BgaAs1rs.js.map +1 -1
- package/build/server/chunks/Button-D0hZ7JYt.js.map +1 -1
- package/build/server/chunks/Icon-D0GBnDcs.js.map +1 -1
- package/build/server/chunks/Input-OmIiydSx.js.map +1 -1
- package/build/server/chunks/Pill-4xJ-VhAA.js.map +1 -1
- package/build/server/chunks/Shimmer-Dw2uvTC1.js.map +1 -1
- package/build/server/chunks/_error.svelte-CZnkxeLr.js.map +1 -1
- package/build/server/chunks/_page.svelte-BLo2v_8E.js.map +1 -1
- package/build/server/chunks/_page.svelte-BTlfUsBp.js.map +1 -1
- package/build/server/chunks/_page.svelte-BX2FMgSg.js.map +1 -1
- package/build/server/chunks/_page.svelte-C7B0qdrC.js.map +1 -1
- package/build/server/chunks/_page.svelte-CE7COWnF.js.map +1 -1
- package/build/server/chunks/_page.svelte-CWsjjd4l.js.map +1 -1
- package/build/server/chunks/_page.svelte-D5S2hkBk.js.map +1 -1
- package/build/server/chunks/_page.svelte-D_Ey8QRG.js.map +1 -1
- package/build/server/chunks/{_page.svelte-BUBLUSGo.js → _page.svelte-dabsQl9c.js} +206 -5
- package/build/server/chunks/_page.svelte-dabsQl9c.js.map +1 -0
- package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +1 -1
- package/build/server/chunks/{_server.ts-C_OOUqsd.js → _server.ts-AnBXfZXh.js} +2 -2
- package/build/server/chunks/{_server.ts-C_OOUqsd.js.map → _server.ts-AnBXfZXh.js.map} +1 -1
- package/build/server/chunks/_server.ts-B-evHL2q.js +13 -0
- package/build/server/chunks/_server.ts-B-evHL2q.js.map +1 -0
- package/build/server/chunks/_server.ts-B2wIgsW4.js +95 -0
- package/build/server/chunks/_server.ts-B2wIgsW4.js.map +1 -0
- package/build/server/chunks/{_server.ts-Bi0Oe4PF.js → _server.ts-CJGyN8mw.js} +14 -9
- package/build/server/chunks/_server.ts-CJGyN8mw.js.map +1 -0
- package/build/server/chunks/{_server.ts-DhJx0DLr.js → _server.ts-DEx9-epI.js} +16 -7
- package/build/server/chunks/_server.ts-DEx9-epI.js.map +1 -0
- package/build/server/chunks/{_server.ts-DxT9IlZF.js → _server.ts-DKNIsQeH.js} +3 -3
- package/build/server/chunks/{_server.ts-DxT9IlZF.js.map → _server.ts-DKNIsQeH.js.map} +1 -1
- package/build/server/chunks/_server.ts-DpRr0Tfh.js +68 -0
- package/build/server/chunks/_server.ts-DpRr0Tfh.js.map +1 -0
- package/build/server/chunks/{_server.ts-CRVNEOd2.js → _server.ts-Dz9Jd9Jh.js} +3 -3
- package/build/server/chunks/{_server.ts-CRVNEOd2.js.map → _server.ts-Dz9Jd9Jh.js.map} +1 -1
- package/build/server/chunks/{_server.ts-Bjbr7glm.js → _server.ts-QN-Bo5ql.js} +12 -5
- package/build/server/chunks/_server.ts-QN-Bo5ql.js.map +1 -0
- package/build/server/chunks/{_server.ts-BrqaMMAa.js → _server.ts-W6i3EnGX.js} +29 -6
- package/build/server/chunks/_server.ts-W6i3EnGX.js.map +1 -0
- package/build/server/chunks/{_server.ts-DMm0hBP4.js → _server.ts-bk_EeAdY.js} +2 -2
- package/build/server/chunks/{_server.ts-DMm0hBP4.js.map → _server.ts-bk_EeAdY.js.map} +1 -1
- package/build/server/chunks/cache-BlMaDsHi.js.map +1 -1
- package/build/server/chunks/guest-registry-t0-7Zv5q.js +39 -0
- package/build/server/chunks/guest-registry-t0-7Zv5q.js.map +1 -0
- package/build/server/chunks/index-CoYB03g7.js.map +1 -1
- package/build/server/chunks/index2-dSGQ9Eaa.js.map +1 -1
- package/build/server/chunks/{pty-manager-41h3IK8K.js → pty-manager-CkZNoW1t.js} +6 -2
- package/build/server/chunks/pty-manager-CkZNoW1t.js.map +1 -0
- package/build/server/chunks/root-D4IoFC8F.js.map +1 -1
- package/build/server/chunks/share-auth-BS7JuiHf.js +27 -0
- package/build/server/chunks/share-auth-BS7JuiHf.js.map +1 -0
- package/build/server/chunks/share-store-B9jMpVg0.js +127 -0
- package/build/server/chunks/share-store-B9jMpVg0.js.map +1 -0
- package/build/server/chunks/state.svelte-CmHqngc_.js.map +1 -1
- package/build/server/chunks/stores-CRYxfF0o.js.map +1 -1
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +40 -19
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/server.ts +8 -3
- package/src/lib/modules/client/terminal/ShareGate.svelte +96 -0
- package/src/lib/modules/client/terminal/ShareSheet.svelte +395 -0
- package/src/lib/modules/client/terminal/xterm-wrapper.ts +19 -2
- package/src/lib/modules/server/terminal/pty-manager.ts +6 -0
- package/src/lib/modules/server/terminal/share-auth.ts +37 -0
- package/src/lib/modules/server/terminal/share-store.ts +172 -0
- package/src/lib/modules/server/ws/guest-registry.ts +49 -0
- package/src/lib/modules/server/ws/server.ts +22 -3
- package/src/lib/modules/server/ws/session-handler.ts +18 -4
- package/src/lib/modules/server/ws/terminal-handler.ts +21 -2
- package/src/lib/modules/server/ws/ticket-store.ts +18 -10
- package/src/lib/types/generated/Client.ts +25 -1
- package/src/lib/types/generated/Share.ts +404 -0
- package/src/lib/types/generated/WsProtocol.ts +73 -2
- package/src/lib/types/generated/index.ts +1 -0
- package/src/lib/types/terminal-client.ts +19 -2
- package/src/lib/types/ws.ts +1 -0
- package/src/routes/api/terminals/[id]/+server.ts +14 -3
- package/src/routes/api/terminals/[id]/paste-image/+server.ts +8 -4
- package/src/routes/api/terminals/[id]/resize/+server.ts +8 -4
- package/src/routes/api/terminals/[id]/share/+server.ts +98 -0
- package/src/routes/api/terminals/[id]/share/auth/+server.ts +81 -0
- package/src/routes/api/terminals/[id]/share/status/+server.ts +11 -0
- package/src/routes/api/ws-ticket/+server.ts +26 -5
- package/src/routes/terminals/[id]/+page.svelte +184 -43
- package/build/client/_app/immutable/assets/11.v5KA95xm.css +0 -1
- package/build/client/_app/immutable/assets/11.v5KA95xm.css.br +0 -0
- package/build/client/_app/immutable/assets/11.v5KA95xm.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BcqA7eKM.js +0 -3
- package/build/client/_app/immutable/chunks/BcqA7eKM.js.br +0 -0
- package/build/client/_app/immutable/chunks/BcqA7eKM.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CR6bkGJW.js +0 -6
- package/build/client/_app/immutable/chunks/CR6bkGJW.js.br +0 -0
- package/build/client/_app/immutable/chunks/CR6bkGJW.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DA4Zt9Me.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCDL_9ys.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCDL_9ys.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.D4TXlu7A.js.br +0 -0
- package/build/client/_app/immutable/entry/app.D4TXlu7A.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BBQhtURO.js +0 -1
- package/build/client/_app/immutable/entry/start.BBQhtURO.js.br +0 -0
- package/build/client/_app/immutable/entry/start.BBQhtURO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.1zylwAPT.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.1zylwAPT.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.BVnLUSs-.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.BVnLUSs-.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.D1wl2wPX.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.D1wl2wPX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/11.C18nMGmp.js +0 -2
- package/build/client/_app/immutable/nodes/11.C18nMGmp.js.br +0 -0
- package/build/client/_app/immutable/nodes/11.C18nMGmp.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.Wfz3TcJd.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Wfz3TcJd.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.DtZAEPXb.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.DtZAEPXb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.MfBRh32I.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.MfBRh32I.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.DVE6LnOC.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.DVE6LnOC.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.BCel5OqI.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.BCel5OqI.js.gz +0 -0
- package/build/server/chunks/11-qXSPdF5j.js.map +0 -1
- package/build/server/chunks/_page.svelte-BUBLUSGo.js.map +0 -1
- package/build/server/chunks/_server.ts-Bi0Oe4PF.js.map +0 -1
- package/build/server/chunks/_server.ts-Bjbr7glm.js.map +0 -1
- package/build/server/chunks/_server.ts-BrqaMMAa.js.map +0 -1
- package/build/server/chunks/_server.ts-DhJx0DLr.js.map +0 -1
- package/build/server/chunks/events-handler-Dm1mNPQP.js +0 -20
- package/build/server/chunks/events-handler-Dm1mNPQP.js.map +0 -1
- package/build/server/chunks/pty-manager-41h3IK8K.js.map +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Tracks WebSocket connections opened with a guest (scoped) ticket, per terminal,
|
|
2
|
+
// so revoke / mode-change / password-change can force-close them immediately.
|
|
3
|
+
// globalThis bridges the tsx server.ts and SvelteKit handler module scopes.
|
|
4
|
+
|
|
5
|
+
import type { WebSocket } from 'ws';
|
|
6
|
+
|
|
7
|
+
const GUESTS_KEY = '__shooter_ws_guest_conns';
|
|
8
|
+
const guests: Map<string, Set<WebSocket>> = ((globalThis as Record<string, unknown>)[
|
|
9
|
+
GUESTS_KEY
|
|
10
|
+
] as Map<string, Set<WebSocket>>) || new Map<string, Set<WebSocket>>();
|
|
11
|
+
(globalThis as Record<string, unknown>)[GUESTS_KEY] = guests;
|
|
12
|
+
|
|
13
|
+
/** Force-close every guest connection for a terminal. Returns the number closed. */
|
|
14
|
+
export function closeGuests(terminalId: string): number {
|
|
15
|
+
const set = guests.get(terminalId);
|
|
16
|
+
if (!set) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
let closed = 0;
|
|
20
|
+
for (const ws of set) {
|
|
21
|
+
try {
|
|
22
|
+
ws.close(4001, 'Share revoked');
|
|
23
|
+
closed++;
|
|
24
|
+
} catch {
|
|
25
|
+
// Already closing/closed.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
guests.delete(terminalId);
|
|
29
|
+
return closed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Register a guest connection; auto-removes itself on close. */
|
|
33
|
+
export function registerGuest(terminalId: string, ws: WebSocket): void {
|
|
34
|
+
let set = guests.get(terminalId);
|
|
35
|
+
if (!set) {
|
|
36
|
+
set = new Set<WebSocket>();
|
|
37
|
+
guests.set(terminalId, set);
|
|
38
|
+
}
|
|
39
|
+
set.add(ws);
|
|
40
|
+
ws.on('close', () => {
|
|
41
|
+
const current = guests.get(terminalId);
|
|
42
|
+
if (current) {
|
|
43
|
+
current.delete(ws);
|
|
44
|
+
if (current.size === 0) {
|
|
45
|
+
guests.delete(terminalId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// /ws/session/:id -> Live structured session stream
|
|
6
6
|
// /ws/events -> Global event bus (broadcasts)
|
|
7
7
|
|
|
8
|
+
import type { TicketScope } from '$lib/types';
|
|
8
9
|
import type { IncomingMessage } from 'http';
|
|
9
10
|
import type { Duplex } from 'stream';
|
|
10
11
|
import type { WebSocket, WebSocketServer } from 'ws';
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
getEventsClientCount,
|
|
15
16
|
handleEventsConnection,
|
|
16
17
|
} from './events-handler.js';
|
|
18
|
+
import { registerGuest } from './guest-registry.js';
|
|
17
19
|
import { handleSessionConnection } from './session-handler.js';
|
|
18
20
|
import { handleSuperSessionConnection } from './super-session-handler.js';
|
|
19
21
|
import { handleTerminalConnection } from './terminal-handler.js';
|
|
@@ -49,7 +51,8 @@ export function setupWebSocketHandlers(
|
|
|
49
51
|
wss: WebSocketServer,
|
|
50
52
|
request: IncomingMessage,
|
|
51
53
|
socket: Duplex,
|
|
52
|
-
head: Buffer
|
|
54
|
+
head: Buffer,
|
|
55
|
+
scope?: TicketScope
|
|
53
56
|
): void {
|
|
54
57
|
const host = request.headers.host ?? 'localhost';
|
|
55
58
|
let pathname: string;
|
|
@@ -71,9 +74,25 @@ export function setupWebSocketHandlers(
|
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
// Scoped (guest) tickets may only open the terminal/session channels of
|
|
78
|
+
// their own terminal. Events and super-session channels broadcast global
|
|
79
|
+
// data, so they are denied outright.
|
|
80
|
+
if (scope) {
|
|
81
|
+
const target = terminalMatch?.[1] ?? sessionMatch?.[1];
|
|
82
|
+
if (!target || superSessionMatch || target !== scope.terminalId) {
|
|
83
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
84
|
+
socket.destroy();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {
|
|
75
90
|
allConnections.add(ws);
|
|
76
91
|
|
|
92
|
+
if (scope) {
|
|
93
|
+
registerGuest(scope.terminalId, ws);
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
ws.on('close', () => {
|
|
78
97
|
allConnections.delete(ws);
|
|
79
98
|
});
|
|
@@ -84,13 +103,13 @@ export function setupWebSocketHandlers(
|
|
|
84
103
|
|
|
85
104
|
if (terminalMatch) {
|
|
86
105
|
const terminalId = terminalMatch[1];
|
|
87
|
-
handleTerminalConnection(ws, terminalId);
|
|
106
|
+
handleTerminalConnection(ws, terminalId, scope);
|
|
88
107
|
} else if (superSessionMatch) {
|
|
89
108
|
const superSessionId = superSessionMatch[1];
|
|
90
109
|
handleSuperSessionConnection(ws, superSessionId);
|
|
91
110
|
} else if (sessionMatch) {
|
|
92
111
|
const sessionId = sessionMatch[1];
|
|
93
|
-
handleSessionConnection(ws, sessionId);
|
|
112
|
+
handleSessionConnection(ws, sessionId, scope);
|
|
94
113
|
} else if (isEvents) {
|
|
95
114
|
handleEventsConnection(ws);
|
|
96
115
|
}
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
WireSessionServerMessage as ServerMessage,
|
|
17
17
|
SessionWatcherLike,
|
|
18
18
|
TextContentBlock,
|
|
19
|
+
TicketScope,
|
|
19
20
|
} from '$lib/types';
|
|
20
21
|
import type { WebSocket } from 'ws';
|
|
21
22
|
|
|
@@ -41,7 +42,7 @@ let _sessionWatcher: null | SessionWatcherLike = null;
|
|
|
41
42
|
* 3. If still no terminal, treat as an external session — find the JSONL
|
|
42
43
|
* file directly and stream it via the session watcher.
|
|
43
44
|
*/
|
|
44
|
-
export function handleSessionConnection(ws: WebSocket, id: string): void {
|
|
45
|
+
export function handleSessionConnection(ws: WebSocket, id: string, scope?: TicketScope): void {
|
|
45
46
|
const state: ConnectionState = {
|
|
46
47
|
isExternalSession: false,
|
|
47
48
|
retryInterval: null,
|
|
@@ -66,7 +67,7 @@ export function handleSessionConnection(ws: WebSocket, id: string): void {
|
|
|
66
67
|
if (terminal) {
|
|
67
68
|
state.terminalId = terminal.id;
|
|
68
69
|
subscribeToSession(ws, state, terminal);
|
|
69
|
-
wireClientMessages(ws, state);
|
|
70
|
+
wireClientMessages(ws, state, scope);
|
|
70
71
|
wireCleanup(ws, state);
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
@@ -76,7 +77,7 @@ export function handleSessionConnection(ws: WebSocket, id: string): void {
|
|
|
76
77
|
if (jsonlPath) {
|
|
77
78
|
state.isExternalSession = true;
|
|
78
79
|
subscribeToExternalSession(ws, state, jsonlPath);
|
|
79
|
-
wireClientMessages(ws, state);
|
|
80
|
+
wireClientMessages(ws, state, scope);
|
|
80
81
|
wireCleanup(ws, state);
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
@@ -507,7 +508,7 @@ function wireCleanup(ws: WebSocket, state: ConnectionState): void {
|
|
|
507
508
|
* Extracted so both terminal-backed and external sessions share the same
|
|
508
509
|
* message loop.
|
|
509
510
|
*/
|
|
510
|
-
function wireClientMessages(ws: WebSocket, state: ConnectionState): void {
|
|
511
|
+
function wireClientMessages(ws: WebSocket, state: ConnectionState, scope?: TicketScope): void {
|
|
511
512
|
ws.on('message', (raw: Buffer | string) => {
|
|
512
513
|
const data = typeof raw === 'string' ? raw : raw.toString('utf-8');
|
|
513
514
|
const msg = parseClientMessage(data);
|
|
@@ -518,6 +519,10 @@ function wireClientMessages(ws: WebSocket, state: ConnectionState): void {
|
|
|
518
519
|
try {
|
|
519
520
|
switch (msg.type) {
|
|
520
521
|
case 'cancel': {
|
|
522
|
+
if (scope?.readOnly) {
|
|
523
|
+
safeSend(ws, { message: 'This shared terminal is view-only.', type: 'error' });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
521
526
|
if (state.isExternalSession) {
|
|
522
527
|
safeSend(ws, {
|
|
523
528
|
message: 'Cannot cancel — this is a read-only session. Connect to a terminal first.',
|
|
@@ -535,6 +540,10 @@ function wireClientMessages(ws: WebSocket, state: ConnectionState): void {
|
|
|
535
540
|
}
|
|
536
541
|
|
|
537
542
|
case 'send-input': {
|
|
543
|
+
if (scope?.readOnly) {
|
|
544
|
+
safeSend(ws, { message: 'This shared terminal is view-only.', type: 'error' });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
538
547
|
if (state.isExternalSession) {
|
|
539
548
|
safeSend(ws, {
|
|
540
549
|
message:
|
|
@@ -557,6 +566,11 @@ function wireClientMessages(ws: WebSocket, state: ConnectionState): void {
|
|
|
557
566
|
}
|
|
558
567
|
|
|
559
568
|
case 'subscribe': {
|
|
569
|
+
// Scoped guests may only (re)subscribe to their own terminal's session.
|
|
570
|
+
if (scope && msg.sessionId !== scope.terminalId) {
|
|
571
|
+
safeSend(ws, { message: 'Not authorized for this session.', type: 'error' });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
560
574
|
// (Re)subscribe to a different session. Clean up the old subscription.
|
|
561
575
|
if (state.retryInterval) {
|
|
562
576
|
clearInterval(state.retryInterval);
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TerminalPtyManagerLike as PtyManagerLike,
|
|
8
8
|
WireTerminalServerMessage as ServerMessage,
|
|
9
9
|
TerminalSignal,
|
|
10
|
+
TicketScope,
|
|
10
11
|
} from '$lib/types';
|
|
11
12
|
import type { WebSocket } from 'ws';
|
|
12
13
|
|
|
@@ -28,7 +29,11 @@ let _ptyManager: null | PtyManagerLike = null;
|
|
|
28
29
|
* Attaches the client to the terminal's viewer set, replays scrollback,
|
|
29
30
|
* and relays PTY I/O bidirectionally.
|
|
30
31
|
*/
|
|
31
|
-
export function handleTerminalConnection(
|
|
32
|
+
export function handleTerminalConnection(
|
|
33
|
+
ws: WebSocket,
|
|
34
|
+
terminalId: string,
|
|
35
|
+
scope?: TicketScope
|
|
36
|
+
): void {
|
|
32
37
|
// ── 1. Look up the terminal ──────────────────────────────────────
|
|
33
38
|
if (!_ptyManager) {
|
|
34
39
|
safeSend(ws, { message: 'PTY manager not initialised', type: 'error' });
|
|
@@ -62,6 +67,11 @@ export function handleTerminalConnection(ws: WebSocket, terminalId: string): voi
|
|
|
62
67
|
return; // Silently ignore malformed messages.
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
// View-only guests: every inbound frame type mutates the PTY — drop them all.
|
|
71
|
+
if (scope?.readOnly) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
// Don't allow input to exited terminals.
|
|
66
76
|
if (terminal.status === 'exited') {
|
|
67
77
|
safeSend(ws, { message: 'Terminal has exited', type: 'error' });
|
|
@@ -74,9 +84,18 @@ export function handleTerminalConnection(ws: WebSocket, terminalId: string): voi
|
|
|
74
84
|
terminal.pty.write(msg.data);
|
|
75
85
|
break;
|
|
76
86
|
|
|
77
|
-
case 'resize':
|
|
87
|
+
case 'resize': {
|
|
78
88
|
terminal.pty.resize(msg.cols, msg.rows);
|
|
89
|
+
// Broadcast the new PTY size to the other attached clients so
|
|
90
|
+
// view-only guests can follow the owner's terminal dimensions.
|
|
91
|
+
const resizeMsg: ServerMessage = { cols: msg.cols, rows: msg.rows, type: 'resize' };
|
|
92
|
+
for (const client of terminal.clients) {
|
|
93
|
+
if (client !== ws) {
|
|
94
|
+
safeSend(client, resizeMsg);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
79
97
|
break;
|
|
98
|
+
}
|
|
80
99
|
|
|
81
100
|
case 'signal': {
|
|
82
101
|
if (msg.signal === 'SIGINT') {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// This avoids putting the long-lived API_KEY in WebSocket URL query parameters,
|
|
6
6
|
// which would appear in proxy logs, Cloudflare access logs, and browser history.
|
|
7
7
|
|
|
8
|
-
import type { Ticket } from '$lib/types';
|
|
8
|
+
import type { Ticket, TicketScope } from '$lib/types';
|
|
9
9
|
|
|
10
10
|
import { randomBytes } from 'crypto';
|
|
11
11
|
|
|
@@ -21,32 +21,40 @@ const tickets: Map<string, Ticket> =
|
|
|
21
21
|
/**
|
|
22
22
|
* Generate a new single-use ticket (32-byte hex string).
|
|
23
23
|
* The ticket is valid for 30 seconds and can only be consumed once.
|
|
24
|
+
* An optional scope restricts the ticket to one terminal's channels
|
|
25
|
+
* (and optionally to read-only access).
|
|
24
26
|
*/
|
|
25
|
-
export function generateTicket(): string {
|
|
27
|
+
export function generateTicket(scope?: TicketScope): string {
|
|
26
28
|
const ticket = randomBytes(32).toString('hex');
|
|
27
|
-
tickets.set(ticket, {
|
|
29
|
+
tickets.set(ticket, {
|
|
30
|
+
createdAt: Date.now(),
|
|
31
|
+
readOnly: scope?.readOnly ?? null,
|
|
32
|
+
terminalId: scope?.terminalId ?? null,
|
|
33
|
+
used: false,
|
|
34
|
+
});
|
|
28
35
|
return ticket;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/**
|
|
32
39
|
* Validate and consume a ticket.
|
|
33
|
-
* Returns
|
|
34
|
-
* A valid ticket is marked as used
|
|
40
|
+
* Returns the consumed Ticket (including any scope) if it is valid, not yet
|
|
41
|
+
* used, and not expired; otherwise null. A valid ticket is marked as used
|
|
42
|
+
* (single-use) and cannot be reused.
|
|
35
43
|
*/
|
|
36
|
-
export function validateTicket(ticket: null | string):
|
|
44
|
+
export function validateTicket(ticket: null | string): null | Ticket {
|
|
37
45
|
if (!ticket) {
|
|
38
|
-
return
|
|
46
|
+
return null;
|
|
39
47
|
}
|
|
40
48
|
const entry = tickets.get(ticket);
|
|
41
49
|
if (!entry || entry.used) {
|
|
42
|
-
return
|
|
50
|
+
return null;
|
|
43
51
|
}
|
|
44
52
|
if (Date.now() - entry.createdAt > 30_000) {
|
|
45
53
|
tickets.delete(ticket);
|
|
46
|
-
return
|
|
54
|
+
return null;
|
|
47
55
|
}
|
|
48
56
|
entry.used = true;
|
|
49
|
-
return
|
|
57
|
+
return entry;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
// Cleanup expired tickets every 30 seconds (matches ticket lifetime).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SessionSource, decodeSessionSource } from './index';
|
|
1
|
+
import { type ShareMode, decodeShareMode, type SessionSource, decodeSessionSource } from './index';
|
|
2
2
|
import {
|
|
3
3
|
isJSON,
|
|
4
4
|
decodeString,
|
|
@@ -307,6 +307,24 @@ export type TerminalDetailView = {
|
|
|
307
307
|
* @memberof TerminalDetailView
|
|
308
308
|
*/
|
|
309
309
|
sessionWs: string;
|
|
310
|
+
/**
|
|
311
|
+
* @description Current PTY width in columns (for fixed-size guest rendering)
|
|
312
|
+
* @type { number }
|
|
313
|
+
* @memberof TerminalDetailView
|
|
314
|
+
*/
|
|
315
|
+
cols: number | null;
|
|
316
|
+
/**
|
|
317
|
+
* @description Current PTY height in rows (for fixed-size guest rendering)
|
|
318
|
+
* @type { number }
|
|
319
|
+
* @memberof TerminalDetailView
|
|
320
|
+
*/
|
|
321
|
+
rows: number | null;
|
|
322
|
+
/**
|
|
323
|
+
* @description Present when fetched with a guest share token — the guest's access mode
|
|
324
|
+
* @type { ShareMode }
|
|
325
|
+
* @memberof TerminalDetailView
|
|
326
|
+
*/
|
|
327
|
+
shareMode: ShareMode | null;
|
|
310
328
|
};
|
|
311
329
|
|
|
312
330
|
export function decodeTerminalDetailView(rawInput: unknown): TerminalDetailView | null {
|
|
@@ -325,6 +343,9 @@ export function decodeTerminalDetailView(rawInput: unknown): TerminalDetailView
|
|
|
325
343
|
const decodedTimestamp = decodeString(rawInput['timestamp']);
|
|
326
344
|
const decodedWs = decodeString(rawInput['ws']);
|
|
327
345
|
const decodedSessionWs = decodeString(rawInput['sessionWs']);
|
|
346
|
+
const decodedCols = decodeNumber(rawInput['cols']);
|
|
347
|
+
const decodedRows = decodeNumber(rawInput['rows']);
|
|
348
|
+
const decodedShareMode = decodeShareMode(rawInput['shareMode']);
|
|
328
349
|
|
|
329
350
|
if (
|
|
330
351
|
decodedId === null ||
|
|
@@ -355,6 +376,9 @@ export function decodeTerminalDetailView(rawInput: unknown): TerminalDetailView
|
|
|
355
376
|
timestamp: decodedTimestamp,
|
|
356
377
|
ws: decodedWs,
|
|
357
378
|
sessionWs: decodedSessionWs,
|
|
379
|
+
cols: decodedCols,
|
|
380
|
+
rows: decodedRows,
|
|
381
|
+
shareMode: decodedShareMode,
|
|
358
382
|
};
|
|
359
383
|
}
|
|
360
384
|
return null;
|