@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,98 @@
1
+ // /api/terminals/[id]/share — owner management of a terminal's share.
2
+ // GET: current state. PUT: create/update. DELETE: revoke.
3
+ // All methods require the API key (owners only).
4
+
5
+ import type { ShareConfigRequest, ShareInfoResponse, ShareMode } from '$lib/types';
6
+
7
+ import { validateAuth } from '$lib/modules/server/auth';
8
+ import { ptyManager } from '$lib/modules/server/terminal/pty-manager.js';
9
+ import { hashPassword, shareStore } from '$lib/modules/server/terminal/share-store';
10
+ import { closeGuests } from '$lib/modules/server/ws/guest-registry';
11
+ import { json } from '@sveltejs/kit';
12
+
13
+ import type { RequestHandler } from './$types';
14
+
15
+ const MIN_PASSWORD_LENGTH = 6;
16
+ const MODES: ShareMode[] = ['view', 'control'];
17
+
18
+ function toInfo(terminalId: string): ShareInfoResponse {
19
+ const share = shareStore.getShare(terminalId);
20
+ if (!share) {
21
+ return { active: false, createdAt: null, mode: null, updatedAt: null };
22
+ }
23
+ return { active: true, createdAt: share.createdAt, mode: share.mode, updatedAt: share.updatedAt };
24
+ }
25
+
26
+ export const GET: RequestHandler = ({ params, request }) => {
27
+ const authError = validateAuth(request);
28
+ if (authError) {
29
+ return authError;
30
+ }
31
+ return json(toInfo(params.id));
32
+ };
33
+
34
+ export const PUT: RequestHandler = async ({ params, request }) => {
35
+ const authError = validateAuth(request);
36
+ if (authError) {
37
+ return authError;
38
+ }
39
+ if (!ptyManager.get(params.id)) {
40
+ return json({ error: 'Terminal not found' }, { status: 404 });
41
+ }
42
+
43
+ let body: ShareConfigRequest;
44
+ try {
45
+ body = (await request.json()) as ShareConfigRequest;
46
+ } catch {
47
+ return json({ error: 'Invalid JSON' }, { status: 400 });
48
+ }
49
+ if (!MODES.includes(body.mode)) {
50
+ return json({ error: "mode must be 'view' or 'control'" }, { status: 400 });
51
+ }
52
+
53
+ const existing = shareStore.getShare(params.id);
54
+ const password = typeof body.password === 'string' ? body.password : '';
55
+ if (!existing && password.length < MIN_PASSWORD_LENGTH) {
56
+ return json(
57
+ { error: `password is required (min ${String(MIN_PASSWORD_LENGTH)} chars)` },
58
+ { status: 400 }
59
+ );
60
+ }
61
+ if (password && password.length < MIN_PASSWORD_LENGTH) {
62
+ return json(
63
+ { error: `password must be at least ${String(MIN_PASSWORD_LENGTH)} chars` },
64
+ { status: 400 }
65
+ );
66
+ }
67
+
68
+ const now = Date.now();
69
+ shareStore.setShare({
70
+ createdAt: existing?.createdAt ?? now,
71
+ mode: body.mode,
72
+ // `existing` is guaranteed non-null when password is empty (validated above).
73
+ passwordHash: password ? hashPassword(password) : (existing?.passwordHash ?? ''),
74
+ terminalId: params.id,
75
+ updatedAt: now,
76
+ });
77
+
78
+ // A new password invalidates existing guest sessions; any change to the
79
+ // share forces connected guests to reconnect under the new scope.
80
+ if (password) {
81
+ shareStore.deleteSessions(params.id);
82
+ }
83
+ if (password || existing?.mode !== body.mode) {
84
+ closeGuests(params.id);
85
+ }
86
+
87
+ return json(toInfo(params.id));
88
+ };
89
+
90
+ export const DELETE: RequestHandler = ({ params, request }) => {
91
+ const authError = validateAuth(request);
92
+ if (authError) {
93
+ return authError;
94
+ }
95
+ shareStore.deleteShare(params.id);
96
+ const closed = closeGuests(params.id);
97
+ return json({ closedConnections: closed, success: true });
98
+ };
@@ -0,0 +1,81 @@
1
+ // POST /api/terminals/[id]/share/auth — exchange the share password for a
2
+ // guest session token. Public endpoint; brute-force-limited per IP+terminal.
3
+
4
+ import type { ShareAuthRequest } from '$lib/types';
5
+
6
+ import { shareStore, verifyPassword } from '$lib/modules/server/terminal/share-store';
7
+ import { json } from '@sveltejs/kit';
8
+
9
+ import type { RequestHandler } from './$types';
10
+
11
+ const RATE_LIMIT_WINDOW_MS = 60_000;
12
+ const RATE_LIMIT_MAX = 10;
13
+
14
+ /** Maps "ip:terminalId" -> attempt timestamps (epoch ms). */
15
+ const attempts = new Map<string, number[]>();
16
+
17
+ function checkRateLimit(key: string): boolean {
18
+ const now = Date.now();
19
+ const recent = (attempts.get(key) ?? []).filter((t) => t > now - RATE_LIMIT_WINDOW_MS);
20
+ attempts.set(key, recent);
21
+ if (recent.length >= RATE_LIMIT_MAX) {
22
+ return false;
23
+ }
24
+ recent.push(now);
25
+ return true;
26
+ }
27
+
28
+ // Cleanup stale rate limit entries every 5 minutes
29
+ setInterval(() => {
30
+ const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
31
+ for (const [key, timestamps] of attempts) {
32
+ const recent = timestamps.filter((t) => t > cutoff);
33
+ if (recent.length === 0) {
34
+ attempts.delete(key);
35
+ } else {
36
+ attempts.set(key, recent);
37
+ }
38
+ }
39
+ }, 300_000).unref();
40
+
41
+ export const POST: RequestHandler = async (event) => {
42
+ const { params, request } = event;
43
+
44
+ const share = shareStore.getShare(params.id);
45
+ if (!share) {
46
+ return json({ error: 'Not shared' }, { status: 404 });
47
+ }
48
+
49
+ // Behind Cloudflare Tunnel the connecting IP arrives in headers.
50
+ let ip = 'unknown';
51
+ const cfIp = request.headers.get('cf-connecting-ip');
52
+ const fwd = request.headers.get('x-forwarded-for');
53
+ if (cfIp) {
54
+ ip = cfIp;
55
+ } else if (fwd) {
56
+ ip = fwd.split(',')[0].trim();
57
+ } else {
58
+ try {
59
+ ip = event.getClientAddress();
60
+ } catch {
61
+ // Keep 'unknown' — the rate limit still applies per terminal.
62
+ }
63
+ }
64
+
65
+ if (!checkRateLimit(`${ip}:${params.id}`)) {
66
+ return json({ error: 'Too many attempts. Try again in a minute.' }, { status: 429 });
67
+ }
68
+
69
+ let body: ShareAuthRequest;
70
+ try {
71
+ body = (await request.json()) as ShareAuthRequest;
72
+ } catch {
73
+ return json({ error: 'Invalid JSON' }, { status: 400 });
74
+ }
75
+ if (typeof body.password !== 'string' || !verifyPassword(body.password, share.passwordHash)) {
76
+ return json({ error: 'Invalid password' }, { status: 401 });
77
+ }
78
+
79
+ const { expiresAt, token } = shareStore.createSession(params.id);
80
+ return json({ expiresAt, mode: share.mode, token });
81
+ };
@@ -0,0 +1,11 @@
1
+ // GET /api/terminals/[id]/share/status — public probe used by the page to
2
+ // decide whether to show the password gate. Reveals only a boolean.
3
+
4
+ import { shareStore } from '$lib/modules/server/terminal/share-store';
5
+ import { json } from '@sveltejs/kit';
6
+
7
+ import type { RequestHandler } from './$types';
8
+
9
+ export const GET: RequestHandler = ({ params }) => {
10
+ return json({ shared: shareStore.getShare(params.id) !== null });
11
+ };
@@ -8,6 +8,7 @@
8
8
  // Rate limited to 30 requests per minute per API key.
