@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,395 @@
1
+ <script lang="ts">
2
+ import type { ShareInfoResponse, ShareMode, ShareSheetProps } from '$lib/types';
3
+
4
+ import { getApiKey } from '$lib/modules/client/common';
5
+ import { Button, Input } from '@juspay/svelte-ui-components';
6
+
7
+ const { onClose, open = false, shareUrl, terminalId }: ShareSheetProps = $props();
8
+
9
+ let active = $state(false);
10
+ let currentMode = $state<null | ShareMode>(null);
11
+ let mode = $state<ShareMode>('view');
12
+ let password = $state('');
13
+ let busy = $state(false);
14
+ let errorMsg = $state<null | string>(null);
15
+ let copied = $state(false);
16
+
17
+ // Password characters avoid ambiguous glyphs (0/O, 1/l/I).
18
+ const PASSWORD_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
19
+
20
+ const canSubmit = $derived(
21
+ active ? password.length === 0 || password.length >= 6 : password.length >= 6
22
+ );
23
+
24
+ $effect(() => {
25
+ if (open) {
26
+ void loadInfo();
27
+ }
28
+ });
29
+
30
+ async function loadInfo(): Promise<void> {
31
+ errorMsg = null;
32
+ copied = false;
33
+ try {
34
+ const res = await fetch(`/api/terminals/${terminalId}/share`, {
35
+ headers: { Authorization: `Bearer ${getApiKey()}` },
36
+ });
37
+ if (!res.ok) {
38
+ errorMsg = 'Failed to load share state.';
39
+ return;
40
+ }
41
+ const info = (await res.json()) as ShareInfoResponse;
42
+ active = info.active;
43
+ currentMode = info.mode ?? null;
44
+ mode = info.mode ?? 'view';
45
+ password = '';
46
+ } catch {
47
+ errorMsg = 'Failed to reach the server.';
48
+ }
49
+ }
50
+
51
+ function generatePassword(): void {
52
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
53
+ password = Array.from(bytes, (b) => PASSWORD_ALPHABET[b % PASSWORD_ALPHABET.length]).join('');
54
+ }
55
+
56
+ async function saveShare(): Promise<void> {
57
+ if (busy || !canSubmit) {
58
+ return;
59
+ }
60
+ busy = true;
61
+ errorMsg = null;
62
+ try {
63
+ const res = await fetch(`/api/terminals/${terminalId}/share`, {
64
+ body: JSON.stringify({ mode, ...(password ? { password } : {}) }),
65
+ headers: {
66
+ Authorization: `Bearer ${getApiKey()}`,
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ method: 'PUT',
70
+ });
71
+ if (!res.ok) {
72
+ const data = (await res.json().catch(() => ({}))) as { error?: string };
73
+ errorMsg = data.error ?? 'Failed to save share.';
74
+ return;
75
+ }
76
+ const info = (await res.json()) as ShareInfoResponse;
77
+ active = info.active;
78
+ currentMode = info.mode ?? null;
79
+ password = '';
80
+ } catch {
81
+ errorMsg = 'Failed to reach the server.';
82
+ } finally {
83
+ busy = false;
84
+ }
85
+ }
86
+
87
+ async function revokeShare(): Promise<void> {
88
+ if (busy) {
89
+ return;
90
+ }
91
+ busy = true;
92
+ errorMsg = null;
93
+ try {
94
+ const res = await fetch(`/api/terminals/${terminalId}/share`, {
95
+ headers: { Authorization: `Bearer ${getApiKey()}` },
96
+ method: 'DELETE',
97
+ });
98
+ if (!res.ok) {
99
+ errorMsg = 'Failed to revoke share.';
100
+ return;
101
+ }
102
+ active = false;
103
+ currentMode = null;
104
+ password = '';
105
+ } catch {
106
+ errorMsg = 'Failed to reach the server.';
107
+ } finally {
108
+ busy = false;
109
+ }
110
+ }
111
+
112
+ async function copyUrl(): Promise<void> {
113
+ try {
114
+ await navigator.clipboard.writeText(shareUrl);
115
+ copied = true;
116
+ setTimeout(() => {
117
+ copied = false;
118
+ }, 2000);
119
+ } catch {
120
+ errorMsg = 'Failed to copy.';
121
+ }
122
+ }
123
+ </script>
124
+
125
+ {#if open}
126
+ <div
127
+ class="share-backdrop"
128
+ onclick={onClose}
129
+ onkeydown={(e: KeyboardEvent): void => {
130
+ if (e.key === 'Escape') {
131
+ onClose();
132
+ }
133
+ }}
134
+ role="presentation"
135
+ >
136
+ <div
137
+ class="share-sheet"
138
+ onclick={(e: MouseEvent): void => {
139
+ e.stopPropagation();
140
+ }}
141
+ role="dialog"
142
+ aria-label="Share terminal"
143
+ tabindex="-1"
144
+ onkeydown={(e: KeyboardEvent): void => {
145
+ if (e.key === 'Escape') {
146
+ onClose();
147
+ }
148
+ }}
149
+ >
150
+ <div class="share-sheet-header">
151
+ <h2 class="share-sheet-title">Share terminal</h2>
152
+ <button class="share-sheet-close" onclick={onClose} aria-label="Close">&times;</button>
153
+ </div>
154
+
155
+ {#if active}
156
+ <div class="share-active-row">
157
+ <span class="share-active-dot"></span>
158
+ <span class="share-active-label">
159
+ Sharing is active ({currentMode === 'control' ? 'full control' : 'view only'})
160
+ </span>
161
+ </div>
162
+ <div class="share-url-row">
163
+ <span class="share-url">{shareUrl}</span>
164
+ <Button
165
+ classes="btn-secondary btn-sm"
166
+ onclick={(): void => {
167
+ void copyUrl();
168
+ }}
169
+ text={copied ? 'Copied' : 'Copy'}
170
+ />
171
+ </div>
172
+ {:else}
173
+ <p class="share-sheet-sub">
174
+ Anyone with this page's link and the password below can access this terminal.
175
+ </p>
176
+ {/if}
177
+
178
+ <div class="share-field">
179
+ <span class="share-field-label">Access</span>
180
+ <div class="share-mode-toggle">
181
+ <button
182
+ class="share-mode-btn {mode === 'view' ? 'share-mode-active' : ''}"
183
+ onclick={(): void => {
184
+ mode = 'view';
185
+ }}
186
+ >
187
+ View only
188
+ </button>
189
+ <button
190
+ class="share-mode-btn {mode === 'control' ? 'share-mode-active' : ''}"
191
+ onclick={(): void => {
192
+ mode = 'control';
193
+ }}
194
+ >
195
+ Full control
196
+ </button>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="share-field">
201
+ <span class="share-field-label">
202
+ {active ? 'New password (leave empty to keep current)' : 'Password (min 6 chars)'}
203
+ </span>
204
+ <div class="share-password-row">
205
+ <Input
206
+ bind:value={password}
207
+ dataType="text"
208
+ placeholder="Password"
209
+ classes="share-password-input"
210
+ />
211
+ <Button classes="btn-secondary btn-sm" onclick={generatePassword} text="Generate" />
212
+ </div>
213
+ </div>
214
+
215
+ {#if errorMsg}
216
+ <p class="share-error">{errorMsg}</p>
217
+ {/if}
218
+
219
+ <div class="share-actions">
220
+ {#if active}
221
+ <Button
222
+ classes="btn-danger btn-sm"
223
+ onclick={(): void => {
224
+ void revokeShare();
225
+ }}
226
+ disabled={busy}
227
+ text="Stop sharing"
228
+ />
229
+ {/if}
230
+ <Button
231
+ classes="btn-primary btn-sm"
232
+ onclick={(): void => {
233
+ void saveShare();
234
+ }}
235
+ disabled={busy || !canSubmit}
236
+ showLoader={busy}
237
+ text={active ? 'Update' : 'Start sharing'}
238
+ />
239
+ </div>
240
+ </div>
241
+ </div>
242
+ {/if}
243
+
244
+ <style>
245
+ .share-backdrop {
246
+ position: fixed;
247
+ inset: 0;
248
+ z-index: 100;
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ background: rgba(0, 0, 0, 0.5);
253
+ padding: var(--space-4);
254
+ }
255
+
256
+ .share-sheet {
257
+ display: flex;
258
+ flex-direction: column;
259
+ gap: var(--space-3);
260
+ width: 100%;
261
+ max-width: 420px;
262
+ padding: var(--space-5);
263
+ background: var(--ds-background-100);
264
+ border: 1px solid var(--border);
265
+ border-radius: var(--radius-lg);
266
+ }
267
+
268
+ .share-sheet-header {
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: space-between;
272
+ }
273
+
274
+ .share-sheet-title {
275
+ margin: 0;
276
+ font-size: var(--text-lg);
277
+ color: var(--text-primary);
278
+ }
279
+
280
+ .share-sheet-close {
281
+ background: none;
282
+ border: none;
283
+ color: var(--text-tertiary);
284
+ font-size: 22px;
285
+ cursor: pointer;
286
+ line-height: 1;
287
+ padding: 4px;
288
+ }
289
+
290
+ .share-sheet-close:hover {
291
+ color: var(--text-primary);
292
+ }
293
+
294
+ .share-sheet-sub {
295
+ margin: 0;
296
+ font-size: var(--text-sm);
297
+ color: var(--text-tertiary);
298
+ }
299
+
300
+ .share-active-row {
301
+ display: flex;
302
+ align-items: center;
303
+ gap: var(--space-2);
304
+ }
305
+
306
+ .share-active-dot {
307
+ width: 8px;
308
+ height: 8px;
309
+ border-radius: 50%;
310
+ background: var(--ds-green-500, #22c55e);
311
+ }
312
+
313
+ .share-active-label {
314
+ font-size: var(--text-sm);
315
+ color: var(--text-secondary);
316
+ }
317
+
318
+ .share-url-row {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: var(--space-2);
322
+ padding: var(--space-2);
323
+ background: var(--ds-gray-200);
324
+ border-radius: var(--radius-md);
325
+ }
326
+
327
+ .share-url {
328
+ flex: 1;
329
+ font-family: var(--font-mono);
330
+ font-size: 11px;
331
+ color: var(--text-secondary);
332
+ overflow: hidden;
333
+ text-overflow: ellipsis;
334
+ white-space: nowrap;
335
+ }
336
+
337
+ .share-field {
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: var(--space-1);
341
+ }
342
+
343
+ .share-field-label {
344
+ font-size: var(--text-xs);
345
+ color: var(--text-tertiary);
346
+ }
347
+
348
+ .share-mode-toggle {
349
+ display: flex;
350
+ gap: 2px;
351
+ padding: 2px;
352
+ background: var(--ds-gray-200);
353
+ border: 1px solid var(--ds-gray-400);
354
+ border-radius: var(--radius-md);
355
+ width: fit-content;
356
+ }
357
+
358
+ .share-mode-btn {
359
+ padding: 6px 12px;
360
+ font-size: var(--text-xs);
361
+ color: var(--text-tertiary);
362
+ background: none;
363
+ border: none;
364
+ border-radius: var(--radius-sm);
365
+ cursor: pointer;
366
+ }
367
+
368
+ .share-mode-active {
369
+ background: var(--ds-background-100);
370
+ color: var(--text-primary);
371
+ }
372
+
373
+ .share-password-row {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: var(--space-2);
377
+ }
378
+
379
+ .share-password-row :global(.share-password-input) {
380
+ --input-container-margin: 0;
381
+ flex: 1;
382
+ }
383
+
384
+ .share-error {
385
+ margin: 0;
386
+ font-size: var(--text-sm);
387
+ color: var(--ds-red-700, #ef4444);
388
+ }
389
+
390
+ .share-actions {
391
+ display: flex;
392
+ justify-content: flex-end;
393
+ gap: var(--space-2);
394
+ }
395
+ </style>
@@ -48,7 +48,12 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
48
48
  term.loadAddon(fitAddon);
49
49
  term.loadAddon(new WebLinksAddon());
50
50
  term.open(options.container);
51
- fitAddon.fit();
51
+ if (options.readOnly && options.initialCols && options.initialRows) {
52
+ // View-only: render at the PTY's size (we may not resize the shared PTY).
53
+ term.resize(options.initialCols, options.initialRows);
54
+ } else {
55
+ fitAddon.fit();
56
+ }
52
57
 
53
58
  // Block browser-level Cmd/Ctrl shortcuts from reaching the PTY.
54
59
  // Allow Ctrl+<letter> terminal signals (Ctrl+C/D/L/R/Z etc.) through.
@@ -71,7 +76,7 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
71
76
 
72
77
  // Clipboard image paste interception
73
78
  let pasteListener: ((e: ClipboardEvent) => void) | null = null;
74
- if (options.terminalId && options.apiKey) {
79
+ if (options.terminalId && options.apiKey && !options.readOnly) {
75
80
  const pasteTermId = options.terminalId;
76
81
  const pasteApiKey = options.apiKey;
77
82
 
@@ -185,6 +190,12 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
185
190
  options.onActivity?.(msg.active ?? false);
186
191
  } else if (msg.type === 'cwd') {
187
192
  options.onCwd?.(msg.path ?? '');
193
+ } else if (msg.type === 'resize') {
194
+ // PTY was resized by another client (e.g. the owner). View-only
195
+ // terminals follow it; interactive ones are governed by their fit.
196
+ if (options.readOnly && msg.cols && msg.rows) {
197
+ term.resize(msg.cols, msg.rows);
198
+ }
188
199
  }
189
200
  };
190
201
 
@@ -205,6 +216,9 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
205
216
 
206
217
  // Terminal input -> WebSocket
207
218
  term.onData((data) => {
219
+ if (options.readOnly) {
220
+ return;
221
+ }
208
222
  if (ws?.readyState === WebSocket.OPEN) {
209
223
  ws.send(JSON.stringify({ data, type: 'input' }));
210
224
  }
@@ -212,6 +226,9 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
212
226
 
213
227
  // Handle resize — skip when container is hidden (display:none → size 0)
214
228
  const resizeObserver = new ResizeObserver((): void => {
229
+ if (options.readOnly) {
230
+ return; // View-only terminals keep the PTY's dimensions.
231
+ }
215
232
  if (!options.container.offsetWidth || !options.container.offsetHeight) {
216
233
  return;
217
234
  }
@@ -483,6 +483,12 @@ class PtyManager {
483
483
  terminal.pty.resize(cols, rows);
484
484
  terminal.cols = cols;
485
485
  terminal.rows = rows;
486
+ // Broadcast the new PTY size so attached clients (e.g. view-only
487
+ // guests) can follow the terminal dimensions.
488
+ const msg = JSON.stringify({ cols, rows, type: 'resize' });
489
+ for (const ws of terminal.clients) {
490
+ this.safeSend(ws, msg);
491
+ }
486
492
  return true;
487
493
  } catch {
488
494
  return false;
@@ -0,0 +1,37 @@
1
+ // Resolves a terminal-scoped request to owner (API key) or guest (share token).
2
+ // Kept separate from auth.ts so routes without share semantics don't pull in SQLite.
3
+
4
+ import type { AccessContext } from '$lib/types';
5
+
6
+ import { validateAuth } from '../auth';
7
+ import { shareStore } from './share-store';
8
+
9
+ /** Extract the Bearer token from a request, or null. */
10
+ export function bearerToken(request: Request): null | string {
11
+ const auth = request.headers.get('Authorization') || request.headers.get('authorization');
12
+ if (!auth?.startsWith('Bearer ')) {
13
+ return null;
14
+ }
15
+ return auth.slice(7).trim();
16
+ }
17
+
18
+ /**
19
+ * Resolve access for a request targeting one terminal.
20
+ * Owner (valid API key) → { level: 'owner' }.
21
+ * Guest (valid share session for THIS terminal) → { level: 'guest', mode }.
22
+ * Anything else → null.
23
+ */
24
+ export function resolveAccess(request: Request, terminalId: string): AccessContext | null {
25
+ if (validateAuth(request) === null) {
26
+ return { level: 'owner', mode: null };
27
+ }
28
+ const token = bearerToken(request);
29
+ if (!token) {
30
+ return null;
31
+ }
32
+ const session = shareStore.resolveToken(token);
33
+ if (session?.terminalId !== terminalId) {
34
+ return null;
35
+ }
36
+ return { level: 'guest', mode: session.mode };
37
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Share Store — SQLite persistence for terminal sharing.
3
+ *
4
+ * terminal_shares: one share per terminal (scrypt password hash + mode).
5
+ * share_sessions: guest sessions keyed by sha256(token), 7-day TTL.
6
+ *
7
+ * Database location: ~/.shooter/shooter.db (same file as terminal-store).
8
+ */
9
+
10
+ import type { ShareMode, TerminalShareRecord } from '$lib/types';
11
+
12
+ import Database from 'better-sqlite3';
13
+ import { createHash, randomBytes, scryptSync, timingSafeEqual } from 'crypto';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+
17
+ const DB_DIR = path.join(process.env.HOME || '', '.shooter');
18
+ const DB_PATH = path.join(DB_DIR, 'shooter.db');
19
+
20
+ const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
21
+
22
+ // ── Password hashing (scrypt, per-share random salt) ─────────────────
23
+
24
+ export class ShareStore {
25
+ private db: Database.Database;
26
+
27
+ constructor() {
28
+ fs.mkdirSync(DB_DIR, { recursive: true });
29
+ this.db = new Database(DB_PATH);
30
+ this.db.pragma('journal_mode = WAL');
31
+
32
+ this.db.exec(`
33
+ CREATE TABLE IF NOT EXISTS terminal_shares (
34
+ terminal_id TEXT PRIMARY KEY,
35
+ password_hash TEXT NOT NULL,
36
+ mode TEXT NOT NULL,
37
+ created_at INTEGER NOT NULL,
38
+ updated_at INTEGER NOT NULL
39
+ );
40
+ CREATE TABLE IF NOT EXISTS share_sessions (
41
+ token_hash TEXT PRIMARY KEY,
42
+ terminal_id TEXT NOT NULL,
43
+ created_at INTEGER NOT NULL,
44
+ expires_at INTEGER NOT NULL
45
+ )
46
+ `);
47
+
48
+ this.cleanup();
49
+ }
50
+
51
+ /** Purge expired sessions and shares whose terminal no longer exists. */
52
+ cleanup(): void {
53
+ this.db.prepare('DELETE FROM share_sessions WHERE expires_at < ?').run(Date.now());
54
+ try {
55
+ this.db
56
+ .prepare('DELETE FROM terminal_shares WHERE terminal_id NOT IN (SELECT id FROM terminals)')
57
+ .run();
58
+ this.db
59
+ .prepare('DELETE FROM share_sessions WHERE terminal_id NOT IN (SELECT id FROM terminals)')
60
+ .run();
61
+ } catch {
62
+ // terminals table may not exist yet on a fresh database — skip orphan cleanup.
63
+ }
64
+ }
65
+
66
+ /** Issue a new guest session for a shared terminal. Returns the raw token (stored hashed). */
67
+ createSession(terminalId: string): { expiresAt: number; token: string } {
68
+ const token = randomBytes(32).toString('hex');
69
+ const now = Date.now();
70
+ const expiresAt = now + SESSION_TTL_MS;
71
+ this.db
72
+ .prepare(
73
+ 'INSERT INTO share_sessions (token_hash, terminal_id, created_at, expires_at) VALUES (?, ?, ?, ?)'
74
+ )
75
+ .run(hashToken(token), terminalId, now, expiresAt);
76
+ return { expiresAt, token };
77
+ }
78
+
79
+ /** Delete all guest sessions for a terminal (password change / revoke). */
80
+ deleteSessions(terminalId: string): void {
81
+ this.db.prepare('DELETE FROM share_sessions WHERE terminal_id = ?').run(terminalId);
82
+ }
83
+
84
+ /** Revoke a share: delete the share row and every guest session for it. */
85
+ deleteShare(terminalId: string): void {
86
+ this.db.prepare('DELETE FROM terminal_shares WHERE terminal_id = ?').run(terminalId);
87
+ this.deleteSessions(terminalId);
88
+ }
89
+
90
+ getShare(terminalId: string): null | TerminalShareRecord {
91
+ const row = this.db
92
+ .prepare('SELECT * FROM terminal_shares WHERE terminal_id = ?')
93
+ .get(terminalId) as Record<string, unknown> | undefined;
94
+ return row ? rowToShare(row) : null;
95
+ }
96
+
97
+ /**
98
+ * Resolve a guest bearer token to its terminal + mode.
99
+ * Returns null if unknown, expired, or the share was revoked.
100
+ */
101
+ resolveToken(token: string): null | { mode: ShareMode; terminalId: string } {
102
+ if (!token) {
103
+ return null;
104
+ }
105
+ const row = this.db
106
+ .prepare(
107
+ `SELECT s.terminal_id, s.expires_at, sh.mode
108
+ FROM share_sessions s
109
+ JOIN terminal_shares sh ON sh.terminal_id = s.terminal_id
110
+ WHERE s.token_hash = ?`
111
+ )
112
+ .get(hashToken(token)) as Record<string, unknown> | undefined;
113
+ if (!row) {
114
+ return null;
115
+ }
116
+ if ((row.expires_at as number) < Date.now()) {
117
+ this.db.prepare('DELETE FROM share_sessions WHERE token_hash = ?').run(hashToken(token));
118
+ return null;
119
+ }
120
+ return { mode: row.mode as ShareMode, terminalId: row.terminal_id as string };
121
+ }
122
+
123
+ /** Create or replace the share for a terminal. */
124
+ setShare(record: TerminalShareRecord): void {
125
+ this.db
126
+ .prepare(
127
+ `INSERT OR REPLACE INTO terminal_shares
128
+ (terminal_id, password_hash, mode, created_at, updated_at)
129
+ VALUES (?, ?, ?, ?, ?)`
130
+ )
131
+ .run(record.terminalId, record.passwordHash, record.mode, record.createdAt, record.updatedAt);
132
+ }
133
+ }
134
+
135
+ export function hashPassword(password: string): string {
136
+ const salt = randomBytes(16).toString('hex');
137
+ const hash = scryptSync(password, salt, 64).toString('hex');
138
+ return `scrypt:${salt}:${hash}`;
139
+ }
140
+
141
+ export function verifyPassword(password: string, stored: string): boolean {
142
+ const parts = stored.split(':');
143
+ if (parts.length !== 3 || parts[0] !== 'scrypt') {
144
+ return false;
145
+ }
146
+ const expected = Buffer.from(parts[2], 'hex');
147
+ const actual = scryptSync(password, parts[1], 64);
148
+ return expected.length === actual.length && timingSafeEqual(actual, expected);
149
+ }
150
+
151
+ // ── Row mapping ──────────────────────────────────────────────────────
152
+
153
+ function hashToken(token: string): string {
154
+ return createHash('sha256').update(token).digest('hex');
155
+ }
156
+
157
+ function rowToShare(row: Record<string, unknown>): TerminalShareRecord {
158
+ return {
159
+ createdAt: row.created_at as number,
160
+ mode: row.mode as ShareMode,
161
+ passwordHash: row.password_hash as string,
162
+ terminalId: row.terminal_id as string,
163
+ updatedAt: row.updated_at as number,
164
+ };
165
+ }
166
+
167
+ // ── Singleton (globalThis bridges tsx server.ts + SvelteKit handler) ─
168
+
169
+ const SS_GLOBAL_KEY = '__shooter_share_store';
170
+ export const shareStore: ShareStore =
171
+ ((globalThis as Record<string, unknown>)[SS_GLOBAL_KEY] as ShareStore) || new ShareStore();
172
+ (globalThis as Record<string, unknown>)[SS_GLOBAL_KEY] = shareStore;