@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.
Files changed (206) hide show
  1. package/build/client/_app/immutable/assets/11.F10lvwyh.css +1 -0
  2. package/build/client/_app/immutable/assets/11.F10lvwyh.css.br +0 -0
  3. package/build/client/_app/immutable/assets/11.F10lvwyh.css.gz +0 -0
  4. package/build/client/_app/immutable/chunks/C_YNQL8b.js +3 -0
  5. package/build/client/_app/immutable/chunks/C_YNQL8b.js.br +0 -0
  6. package/build/client/_app/immutable/chunks/C_YNQL8b.js.gz +0 -0
  7. package/build/client/_app/immutable/chunks/{DA4Zt9Me.js → DIZ3Qst5.js} +1 -1
  8. package/build/client/_app/immutable/chunks/DIZ3Qst5.js.br +0 -0
  9. package/build/client/_app/immutable/chunks/{DA4Zt9Me.js.gz → DIZ3Qst5.js.gz} +0 -0
  10. package/build/client/_app/immutable/chunks/{DCDL_9ys.js → DT4H19pV.js} +1 -1
  11. package/build/client/_app/immutable/chunks/DT4H19pV.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/DT4H19pV.js.gz +0 -0
  13. package/build/client/_app/immutable/chunks/J5-Cr5oR.js +6 -0
  14. package/build/client/_app/immutable/chunks/J5-Cr5oR.js.br +0 -0
  15. package/build/client/_app/immutable/chunks/J5-Cr5oR.js.gz +0 -0
  16. package/build/client/_app/immutable/entry/{app.D4TXlu7A.js → app.Bd-DfeJi.js} +2 -2
  17. package/build/client/_app/immutable/entry/app.Bd-DfeJi.js.br +0 -0
  18. package/build/client/_app/immutable/entry/app.Bd-DfeJi.js.gz +0 -0
  19. package/build/client/_app/immutable/entry/start.evvp4tX7.js +1 -0
  20. package/build/client/_app/immutable/entry/start.evvp4tX7.js.br +2 -0
  21. package/build/client/_app/immutable/entry/start.evvp4tX7.js.gz +0 -0
  22. package/build/client/_app/immutable/nodes/{0.1zylwAPT.js → 0.Bl-1LQWM.js} +1 -1
  23. package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js.br +0 -0
  24. package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js.gz +0 -0
  25. package/build/client/_app/immutable/nodes/{1.BVnLUSs-.js → 1.DT4dq6Ay.js} +1 -1
  26. package/build/client/_app/immutable/nodes/1.DT4dq6Ay.js.br +0 -0
  27. package/build/client/_app/immutable/nodes/1.DT4dq6Ay.js.gz +0 -0
  28. package/build/client/_app/immutable/nodes/{10.D1wl2wPX.js → 10.CF7RGXpe.js} +1 -1
  29. package/build/client/_app/immutable/nodes/10.CF7RGXpe.js.br +0 -0
  30. package/build/client/_app/immutable/nodes/10.CF7RGXpe.js.gz +0 -0
  31. package/build/client/_app/immutable/nodes/11.BV_G7yLI.js +2 -0
  32. package/build/client/_app/immutable/nodes/11.BV_G7yLI.js.br +0 -0
  33. package/build/client/_app/immutable/nodes/11.BV_G7yLI.js.gz +0 -0
  34. package/build/client/_app/immutable/nodes/{2.D1Mm0DUX.js → 2.DcRhsjYp.js} +1 -1
  35. package/build/client/_app/immutable/nodes/2.DcRhsjYp.js.br +0 -0
  36. package/build/client/_app/immutable/nodes/2.DcRhsjYp.js.gz +0 -0
  37. package/build/client/_app/immutable/nodes/{3.Wfz3TcJd.js → 3.0MMe3oxR.js} +1 -1
  38. package/build/client/_app/immutable/nodes/3.0MMe3oxR.js.br +0 -0
  39. package/build/client/_app/immutable/nodes/3.0MMe3oxR.js.gz +0 -0
  40. package/build/client/_app/immutable/nodes/{6.DtZAEPXb.js → 6.ComiWlV6.js} +1 -1
  41. package/build/client/_app/immutable/nodes/6.ComiWlV6.js.br +0 -0
  42. package/build/client/_app/immutable/nodes/6.ComiWlV6.js.gz +0 -0
  43. package/build/client/_app/immutable/nodes/{7.MfBRh32I.js → 7.vkPx1kVP.js} +1 -1
  44. package/build/client/_app/immutable/nodes/7.vkPx1kVP.js.br +0 -0
  45. package/build/client/_app/immutable/nodes/7.vkPx1kVP.js.gz +0 -0
  46. package/build/client/_app/immutable/nodes/{8.DVE6LnOC.js → 8.Bmr3sWbS.js} +1 -1
  47. package/build/client/_app/immutable/nodes/8.Bmr3sWbS.js.br +0 -0
  48. package/build/client/_app/immutable/nodes/8.Bmr3sWbS.js.gz +0 -0
  49. package/build/client/_app/immutable/nodes/{9.BCel5OqI.js → 9.CAJucyeI.js} +1 -1
  50. package/build/client/_app/immutable/nodes/9.CAJucyeI.js.br +0 -0
  51. package/build/client/_app/immutable/nodes/9.CAJucyeI.js.gz +0 -0
  52. package/build/client/_app/version.json +1 -1
  53. package/build/client/_app/version.json.br +0 -0
  54. package/build/client/_app/version.json.gz +0 -0
  55. package/build/server/chunks/{0-DJqyZZTr.js → 0-DDGB6CRT.js} +2 -2
  56. package/build/server/chunks/{0-DJqyZZTr.js.map → 0-DDGB6CRT.js.map} +1 -1
  57. package/build/server/chunks/{1-2YUVen1F.js → 1-DEjonQXD.js} +2 -2
  58. package/build/server/chunks/{1-2YUVen1F.js.map → 1-DEjonQXD.js.map} +1 -1
  59. package/build/server/chunks/{10-D1X7LB3v.js → 10-BK1kiiiw.js} +2 -2
  60. package/build/server/chunks/{10-D1X7LB3v.js.map → 10-BK1kiiiw.js.map} +1 -1
  61. package/build/server/chunks/{11-qXSPdF5j.js → 11-CJPjkEF3.js} +4 -4
  62. package/build/server/chunks/11-CJPjkEF3.js.map +1 -0
  63. package/build/server/chunks/{2-BD7kj1mt.js → 2-RLnhlWh5.js} +2 -2
  64. package/build/server/chunks/{2-BD7kj1mt.js.map → 2-RLnhlWh5.js.map} +1 -1
  65. package/build/server/chunks/{3-oNjv-BhZ.js → 3-Dd4pJBqZ.js} +2 -2
  66. package/build/server/chunks/{3-oNjv-BhZ.js.map → 3-Dd4pJBqZ.js.map} +1 -1
  67. package/build/server/chunks/{6-DRJGUqHG.js → 6-DdRMnKNa.js} +2 -2
  68. package/build/server/chunks/{6-DRJGUqHG.js.map → 6-DdRMnKNa.js.map} +1 -1
  69. package/build/server/chunks/{7-_giJiu0L.js → 7-vLOMMetm.js} +2 -2
  70. package/build/server/chunks/{7-_giJiu0L.js.map → 7-vLOMMetm.js.map} +1 -1
  71. package/build/server/chunks/{8-zvWAVNT5.js → 8-rJyiQLFs.js} +2 -2
  72. package/build/server/chunks/{8-zvWAVNT5.js.map → 8-rJyiQLFs.js.map} +1 -1
  73. package/build/server/chunks/{9-DVyDL445.js → 9-CVSNNYED.js} +2 -2
  74. package/build/server/chunks/{9-DVyDL445.js.map → 9-CVSNNYED.js.map} +1 -1
  75. package/build/server/chunks/Banner-BgaAs1rs.js.map +1 -1
  76. package/build/server/chunks/Button-D0hZ7JYt.js.map +1 -1
  77. package/build/server/chunks/Icon-D0GBnDcs.js.map +1 -1
  78. package/build/server/chunks/Input-OmIiydSx.js.map +1 -1
  79. package/build/server/chunks/Pill-4xJ-VhAA.js.map +1 -1
  80. package/build/server/chunks/Shimmer-Dw2uvTC1.js.map +1 -1
  81. package/build/server/chunks/_error.svelte-CZnkxeLr.js.map +1 -1
  82. package/build/server/chunks/_page.svelte-BLo2v_8E.js.map +1 -1
  83. package/build/server/chunks/_page.svelte-BTlfUsBp.js.map +1 -1
  84. package/build/server/chunks/_page.svelte-BX2FMgSg.js.map +1 -1
  85. package/build/server/chunks/_page.svelte-C7B0qdrC.js.map +1 -1
  86. package/build/server/chunks/_page.svelte-CE7COWnF.js.map +1 -1
  87. package/build/server/chunks/_page.svelte-CWsjjd4l.js.map +1 -1
  88. package/build/server/chunks/_page.svelte-D5S2hkBk.js.map +1 -1
  89. package/build/server/chunks/_page.svelte-D_Ey8QRG.js.map +1 -1
  90. package/build/server/chunks/{_page.svelte-BUBLUSGo.js → _page.svelte-dabsQl9c.js} +206 -5
  91. package/build/server/chunks/_page.svelte-dabsQl9c.js.map +1 -0
  92. package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +1 -1
  93. package/build/server/chunks/{_server.ts-C_OOUqsd.js → _server.ts-AnBXfZXh.js} +2 -2
  94. package/build/server/chunks/{_server.ts-C_OOUqsd.js.map → _server.ts-AnBXfZXh.js.map} +1 -1
  95. package/build/server/chunks/_server.ts-B-evHL2q.js +13 -0
  96. package/build/server/chunks/_server.ts-B-evHL2q.js.map +1 -0
  97. package/build/server/chunks/_server.ts-B2wIgsW4.js +95 -0
  98. package/build/server/chunks/_server.ts-B2wIgsW4.js.map +1 -0
  99. package/build/server/chunks/{_server.ts-Bi0Oe4PF.js → _server.ts-CJGyN8mw.js} +14 -9
  100. package/build/server/chunks/_server.ts-CJGyN8mw.js.map +1 -0
  101. package/build/server/chunks/{_server.ts-DhJx0DLr.js → _server.ts-DEx9-epI.js} +16 -7
  102. package/build/server/chunks/_server.ts-DEx9-epI.js.map +1 -0
  103. package/build/server/chunks/{_server.ts-DxT9IlZF.js → _server.ts-DKNIsQeH.js} +3 -3
  104. package/build/server/chunks/{_server.ts-DxT9IlZF.js.map → _server.ts-DKNIsQeH.js.map} +1 -1
  105. package/build/server/chunks/_server.ts-DpRr0Tfh.js +68 -0
  106. package/build/server/chunks/_server.ts-DpRr0Tfh.js.map +1 -0
  107. package/build/server/chunks/{_server.ts-CRVNEOd2.js → _server.ts-Dz9Jd9Jh.js} +3 -3
  108. package/build/server/chunks/{_server.ts-CRVNEOd2.js.map → _server.ts-Dz9Jd9Jh.js.map} +1 -1
  109. package/build/server/chunks/{_server.ts-Bjbr7glm.js → _server.ts-QN-Bo5ql.js} +12 -5
  110. package/build/server/chunks/_server.ts-QN-Bo5ql.js.map +1 -0
  111. package/build/server/chunks/{_server.ts-BrqaMMAa.js → _server.ts-W6i3EnGX.js} +29 -6
  112. package/build/server/chunks/_server.ts-W6i3EnGX.js.map +1 -0
  113. package/build/server/chunks/{_server.ts-DMm0hBP4.js → _server.ts-bk_EeAdY.js} +2 -2
  114. package/build/server/chunks/{_server.ts-DMm0hBP4.js.map → _server.ts-bk_EeAdY.js.map} +1 -1
  115. package/build/server/chunks/cache-BlMaDsHi.js.map +1 -1
  116. package/build/server/chunks/guest-registry-t0-7Zv5q.js +39 -0
  117. package/build/server/chunks/guest-registry-t0-7Zv5q.js.map +1 -0
  118. package/build/server/chunks/index-CoYB03g7.js.map +1 -1
  119. package/build/server/chunks/index2-dSGQ9Eaa.js.map +1 -1
  120. package/build/server/chunks/{pty-manager-41h3IK8K.js → pty-manager-CkZNoW1t.js} +6 -2
  121. package/build/server/chunks/pty-manager-CkZNoW1t.js.map +1 -0
  122. package/build/server/chunks/root-D4IoFC8F.js.map +1 -1
  123. package/build/server/chunks/share-auth-BS7JuiHf.js +27 -0
  124. package/build/server/chunks/share-auth-BS7JuiHf.js.map +1 -0
  125. package/build/server/chunks/share-store-B9jMpVg0.js +127 -0
  126. package/build/server/chunks/share-store-B9jMpVg0.js.map +1 -0
  127. package/build/server/chunks/state.svelte-CmHqngc_.js.map +1 -1
  128. package/build/server/chunks/stores-CRYxfF0o.js.map +1 -1
  129. package/build/server/index.js +1 -1
  130. package/build/server/index.js.map +1 -1
  131. package/build/server/manifest.js +40 -19
  132. package/build/server/manifest.js.map +1 -1
  133. package/package.json +2 -2
  134. package/server.ts +8 -3
  135. package/src/lib/modules/client/terminal/ShareGate.svelte +96 -0
  136. package/src/lib/modules/client/terminal/ShareSheet.svelte +395 -0
  137. package/src/lib/modules/client/terminal/xterm-wrapper.ts +19 -2
  138. package/src/lib/modules/server/terminal/pty-manager.ts +6 -0
  139. package/src/lib/modules/server/terminal/share-auth.ts +37 -0
  140. package/src/lib/modules/server/terminal/share-store.ts +172 -0
  141. package/src/lib/modules/server/ws/guest-registry.ts +49 -0
  142. package/src/lib/modules/server/ws/server.ts +22 -3
  143. package/src/lib/modules/server/ws/session-handler.ts +18 -4
  144. package/src/lib/modules/server/ws/terminal-handler.ts +21 -2
  145. package/src/lib/modules/server/ws/ticket-store.ts +18 -10
  146. package/src/lib/types/generated/Client.ts +25 -1
  147. package/src/lib/types/generated/Share.ts +404 -0
  148. package/src/lib/types/generated/WsProtocol.ts +73 -2
  149. package/src/lib/types/generated/index.ts +1 -0
  150. package/src/lib/types/terminal-client.ts +19 -2
  151. package/src/lib/types/ws.ts +1 -0
  152. package/src/routes/api/terminals/[id]/+server.ts +14 -3
  153. package/src/routes/api/terminals/[id]/paste-image/+server.ts +8 -4
  154. package/src/routes/api/terminals/[id]/resize/+server.ts +8 -4
  155. package/src/routes/api/terminals/[id]/share/+server.ts +98 -0
  156. package/src/routes/api/terminals/[id]/share/auth/+server.ts +81 -0
  157. package/src/routes/api/terminals/[id]/share/status/+server.ts +11 -0
  158. package/src/routes/api/ws-ticket/+server.ts +26 -5
  159. package/src/routes/terminals/[id]/+page.svelte +184 -43
  160. package/build/client/_app/immutable/assets/11.v5KA95xm.css +0 -1
  161. package/build/client/_app/immutable/assets/11.v5KA95xm.css.br +0 -0
  162. package/build/client/_app/immutable/assets/11.v5KA95xm.css.gz +0 -0
  163. package/build/client/_app/immutable/chunks/BcqA7eKM.js +0 -3
  164. package/build/client/_app/immutable/chunks/BcqA7eKM.js.br +0 -0
  165. package/build/client/_app/immutable/chunks/BcqA7eKM.js.gz +0 -0
  166. package/build/client/_app/immutable/chunks/CR6bkGJW.js +0 -6
  167. package/build/client/_app/immutable/chunks/CR6bkGJW.js.br +0 -0
  168. package/build/client/_app/immutable/chunks/CR6bkGJW.js.gz +0 -0
  169. package/build/client/_app/immutable/chunks/DA4Zt9Me.js.br +0 -0
  170. package/build/client/_app/immutable/chunks/DCDL_9ys.js.br +0 -0
  171. package/build/client/_app/immutable/chunks/DCDL_9ys.js.gz +0 -0
  172. package/build/client/_app/immutable/entry/app.D4TXlu7A.js.br +0 -0
  173. package/build/client/_app/immutable/entry/app.D4TXlu7A.js.gz +0 -0
  174. package/build/client/_app/immutable/entry/start.BBQhtURO.js +0 -1
  175. package/build/client/_app/immutable/entry/start.BBQhtURO.js.br +0 -0
  176. package/build/client/_app/immutable/entry/start.BBQhtURO.js.gz +0 -0
  177. package/build/client/_app/immutable/nodes/0.1zylwAPT.js.br +0 -0
  178. package/build/client/_app/immutable/nodes/0.1zylwAPT.js.gz +0 -0
  179. package/build/client/_app/immutable/nodes/1.BVnLUSs-.js.br +0 -0
  180. package/build/client/_app/immutable/nodes/1.BVnLUSs-.js.gz +0 -0
  181. package/build/client/_app/immutable/nodes/10.D1wl2wPX.js.br +0 -0
  182. package/build/client/_app/immutable/nodes/10.D1wl2wPX.js.gz +0 -0
  183. package/build/client/_app/immutable/nodes/11.C18nMGmp.js +0 -2
  184. package/build/client/_app/immutable/nodes/11.C18nMGmp.js.br +0 -0
  185. package/build/client/_app/immutable/nodes/11.C18nMGmp.js.gz +0 -0
  186. package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js.br +0 -0
  187. package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js.gz +0 -0
  188. package/build/client/_app/immutable/nodes/3.Wfz3TcJd.js.br +0 -0
  189. package/build/client/_app/immutable/nodes/3.Wfz3TcJd.js.gz +0 -0
  190. package/build/client/_app/immutable/nodes/6.DtZAEPXb.js.br +0 -0
  191. package/build/client/_app/immutable/nodes/6.DtZAEPXb.js.gz +0 -0
  192. package/build/client/_app/immutable/nodes/7.MfBRh32I.js.br +0 -0
  193. package/build/client/_app/immutable/nodes/7.MfBRh32I.js.gz +0 -0
  194. package/build/client/_app/immutable/nodes/8.DVE6LnOC.js.br +0 -0
  195. package/build/client/_app/immutable/nodes/8.DVE6LnOC.js.gz +0 -0
  196. package/build/client/_app/immutable/nodes/9.BCel5OqI.js.br +0 -0
  197. package/build/client/_app/immutable/nodes/9.BCel5OqI.js.gz +0 -0
  198. package/build/server/chunks/11-qXSPdF5j.js.map +0 -1
  199. package/build/server/chunks/_page.svelte-BUBLUSGo.js.map +0 -1
  200. package/build/server/chunks/_server.ts-Bi0Oe4PF.js.map +0 -1
  201. package/build/server/chunks/_server.ts-Bjbr7glm.js.map +0 -1
  202. package/build/server/chunks/_server.ts-BrqaMMAa.js.map +0 -1
  203. package/build/server/chunks/_server.ts-DhJx0DLr.js.map +0 -1
  204. package/build/server/chunks/events-handler-Dm1mNPQP.js +0 -20
  205. package/build/server/chunks/events-handler-Dm1mNPQP.js.map +0 -1
  206. 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(ws: WebSocket, terminalId: string): void {
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, { createdAt: Date.now(), used: false });
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 true if the ticket is valid, not yet used, and not expired.
34
- * A valid ticket is marked as used (single-use) and cannot be reused.
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): boolean {
44
+ export function validateTicket(ticket: null | string): null | Ticket {
37
45
  if (!ticket) {
38
- return false;
46
+ return null;
39
47
  }
40
48
  const entry = tickets.get(ticket);
41
49
  if (!entry || entry.used) {
42
- return false;
50
+ return null;
43
51
  }
44
52
  if (Date.now() - entry.createdAt > 30_000) {
45
53
  tickets.delete(ticket);
46
- return false;
54
+ return null;
47
55
  }
48
56
  entry.used = true;
49
- return true;
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;