9
9
 
10
10
  import { validateAuth } from '$lib/modules/server/auth';
11
+ import { shareStore } from '$lib/modules/server/terminal/share-store';
11
12
  import { generateTicket } from '$lib/modules/server/ws/ticket-store';
12
13
  import { json } from '@sveltejs/kit';
13
14
 
@@ -63,15 +64,35 @@ setInterval(() => {
63
64
  // ── Endpoint ────────────────────────────────────────────────────────
64
65
 
65
66
  export const POST: RequestHandler = ({ request }) => {
67
+ const bearer = (
68
+ request.headers.get('authorization') ??
69
+ request.headers.get('Authorization') ??
70
+ ''
71
+ )
72
+ .replace(/^Bearer\s+/i, '')
73
+ .trim();
74
+
66
75
  const authError = validateAuth(request);
67
76
  if (authError) {
68
- return authError;
77
+ // Not the API key — maybe a guest share token (issues a scoped ticket).
78
+ const session = bearer ? shareStore.resolveToken(bearer) : null;
79
+ if (!session) {
80
+ return authError;
81
+ }
82
+ if (!checkRateLimit(bearer)) {
83
+ return json(
84
+ { error: 'Rate limit exceeded. Maximum 30 ticket requests per minute.' },
85
+ { status: 429 }
86
+ );
87
+ }
88
+ const ticket = generateTicket({
89
+ readOnly: session.mode === 'view',
90
+ terminalId: session.terminalId,
91
+ });
92
+ return json({ expiresIn: 30, ticket });
69
93
  }
70
94
 
71
- // Extract the API key for rate limiting
72
- const apiKey = (request.headers.get('authorization') ?? '').substring(7).trim();
73
-
74
- if (!checkRateLimit(apiKey)) {
95
+ if (!checkRateLimit(bearer)) {
75
96
  return json(
76
97
  { error: 'Rate limit exceeded. Maximum 30 ticket requests per minute.' },
77
98
  { status: 429 }
@@ -2,6 +2,9 @@
2
2
  import type {
3
3
  ConversationMessage,
4
4
  MessagePart,
5
+ ShareAuthResponse,
6
+ ShareMode,
7
+ ShareStatusResponse,
5
8
  ShooterConfig,
6
9
  TerminalDetailView,
7
10
  ToolUsePart,
@@ -17,6 +20,8 @@
17
20
  import ConnectionStatus from '$lib/modules/client/terminal/ConnectionStatus.svelte';
18
21
  import { createShortcutManager } from '$lib/modules/client/terminal/keyboard-shortcuts';
19
22
  import QuickKeys from '$lib/modules/client/terminal/QuickKeys.svelte';
23
+ import ShareGate from '$lib/modules/client/terminal/ShareGate.svelte';
24
+ import ShareSheet from '$lib/modules/client/terminal/ShareSheet.svelte';
20
25
  import ShortcutsHelp from '$lib/modules/client/terminal/ShortcutsHelp.svelte';
21
26
  import {
22
27
  Button,
@@ -46,6 +51,10 @@
46
51
  let inputText = $state('');
47
52
  let chatMessages = $state<ConversationMessage[]>([]);
48
53
  let chatSessionEnded = $state(false);
54
+ let authMode = $state<'guest' | 'owner' | null>(null);
55
+ let guestMode = $state<null | ShareMode>(null);
56
+ let shareGateVisible = $state(false);
57
+ let shareSheetOpen = $state(false);
49
58
 
50
59
  // DOM references
51
60
  let termContainer = $state<HTMLDivElement | null>(null);
@@ -80,6 +89,11 @@
80
89
  );
81
90
  const tabActiveIndex = $derived(viewMode === 'raw' ? 0 : 1);
82
91
  const displayCwd = $derived(shortenPath(currentCwd || terminal?.cwd || ''));
92
+ const isOwner = $derived(authMode === 'owner');
93
+ const viewOnly = $derived(authMode === 'guest' && guestMode === 'view');
94
+ const shareUrl = $derived(
95
+ typeof window !== 'undefined' ? `${window.location.origin}/terminals/${terminalId}` : ''
96
+ );
83
97
  const paletteCommands = $derived.by((): { action: () => void; label: string }[] => {
84
98
  const cmds: { action: () => void; label: string }[] = [
85
99
  { action: (): void => void goto('/'), label: 'Go to Home' },
@@ -92,7 +106,7 @@
92
106
  label: 'Show keyboard shortcuts',
93
107
  },
94
108
  ];
95
- if (isRunning) {
109
+ if (isRunning && isOwner) {
96
110
  cmds.push({ action: (): void => void killTerminal(), label: 'Kill terminal' });
97
111
  }
98
112
  return cmds;
@@ -142,6 +156,47 @@
142
156
  }
143
157
  }
144
158
 
159
+ // ------- Guest share tokens -------
160
+
161
+ const SHARE_TOKENS_KEY = 'shooter_share_tokens';
162
+
163
+ function getShareToken(): null | string {
164
+ const id = terminalId;
165
+ if (!id) {
166
+ return null;
167
+ }
168
+ try {
169
+ const raw = localStorage.getItem(SHARE_TOKENS_KEY);
170
+ if (!raw) {
171
+ return null;
172
+ }
173
+ const map = JSON.parse(raw) as Record<string, string>;
174
+ return typeof map[id] === 'string' ? map[id] : null;
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ function storeShareToken(token: string): void {
181
+ const id = terminalId;
182
+ if (!id) {
183
+ return;
184
+ }
185
+ let map: Record<string, string> = {};
186
+ try {
187
+ map = JSON.parse(localStorage.getItem(SHARE_TOKENS_KEY) ?? '{}') as Record<string, string>;
188
+ } catch {
189
+ // Corrupt entry — start fresh.
190
+ }
191
+ map[id] = token;
192
+ localStorage.setItem(SHARE_TOKENS_KEY, JSON.stringify(map));
193
+ }
194
+
195
+ /** Bearer for API calls: the owner's API key, or this terminal's guest token. */
196
+ function getBearer(): null | string {
197
+ return getConfig()?.apiKey ?? getShareToken();
198
+ }
199
+
145
200
  // ------- API calls -------
146
201
 
147
202
  async function fetchTerminal(): Promise<void> {
@@ -150,36 +205,92 @@
150
205
  }
151
206
 
152
207
  const config = getConfig();
153
- if (!config) {
154
- error = 'No configuration found. Please configure settings first.';
155
- loading = false;
208
+ const bearer = config?.apiKey ?? getShareToken();
209
+ if (!bearer) {
210
+ await checkShareAccess();
156
211
  return;
157
212
  }
158
213
 
159
214
  try {
160
215
  const res = await fetch(`/api/terminals/${terminalId}`, {
161
- headers: { Authorization: `Bearer ${config.apiKey}` },
216
+ headers: { Authorization: `Bearer ${bearer}` },
162
217
  });
218
+ if (res.status === 401 && !config) {
219
+ // Stale/revoked guest token — fall back to the password gate.
220
+ await checkShareAccess();
221
+ return;
222
+ }
163
223
  if (!res.ok) {
164
224
  error = res.status === 404 ? 'Terminal not found' : 'Failed to load terminal';
165
225
  loading = false;
166
226
  return;
167
227
  }
168
228
  terminal = (await res.json()) as TerminalDetailView;
229
+ if (config) {
230
+ authMode = 'owner';
231
+ } else {
232
+ authMode = 'guest';
233
+ guestMode = terminal.shareMode ?? 'view';
234
+ }
169
235
  } catch {
170
236
  error = 'Failed to connect to server';
171
237
  }
172
238
  loading = false;
173
239
  }
174
240
 
241
+ async function checkShareAccess(): Promise<void> {
242
+ try {
243
+ const res = await fetch(`/api/terminals/${terminalId}/share/status`);
244
+ if (res.ok) {
245
+ const data = (await res.json()) as ShareStatusResponse;
246
+ if (data.shared) {
247
+ shareGateVisible = true;
248
+ loading = false;
249
+ return;
250
+ }
251
+ }
252
+ } catch {
253
+ // Fall through to the configuration error.
254
+ }
255
+ error = 'No configuration found. Please configure settings first.';
256
+ loading = false;
257
+ }
258
+
259
+ async function submitSharePassword(password: string): Promise<null | string> {
260
+ try {
261
+ const res = await fetch(`/api/terminals/${terminalId}/share/auth`, {
262
+ body: JSON.stringify({ password }),
263
+ headers: { 'Content-Type': 'application/json' },
264
+ method: 'POST',
265
+ });
266
+ if (res.status === 429) {
267
+ return 'Too many attempts — try again in a minute.';
268
+ }
269
+ if (!res.ok) {
270
+ return 'Incorrect password.';
271
+ }
272
+ const data = (await res.json()) as ShareAuthResponse;
273
+ storeShareToken(data.token);
274
+ shareGateVisible = false;
275
+ loading = true;
276
+ await fetchTerminal();
277
+ if (terminal && !error) {
278
+ initViews();
279
+ }
280
+ return null;
281
+ } catch {
282
+ return 'Failed to reach the server.';
283
+ }
284
+ }
285
+
175
286
  async function getWsTicket(): Promise<null | string> {
176
- const config = getConfig();
177
- if (!config) {
287
+ const bearer = getBearer();
288
+ if (!bearer) {
178
289
  return null;
179
290
  }
180
291
  try {
181
292
  const res = await fetch('/api/ws-ticket', {
182
- headers: { Authorization: `Bearer ${config.apiKey}` },
293
+ headers: { Authorization: `Bearer ${bearer}` },
183
294
  method: 'POST',
184
295
  });
185
296
  if (!res.ok) {
@@ -269,10 +380,12 @@
269
380
  }
270
381
 
271
382
  const instance = await createTerminal({
272
- apiKey: getConfig()?.apiKey,
383
+ apiKey: getBearer() ?? undefined,
273
384
  container: termContainer,
274
385
  fontSize: window.innerWidth < 768 ? 12 : 14,
275
386
  getTicket,
387
+ initialCols: terminal.cols ?? undefined,
388
+ initialRows: terminal.rows ?? undefined,
276
389
  onActivity: (active: boolean) => {
277
390
  if (!disposed) {
278
391
  isActive = active;
@@ -304,6 +417,7 @@
304
417
  rawConnectionStatus = 'connected';
305
418
  }
306
419
  },
420
+ readOnly: viewOnly,
307
421
  terminalId,
308
422
  wsUrl,
309
423
  });
@@ -606,23 +720,7 @@
606
720
 
607
721
  // ------- Lifecycle -------
608
722
 
609
- onMount(async () => {
610
- await fetchTerminal();
611
- if (disposed) {
612
- return;
613
- }
614
-
615
- // Set up keyboard shortcuts
616
- shortcutManager = createShortcutManager({
617
- onHelp: () => {
618
- showShortcutsHelp = !showShortcutsHelp;
619
- },
620
- });
621
-
622
- if (!terminal || error) {
623
- return;
624
- }
625
-
723
+ function initViews(): void {
626
724
  // Default view: Chat on mobile for AI sessions, Raw on desktop
627
725
  if (isAI && window.innerWidth < 768) {
628
726
  viewMode = 'chat';
@@ -640,6 +738,26 @@
640
738
  void connectSessionWs();
641
739
  chatInitialized = true;
642
740
  }
741
+ }
742
+
743
+ onMount(async () => {
744
+ await fetchTerminal();
745
+ if (disposed) {
746
+ return;
747
+ }
748
+
749
+ // Set up keyboard shortcuts
750
+ shortcutManager = createShortcutManager({
751
+ onHelp: () => {
752
+ showShortcutsHelp = !showShortcutsHelp;
753
+ },
754
+ });
755
+
756
+ if (!terminal || error) {
757
+ return;
758
+ }
759
+
760
+ initViews();
643
761
  });
644
762
 
645
763
  onDestroy(() => {
@@ -664,6 +782,10 @@
664
782
  <div class="skeleton" style="width: 100%; height: 100%;"></div>
665
783
  </div>
666
784
  </div>
785
+ {:else if shareGateVisible}
786
+ <div class="term-page">
787
+ <ShareGate onSubmit={submitSharePassword} />
788
+ </div>
667
789
  {:else if error}
668
790
  <main class="main">
669
791
  <div class="session-back-row">
@@ -681,7 +803,9 @@
681
803
  <!-- Top Bar -->
682
804
  <div class="term-topbar">
683
805
  <div class="term-topbar-left">
684
- <a href="/terminals" class="term-back" aria-label="Back to terminals">&larr;</a>
806
+ {#if isOwner}
807
+ <a href="/terminals" class="term-back" aria-label="Back to terminals">&larr;</a>
808
+ {/if}
685
809
  <span class="term-command-name">{commandName}</span>
686
810
  <Pill text={badgeLabel} classes={badgeClass} />
687
811
  {#if isRunning}
@@ -695,7 +819,7 @@
695
819
  </Tooltip>
696
820
  {/if}
697
821
  <ConnectionStatus status={connectionStatus} onretry={handleRetry} />
698
- {#if isAI && (terminal as TerminalDetailView & { sessionFile?: string })?.sessionFile}
822
+ {#if isOwner && isAI && (terminal as TerminalDetailView & { sessionFile?: string })?.sessionFile}
699
823
  {@const sessionFile =
700
824
  (terminal as TerminalDetailView & { sessionFile?: string }).sessionFile ?? ''}
701
825
  <a
@@ -729,22 +853,31 @@
729
853
  ariaLabel="Keyboard shortcuts"
730
854
  />
731
855
 
732
- {#if isRunning}
733
- <Button
734
- classes="btn-danger btn-sm"
735
- onclick={killTerminal}
736
- disabled={killing}
737
- showLoader={killing}
738
- text="Kill"
739
- />
740
- {:else}
856
+ {#if isOwner}
741
857
  <Button
742
858
  classes="btn-secondary btn-sm"
743
- onclick={removeTerminal}
744
- disabled={removing}
745
- showLoader={removing}
746
- text="Remove"
859
+ onclick={(): void => {
860
+ shareSheetOpen = true;
861
+ }}
862
+ text="Share"
747
863
  />
864
+ {#if isRunning}
865
+ <Button
866
+ classes="btn-danger btn-sm"
867
+ onclick={killTerminal}
868
+ disabled={killing}
869
+ showLoader={killing}
870
+ text="Kill"
871
+ />
872
+ {:else}
873
+ <Button
874
+ classes="btn-secondary btn-sm"
875
+ onclick={removeTerminal}
876
+ disabled={removing}
877
+ showLoader={removing}
878
+ text="Remove"
879
+ />
880
+ {/if}
748
881
  {/if}
749
882
  </div>
750
883
  </div>
@@ -759,7 +892,7 @@
759
892
  ></div>
760
893
 
761
894
  <!-- Raw Input Bar + Quick Keys -->
762
- {#if isRunning && viewMode === 'raw'}
895
+ {#if isRunning && viewMode === 'raw' && !viewOnly}
763
896
  <div class="term-input-area">
764
897
  <QuickKeys onKey={handleQuickKey} />
765
898
  <div class="term-input-bar">
@@ -788,7 +921,7 @@
788
921
  messages={chatMessages}
789
922
  connectionState={connectionStatus}
790
923
  sessionEnded={chatSessionEnded}
791
- showInput={isRunning}
924
+ showInput={isRunning && !viewOnly}
792
925
  onSendInput={handleChatSendInput}
793
926
  onCancel={handleChatCancel}
794
927
  />
@@ -815,6 +948,14 @@
815
948
  showShortcutsHelp = false;
816
949
  }}
817
950
  />
951
+ <ShareSheet
952
+ open={shareSheetOpen}
953
+ terminalId={terminalId ?? ''}
954
+ {shareUrl}
955
+ onClose={(): void => {
956
+ shareSheetOpen = false;
957
+ }}
958
+ />
818
959
  <CommandPalette
819
960
  bind:open={showCommandPalette}
820
961
  commands={paletteCommands}
@@ -1 +0,0 @@
1
- .command-palette{--command-menu-overlay-background: rgba(0, 0, 0, .5);--command-menu-background: var(--component-bg);--command-menu-border: 1px solid var(--border);--command-menu-border-radius: var(--radius-lg);--command-menu-width: 500px;--command-menu-max-width: 90vw;--command-menu-input-color: var(--text-primary);--command-menu-input-placeholder-color: var(--text-tertiary);--command-menu-input-font-family: var(--font-mono);--command-menu-separator-color: var(--border);--command-menu-item-color: var(--text-secondary);--command-menu-item-active-background: var(--component-bg-hover);--command-menu-item-active-color: var(--text-primary);--command-menu-empty-color: var(--text-tertiary);--command-menu-z-index: 1001}.connection-status.svelte-1cg9pai{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:500;line-height:1;flex-shrink:0}.connection-status.connected.svelte-1cg9pai{color:var(--ds-green-700)}.connection-status.reconnecting.svelte-1cg9pai{color:var(--ds-amber-900)}.connection-status.disconnected.svelte-1cg9pai{color:var(--ds-red-900)}.conn-dot.svelte-1cg9pai{width:8px;height:8px;border-radius:50%;flex-shrink:0}.conn-dot.connected.svelte-1cg9pai{background:var(--ds-green-700)}.conn-dot.reconnecting.svelte-1cg9pai{background:var(--ds-amber-700);animation:pulse-dot 1.5s ease-in-out infinite}.conn-dot.disconnected.svelte-1cg9pai{background:var(--ds-red-700)}.status-label.svelte-1cg9pai{white-space:nowrap}.btn-retry{--button-height: auto;--button-padding: 2px 8px;--button-font-size: 12px;--button-border: 1px solid currentColor;--button-text-color: inherit;margin-left:2px}@media(max-width:480px){.status-label.svelte-1cg9pai{display:none}}.quick-keys.svelte-64qat5{display:flex;overflow-x:auto;gap:6px;padding:var(--space-2) var(--space-3);scrollbar-width:none;-webkit-overflow-scrolling:touch;flex-shrink:0}.quick-keys.svelte-64qat5::-webkit-scrollbar{display:none}.btn-quick-key{--button-color: var(--ds-gray-200);--button-text-color: var(--ds-gray-700);--button-border: 1px solid var(--ds-gray-400);--button-hover-color: var(--ds-gray-300);--button-hover-text-color: var(--text-primary);--button-hover-border: 1px solid var(--ds-gray-400);--button-height: 44px;--button-padding: 0 var(--space-3);--button-border-radius: var(--radius-md);--button-font-family: var(--font-mono);--button-font-size: var(--text-xs);min-width:52px;flex-shrink:0;white-space:nowrap;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation}.shortcuts-modal{--modal-content-background-color: var(--component-bg);--modal-border-radius: var(--radius-lg);--modal-header-background-color: var(--component-bg);--modal-header-padding: var(--space-4) var(--space-5);--modal-header-border-bottom: 1px solid var(--border);--header-text-size: var(--text-lg);--modal-header-text-weight: 600;--background-color: rgba(0, 0, 0, .5);--modal-z-index: 1000}.shortcuts-modal .modal-content{max-width:420px;width:90vw;min-width:320px}.shortcuts-list.svelte-1u7lstk{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-4) var(--space-5)}.shortcut-row.svelte-1u7lstk{display:flex;justify-content:space-between;align-items:center;padding:var(--space-2) 0}.shortcut-desc.svelte-1u7lstk{font-size:var(--text-sm);color:var(--text-secondary)}.shortcut-kbd{--keyboard-input-key-color: var(--text-primary);--keyboard-input-key-background: var(--ds-gray-200);--keyboard-input-key-border: 1px solid var(--ds-gray-400);--keyboard-input-key-box-shadow: 0 1px 0 var(--ds-gray-400);--keyboard-input-font-family: var(--font-mono);--keyboard-input-font-size: var(--text-xs)}.term-page.svelte-1tubujq{display:flex;flex-direction:column;height:calc(100vh - var(--header-height) - 64px);height:calc(100dvh - var(--header-height) - 64px);overflow:hidden;background:var(--ds-background-200)}.term-topbar.svelte-1tubujq{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-2) var(--space-4);background:var(--ds-background-100);border-bottom:1px solid var(--border);flex-shrink:0;min-height:48px}.term-topbar-left.svelte-1tubujq{display:flex;align-items:center;gap:var(--space-2);min-width:0;overflow:hidden}.term-topbar-right.svelte-1tubujq{display:flex;align-items:center;gap:var(--space-3);flex-shrink:1}.term-back.svelte-1tubujq{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:var(--radius-md);background:transparent;color:var(--text-secondary);text-decoration:none;font-size:18px;transition:background var(--transition-fast),color var(--transition-fast);flex-shrink:0}.term-back.svelte-1tubujq:hover{background:var(--ds-gray-alpha-100);color:var(--text-primary)}.term-command-name.svelte-1tubujq{font-family:var(--font-mono);font-size:var(--text-base);font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.term-tabs{--tabs-bar-border-bottom: none;--tabs-bar-background: var(--ds-gray-200);--tabs-bar-padding: 2px;--tabs-item-padding: 6px 12px;--tabs-item-font-size: var(--text-xs);--tabs-indicator-height: 0;--tabs-active-color: var(--text-primary);--tabs-item-color: var(--text-tertiary);border-radius:var(--radius-md);border:1px solid var(--ds-gray-400);overflow:hidden}.term-shortcuts-btn{--button-height: 28px;--button-width: 28px;--button-padding: 0;--button-border-radius: var(--radius-sm);--button-border: 1px solid var(--border);--button-color: transparent;--button-text-color: var(--text-tertiary);--button-font-size: 14px;--button-font-weight: 600;--button-hover-color: var(--component-bg-hover);--button-hover-text-color: var(--text-primary);flex-shrink:0}.term-cwd.svelte-1tubujq{font-family:var(--font-mono);font-size:11px;color:var(--text-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}.term-session-link.svelte-1tubujq{font-size:11px;font-weight:500;color:var(--ds-blue-900);text-decoration:none;white-space:nowrap;padding:2px 8px;border-radius:var(--radius-sm);border:1px solid var(--ds-blue-400);transition:background var(--transition-fast),color var(--transition-fast)}.term-session-link.svelte-1tubujq:hover{background:var(--ds-blue-100);color:var(--ds-blue-1000)}.activity-dot.svelte-1tubujq{width:8px;height:8px;border-radius:50%;flex-shrink:0}.activity-active.svelte-1tubujq{background:var(--ds-green-500);animation:activity-pulse .6s ease-in-out infinite}.activity-idle.svelte-1tubujq{background:var(--ds-gray-600)}.term-body.svelte-1tubujq{flex:1;flex-direction:column;min-height:0;overflow:hidden;padding:var(--space-1);background:var(--ds-background-200, #0a0a0f)}.term-body.svelte-1tubujq .xterm{height:100%}.term-body.svelte-1tubujq .xterm-viewport{overflow-y:auto!important}.term-body-loading.svelte-1tubujq{flex:1;padding:var(--space-4);min-height:200px}.term-chat-body.svelte-1tubujq{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}.term-input-area.svelte-1tubujq{flex-shrink:0;background:var(--ds-background-100);border-top:1px solid var(--border);padding-bottom:env(safe-area-inset-bottom,0px)}.term-input-bar.svelte-1tubujq{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3)}.term-input-field{--input-container-margin: 0;--input-height: 44px;flex:1}.btn-send{--button-height: 44px;--button-width: 44px;--button-padding: 0;--button-font-size: 18px;flex-shrink:0}.term-exited-bar.svelte-1tubujq{display:flex;align-items:center;justify-content:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);padding-bottom:calc(var(--space-2) + env(safe-area-inset-bottom,0px));background:var(--ds-gray-200);border-top:1px solid var(--border);font-size:var(--text-sm);color:var(--text-tertiary);flex-shrink:0}@media(max-width:768px){.term-topbar.svelte-1tubujq{padding:var(--space-2) var(--space-3);gap:var(--space-2)}.term-command-name.svelte-1tubujq{font-size:var(--text-sm);max-width:100px}}@media(max-width:480px){.term-topbar.svelte-1tubujq{min-height:44px;padding:var(--space-1) var(--space-2)}.term-command-name.svelte-1tubujq{max-width:80px;text-overflow:ellipsis}.term-topbar-right.svelte-1tubujq{flex-shrink:1}}