@juspay/shooter 1.19.0 → 1.21.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 (283) 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/assets/2.BHi6pjT2.css +1 -0
  5. package/build/client/_app/immutable/assets/2.BHi6pjT2.css.br +0 -0
  6. package/build/client/_app/immutable/assets/2.BHi6pjT2.css.gz +0 -0
  7. package/build/client/_app/immutable/chunks/{ZS5XYDx_.js → B1bOvemT.js} +1 -1
  8. package/build/client/_app/immutable/chunks/B1bOvemT.js.br +0 -0
  9. package/build/client/_app/immutable/chunks/{ZS5XYDx_.js.gz → B1bOvemT.js.gz} +0 -0
  10. package/build/client/_app/immutable/chunks/{DCDL_9ys.js → BmfLecb1.js} +1 -1
  11. package/build/client/_app/immutable/chunks/BmfLecb1.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/BmfLecb1.js.gz +0 -0
  13. package/build/client/_app/immutable/chunks/C87ZRWX0.js +1 -0
  14. package/build/client/_app/immutable/chunks/C87ZRWX0.js.br +0 -0
  15. package/build/client/_app/immutable/chunks/C87ZRWX0.js.gz +0 -0
  16. package/build/client/_app/immutable/chunks/{BvmdJful.js → CJulw9ux.js} +1 -1
  17. package/build/client/_app/immutable/chunks/CJulw9ux.js.br +0 -0
  18. package/build/client/_app/immutable/chunks/CJulw9ux.js.gz +0 -0
  19. package/build/client/_app/immutable/chunks/DOEXXmsh.js +3 -0
  20. package/build/client/_app/immutable/chunks/DOEXXmsh.js.br +0 -0
  21. package/build/client/_app/immutable/chunks/DOEXXmsh.js.gz +0 -0
  22. package/build/client/_app/immutable/chunks/{ClIPTXf3.js → DomZZqvG.js} +1 -1
  23. package/build/client/_app/immutable/chunks/DomZZqvG.js.br +0 -0
  24. package/build/client/_app/immutable/chunks/DomZZqvG.js.gz +0 -0
  25. package/build/client/_app/immutable/chunks/{DA4Zt9Me.js → EqMAkEha.js} +1 -1
  26. package/build/client/_app/immutable/chunks/EqMAkEha.js.br +0 -0
  27. package/build/client/_app/immutable/chunks/EqMAkEha.js.gz +0 -0
  28. package/build/client/_app/immutable/chunks/J5-Cr5oR.js +6 -0
  29. package/build/client/_app/immutable/chunks/J5-Cr5oR.js.br +0 -0
  30. package/build/client/_app/immutable/chunks/J5-Cr5oR.js.gz +0 -0
  31. package/build/client/_app/immutable/chunks/{BB2l8o4X.js → i5iZvmIH.js} +1 -1
  32. package/build/client/_app/immutable/chunks/i5iZvmIH.js.br +0 -0
  33. package/build/client/_app/immutable/chunks/i5iZvmIH.js.gz +0 -0
  34. package/build/client/_app/immutable/entry/{app.D4TXlu7A.js → app.CeSxgGat.js} +2 -2
  35. package/build/client/_app/immutable/entry/app.CeSxgGat.js.br +0 -0
  36. package/build/client/_app/immutable/entry/app.CeSxgGat.js.gz +0 -0
  37. package/build/client/_app/immutable/entry/start.DrnJFwxA.js +1 -0
  38. package/build/client/_app/immutable/entry/start.DrnJFwxA.js.br +2 -0
  39. package/build/client/_app/immutable/entry/start.DrnJFwxA.js.gz +0 -0
  40. package/build/client/_app/immutable/nodes/0.oaPwxh1O.js +10 -0
  41. package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.br +0 -0
  42. package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.gz +0 -0
  43. package/build/client/_app/immutable/nodes/{1.BVnLUSs-.js → 1.DMPyoM-M.js} +1 -1
  44. package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.br +0 -0
  45. package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.gz +0 -0
  46. package/build/client/_app/immutable/nodes/{10.D1wl2wPX.js → 10.Cbm7nQKK.js} +1 -1
  47. package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.br +0 -0
  48. package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.gz +0 -0
  49. package/build/client/_app/immutable/nodes/11.CKmZjP_a.js +2 -0
  50. package/build/client/_app/immutable/nodes/11.CKmZjP_a.js.br +0 -0
  51. package/build/client/_app/immutable/nodes/11.CKmZjP_a.js.gz +0 -0
  52. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js +13 -0
  53. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.br +0 -0
  54. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.gz +0 -0
  55. package/build/client/_app/immutable/nodes/{3.Wfz3TcJd.js → 3.BgLpGnzb.js} +1 -1
  56. package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.br +0 -0
  57. package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.gz +0 -0
  58. package/build/client/_app/immutable/nodes/{4.CBX9A3ka.js → 4.BFYS2g9C.js} +3 -3
  59. package/build/client/_app/immutable/nodes/4.BFYS2g9C.js.br +0 -0
  60. package/build/client/_app/immutable/nodes/4.BFYS2g9C.js.gz +0 -0
  61. package/build/client/_app/immutable/nodes/{5.DIVKuZc9.js → 5.Avc1-gVb.js} +1 -1
  62. package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.br +0 -0
  63. package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.gz +0 -0
  64. package/build/client/_app/immutable/nodes/{6.DtZAEPXb.js → 6.Dw2wEssJ.js} +1 -1
  65. package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.br +0 -0
  66. package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.gz +0 -0
  67. package/build/client/_app/immutable/nodes/{7.MfBRh32I.js → 7.DwKZjoBg.js} +1 -1
  68. package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.br +0 -0
  69. package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.gz +0 -0
  70. package/build/client/_app/immutable/nodes/{8.DVE6LnOC.js → 8.ZUAI6g5E.js} +1 -1
  71. package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.br +0 -0
  72. package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.gz +0 -0
  73. package/build/client/_app/immutable/nodes/{9.BCel5OqI.js → 9.I_KGXPwB.js} +1 -1
  74. package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.br +0 -0
  75. package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.gz +0 -0
  76. package/build/client/_app/version.json +1 -1
  77. package/build/client/_app/version.json.br +0 -0
  78. package/build/client/_app/version.json.gz +0 -0
  79. package/build/server/chunks/{0-DJqyZZTr.js → 0-vrTNAfZB.js} +4 -2
  80. package/build/server/chunks/0-vrTNAfZB.js.map +1 -0
  81. package/build/server/chunks/{1-2YUVen1F.js → 1-nbr-bOoF.js} +2 -2
  82. package/build/server/chunks/{1-2YUVen1F.js.map → 1-nbr-bOoF.js.map} +1 -1
  83. package/build/server/chunks/{10-D1X7LB3v.js → 10-ChyvvJ6w.js} +2 -2
  84. package/build/server/chunks/{10-D1X7LB3v.js.map → 10-ChyvvJ6w.js.map} +1 -1
  85. package/build/server/chunks/{11-qXSPdF5j.js → 11-6ZAjL3uU.js} +4 -4
  86. package/build/server/chunks/11-6ZAjL3uU.js.map +1 -0
  87. package/build/server/chunks/{2-BD7kj1mt.js → 2-DWFRVDWJ.js} +3 -3
  88. package/build/server/chunks/{2-BD7kj1mt.js.map → 2-DWFRVDWJ.js.map} +1 -1
  89. package/build/server/chunks/{3-oNjv-BhZ.js → 3-CKANM_WM.js} +2 -2
  90. package/build/server/chunks/{3-oNjv-BhZ.js.map → 3-CKANM_WM.js.map} +1 -1
  91. package/build/server/chunks/{4-Bb5VFhsO.js → 4-D92KnTmb.js} +3 -3
  92. package/build/server/chunks/{4-Bb5VFhsO.js.map → 4-D92KnTmb.js.map} +1 -1
  93. package/build/server/chunks/{5-oNoWuIsn.js → 5-BxVjs2qi.js} +2 -2
  94. package/build/server/chunks/{5-oNoWuIsn.js.map → 5-BxVjs2qi.js.map} +1 -1
  95. package/build/server/chunks/{6-DRJGUqHG.js → 6-Cbf1AAMQ.js} +2 -2
  96. package/build/server/chunks/{6-DRJGUqHG.js.map → 6-Cbf1AAMQ.js.map} +1 -1
  97. package/build/server/chunks/{7-_giJiu0L.js → 7-CMK2quEf.js} +2 -2
  98. package/build/server/chunks/{7-_giJiu0L.js.map → 7-CMK2quEf.js.map} +1 -1
  99. package/build/server/chunks/{8-zvWAVNT5.js → 8-DhdfkfDM.js} +2 -2
  100. package/build/server/chunks/{8-zvWAVNT5.js.map → 8-DhdfkfDM.js.map} +1 -1
  101. package/build/server/chunks/{9-DVyDL445.js → 9-CPpxtRM5.js} +2 -2
  102. package/build/server/chunks/{9-DVyDL445.js.map → 9-CPpxtRM5.js.map} +1 -1
  103. package/build/server/chunks/Banner-BgaAs1rs.js.map +1 -1
  104. package/build/server/chunks/Button-D0hZ7JYt.js.map +1 -1
  105. package/build/server/chunks/Icon-D0GBnDcs.js.map +1 -1
  106. package/build/server/chunks/Input-OmIiydSx.js.map +1 -1
  107. package/build/server/chunks/Pill-4xJ-VhAA.js.map +1 -1
  108. package/build/server/chunks/Shimmer-Dw2uvTC1.js.map +1 -1
  109. package/build/server/chunks/_error.svelte-CZnkxeLr.js.map +1 -1
  110. package/build/server/chunks/_layout.svelte-DfgNGGiM.js.map +1 -1
  111. package/build/server/chunks/_page.svelte-BTlfUsBp.js.map +1 -1
  112. package/build/server/chunks/_page.svelte-BX2FMgSg.js.map +1 -1
  113. package/build/server/chunks/_page.svelte-C7B0qdrC.js.map +1 -1
  114. package/build/server/chunks/_page.svelte-CE7COWnF.js.map +1 -1
  115. package/build/server/chunks/_page.svelte-CWsjjd4l.js.map +1 -1
  116. package/build/server/chunks/_page.svelte-D5S2hkBk.js.map +1 -1
  117. package/build/server/chunks/_page.svelte-D_Ey8QRG.js.map +1 -1
  118. package/build/server/chunks/{_page.svelte-BLo2v_8E.js → _page.svelte-Gv9p8nlS.js} +3 -4
  119. package/build/server/chunks/_page.svelte-Gv9p8nlS.js.map +1 -0
  120. package/build/server/chunks/{_page.svelte-BUBLUSGo.js → _page.svelte-dabsQl9c.js} +206 -5
  121. package/build/server/chunks/_page.svelte-dabsQl9c.js.map +1 -0
  122. package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +1 -1
  123. package/build/server/chunks/_server.ts-B-evHL2q.js +13 -0
  124. package/build/server/chunks/_server.ts-B-evHL2q.js.map +1 -0
  125. package/build/server/chunks/_server.ts-BB46Fbqn.js +59 -0
  126. package/build/server/chunks/_server.ts-BB46Fbqn.js.map +1 -0
  127. package/build/server/chunks/{_server.ts-DMm0hBP4.js → _server.ts-BWVlO8iV.js} +8 -5
  128. package/build/server/chunks/_server.ts-BWVlO8iV.js.map +1 -0
  129. package/build/server/chunks/{_server.ts-BRAzC6W1.js → _server.ts-BevnuePu.js} +27 -6
  130. package/build/server/chunks/_server.ts-BevnuePu.js.map +1 -0
  131. package/build/server/chunks/_server.ts-CA5KUENM.js +95 -0
  132. package/build/server/chunks/_server.ts-CA5KUENM.js.map +1 -0
  133. package/build/server/chunks/{_server.ts-DhJx0DLr.js → _server.ts-CC2K8-L2.js} +16 -7
  134. package/build/server/chunks/_server.ts-CC2K8-L2.js.map +1 -0
  135. package/build/server/chunks/{_server.ts-Bi0Oe4PF.js → _server.ts-CD7JP3fz.js} +14 -9
  136. package/build/server/chunks/_server.ts-CD7JP3fz.js.map +1 -0
  137. package/build/server/chunks/{_server.ts-C_OOUqsd.js → _server.ts-D0zRDSx0.js} +2 -2
  138. package/build/server/chunks/{_server.ts-C_OOUqsd.js.map → _server.ts-D0zRDSx0.js.map} +1 -1
  139. package/build/server/chunks/{_server.ts-DxT9IlZF.js → _server.ts-Dp-hXW_I.js} +3 -3
  140. package/build/server/chunks/{_server.ts-DxT9IlZF.js.map → _server.ts-Dp-hXW_I.js.map} +1 -1
  141. package/build/server/chunks/_server.ts-DpRr0Tfh.js +68 -0
  142. package/build/server/chunks/_server.ts-DpRr0Tfh.js.map +1 -0
  143. package/build/server/chunks/{_server.ts-Bjbr7glm.js → _server.ts-QN-Bo5ql.js} +12 -5
  144. package/build/server/chunks/_server.ts-QN-Bo5ql.js.map +1 -0
  145. package/build/server/chunks/_server.ts-VzDcFFgy.js +157 -0
  146. package/build/server/chunks/_server.ts-VzDcFFgy.js.map +1 -0
  147. package/build/server/chunks/{_server.ts-BrqaMMAa.js → _server.ts-W6i3EnGX.js} +29 -6
  148. package/build/server/chunks/_server.ts-W6i3EnGX.js.map +1 -0
  149. package/build/server/chunks/{_server.ts-CRVNEOd2.js → _server.ts-X1R7L_QI.js} +3 -3
  150. package/build/server/chunks/{_server.ts-CRVNEOd2.js.map → _server.ts-X1R7L_QI.js.map} +1 -1
  151. package/build/server/chunks/cache-BlMaDsHi.js.map +1 -1
  152. package/build/server/chunks/guest-registry-Dxvd7p-g.js +48 -0
  153. package/build/server/chunks/guest-registry-Dxvd7p-g.js.map +1 -0
  154. package/build/server/chunks/index-CoYB03g7.js.map +1 -1
  155. package/build/server/chunks/index2-dSGQ9Eaa.js.map +1 -1
  156. package/build/server/chunks/{pty-manager-41h3IK8K.js → pty-manager-ZqXqa-6A.js} +6 -2
  157. package/build/server/chunks/pty-manager-ZqXqa-6A.js.map +1 -0
  158. package/build/server/chunks/root-D4IoFC8F.js.map +1 -1
  159. package/build/server/chunks/share-auth-BS7JuiHf.js +27 -0
  160. package/build/server/chunks/share-auth-BS7JuiHf.js.map +1 -0
  161. package/build/server/chunks/share-store-B9jMpVg0.js +127 -0
  162. package/build/server/chunks/share-store-B9jMpVg0.js.map +1 -0
  163. package/build/server/chunks/state.svelte-CmHqngc_.js.map +1 -1
  164. package/build/server/chunks/stores-CRYxfF0o.js.map +1 -1
  165. package/build/server/index.js +1 -1
  166. package/build/server/index.js.map +1 -1
  167. package/build/server/manifest.js +57 -22
  168. package/build/server/manifest.js.map +1 -1
  169. package/package.json +2 -2
  170. package/server.ts +10 -3
  171. package/src/lib/modules/client/dashboard/AutopilotPanel.svelte +400 -0
  172. package/src/lib/modules/client/dashboard/index.ts +1 -0
  173. package/src/lib/modules/client/neurolink/provider-config.ts +13 -37
  174. package/src/lib/modules/client/terminal/ShareGate.svelte +96 -0
  175. package/src/lib/modules/client/terminal/ShareSheet.svelte +395 -0
  176. package/src/lib/modules/client/terminal/xterm-wrapper.ts +19 -2
  177. package/src/lib/modules/server/sessions/autopilot-engine.ts +346 -0
  178. package/src/lib/modules/server/sessions/litellm-client.ts +115 -0
  179. package/src/lib/modules/server/sessions/next-step-consensus.ts +185 -0
  180. package/src/lib/modules/server/sessions/summary-store.ts +111 -0
  181. package/src/lib/modules/server/terminal/pty-manager.ts +6 -0
  182. package/src/lib/modules/server/terminal/share-auth.ts +37 -0
  183. package/src/lib/modules/server/terminal/share-store.ts +172 -0
  184. package/src/lib/modules/server/ws/events-handler.ts +32 -0
  185. package/src/lib/modules/server/ws/guest-registry.ts +49 -0
  186. package/src/lib/modules/server/ws/server.ts +22 -3
  187. package/src/lib/modules/server/ws/session-handler.ts +18 -4
  188. package/src/lib/modules/server/ws/terminal-handler.ts +21 -2
  189. package/src/lib/modules/server/ws/ticket-store.ts +18 -10
  190. package/src/lib/types/autopilot.ts +73 -0
  191. package/src/lib/types/generated/Client.ts +25 -1
  192. package/src/lib/types/generated/Share.ts +404 -0
  193. package/src/lib/types/generated/WsProtocol.ts +73 -2
  194. package/src/lib/types/generated/index.ts +1 -0
  195. package/src/lib/types/index.ts +1 -0
  196. package/src/lib/types/terminal-client.ts +21 -2
  197. package/src/lib/types/ws.ts +1 -0
  198. package/src/routes/+layout.server.ts +2 -0
  199. package/src/routes/+layout.svelte +19 -4
  200. package/src/routes/+page.svelte +9 -1
  201. package/src/routes/api/autopilot/+server.ts +74 -0
  202. package/src/routes/api/neurolink-proxy/+server.ts +31 -6
  203. package/src/routes/api/notify/+server.ts +20 -3
  204. package/src/routes/api/summaries/+server.ts +99 -0
  205. package/src/routes/api/terminals/[id]/+server.ts +14 -3
  206. package/src/routes/api/terminals/[id]/paste-image/+server.ts +8 -4
  207. package/src/routes/api/terminals/[id]/resize/+server.ts +8 -4
  208. package/src/routes/api/terminals/[id]/share/+server.ts +98 -0
  209. package/src/routes/api/terminals/[id]/share/auth/+server.ts +81 -0
  210. package/src/routes/api/terminals/[id]/share/status/+server.ts +11 -0
  211. package/src/routes/api/ws-ticket/+server.ts +26 -5
  212. package/src/routes/terminals/[id]/+page.svelte +184 -43
  213. package/build/client/_app/immutable/assets/11.v5KA95xm.css +0 -1
  214. package/build/client/_app/immutable/assets/11.v5KA95xm.css.br +0 -0
  215. package/build/client/_app/immutable/assets/11.v5KA95xm.css.gz +0 -0
  216. package/build/client/_app/immutable/assets/2.DjiwkLqE.css +0 -1
  217. package/build/client/_app/immutable/assets/2.DjiwkLqE.css.br +0 -0
  218. package/build/client/_app/immutable/assets/2.DjiwkLqE.css.gz +0 -0
  219. package/build/client/_app/immutable/chunks/BB2l8o4X.js.br +0 -0
  220. package/build/client/_app/immutable/chunks/BB2l8o4X.js.gz +0 -0
  221. package/build/client/_app/immutable/chunks/BcqA7eKM.js +0 -3
  222. package/build/client/_app/immutable/chunks/BcqA7eKM.js.br +0 -0
  223. package/build/client/_app/immutable/chunks/BcqA7eKM.js.gz +0 -0
  224. package/build/client/_app/immutable/chunks/BvmdJful.js.br +0 -0
  225. package/build/client/_app/immutable/chunks/BvmdJful.js.gz +0 -0
  226. package/build/client/_app/immutable/chunks/CR6bkGJW.js +0 -6
  227. package/build/client/_app/immutable/chunks/CR6bkGJW.js.br +0 -0
  228. package/build/client/_app/immutable/chunks/CR6bkGJW.js.gz +0 -0
  229. package/build/client/_app/immutable/chunks/ClIPTXf3.js.br +0 -0
  230. package/build/client/_app/immutable/chunks/ClIPTXf3.js.gz +0 -0
  231. package/build/client/_app/immutable/chunks/DA4Zt9Me.js.br +0 -0
  232. package/build/client/_app/immutable/chunks/DA4Zt9Me.js.gz +0 -0
  233. package/build/client/_app/immutable/chunks/DCDL_9ys.js.br +0 -0
  234. package/build/client/_app/immutable/chunks/DCDL_9ys.js.gz +0 -0
  235. package/build/client/_app/immutable/chunks/ZS5XYDx_.js.br +0 -0
  236. package/build/client/_app/immutable/chunks/pRcLbE0d.js +0 -1
  237. package/build/client/_app/immutable/chunks/pRcLbE0d.js.br +0 -0
  238. package/build/client/_app/immutable/chunks/pRcLbE0d.js.gz +0 -0
  239. package/build/client/_app/immutable/entry/app.D4TXlu7A.js.br +0 -0
  240. package/build/client/_app/immutable/entry/app.D4TXlu7A.js.gz +0 -0
  241. package/build/client/_app/immutable/entry/start.BBQhtURO.js +0 -1
  242. package/build/client/_app/immutable/entry/start.BBQhtURO.js.br +0 -0
  243. package/build/client/_app/immutable/entry/start.BBQhtURO.js.gz +0 -0
  244. package/build/client/_app/immutable/nodes/0.1zylwAPT.js +0 -10
  245. package/build/client/_app/immutable/nodes/0.1zylwAPT.js.br +0 -0
  246. package/build/client/_app/immutable/nodes/0.1zylwAPT.js.gz +0 -0
  247. package/build/client/_app/immutable/nodes/1.BVnLUSs-.js.br +0 -0
  248. package/build/client/_app/immutable/nodes/1.BVnLUSs-.js.gz +0 -0
  249. package/build/client/_app/immutable/nodes/10.D1wl2wPX.js.br +0 -0
  250. package/build/client/_app/immutable/nodes/10.D1wl2wPX.js.gz +0 -0
  251. package/build/client/_app/immutable/nodes/11.C18nMGmp.js +0 -2
  252. package/build/client/_app/immutable/nodes/11.C18nMGmp.js.br +0 -0
  253. package/build/client/_app/immutable/nodes/11.C18nMGmp.js.gz +0 -0
  254. package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js +0 -13
  255. package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js.br +0 -0
  256. package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js.gz +0 -0
  257. package/build/client/_app/immutable/nodes/3.Wfz3TcJd.js.br +0 -0
  258. package/build/client/_app/immutable/nodes/3.Wfz3TcJd.js.gz +0 -0
  259. package/build/client/_app/immutable/nodes/4.CBX9A3ka.js.br +0 -0
  260. package/build/client/_app/immutable/nodes/4.CBX9A3ka.js.gz +0 -0
  261. package/build/client/_app/immutable/nodes/5.DIVKuZc9.js.br +0 -0
  262. package/build/client/_app/immutable/nodes/5.DIVKuZc9.js.gz +0 -0
  263. package/build/client/_app/immutable/nodes/6.DtZAEPXb.js.br +0 -0
  264. package/build/client/_app/immutable/nodes/6.DtZAEPXb.js.gz +0 -0
  265. package/build/client/_app/immutable/nodes/7.MfBRh32I.js.br +0 -0
  266. package/build/client/_app/immutable/nodes/7.MfBRh32I.js.gz +0 -0
  267. package/build/client/_app/immutable/nodes/8.DVE6LnOC.js.br +0 -0
  268. package/build/client/_app/immutable/nodes/8.DVE6LnOC.js.gz +0 -0
  269. package/build/client/_app/immutable/nodes/9.BCel5OqI.js.br +0 -0
  270. package/build/client/_app/immutable/nodes/9.BCel5OqI.js.gz +0 -0
  271. package/build/server/chunks/0-DJqyZZTr.js.map +0 -1
  272. package/build/server/chunks/11-qXSPdF5j.js.map +0 -1
  273. package/build/server/chunks/_page.svelte-BLo2v_8E.js.map +0 -1
  274. package/build/server/chunks/_page.svelte-BUBLUSGo.js.map +0 -1
  275. package/build/server/chunks/_server.ts-BRAzC6W1.js.map +0 -1
  276. package/build/server/chunks/_server.ts-Bi0Oe4PF.js.map +0 -1
  277. package/build/server/chunks/_server.ts-Bjbr7glm.js.map +0 -1
  278. package/build/server/chunks/_server.ts-BrqaMMAa.js.map +0 -1
  279. package/build/server/chunks/_server.ts-DMm0hBP4.js.map +0 -1
  280. package/build/server/chunks/_server.ts-DhJx0DLr.js.map +0 -1
  281. package/build/server/chunks/events-handler-Dm1mNPQP.js +0 -20
  282. package/build/server/chunks/events-handler-Dm1mNPQP.js.map +0 -1
  283. package/build/server/chunks/pty-manager-41h3IK8K.js.map +0 -1
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Summary Store — SQLite persistence for session summary records.
3
+ *
4
+ * Persists SessionSummaryRecord rows written by the autopilot engine after
5
+ * each summarise+consensus pipeline run. Uses better-sqlite3 with WAL journal
6
+ * mode, mirroring the pattern from terminal-store.ts.
7
+ *
8
+ * Database location: ~/.shooter/shooter.db (shared with terminal-store)
9
+ */
10
+
11
+ import type { SessionSummaryRecord } from '$lib/types';
12
+
13
+ import Database from 'better-sqlite3';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+
17
+ // ── Constants ────────────────────────────────────────────────────────
18
+
19
+ const DB_DIR = path.join(process.env.HOME || '', '.shooter');
20
+ const DB_PATH = path.join(DB_DIR, 'shooter.db');
21
+
22
+ // ── Column list ──────────────────────────────────────────────────────
23
+
24
+ const COLUMNS = [
25
+ 'id',
26
+ 'terminal_id',
27
+ 'session_id',
28
+ 'project_name',
29
+ 'summary',
30
+ 'next_steps',
31
+ 'trigger',
32
+ 'created_at',
33
+ ] as const;
34
+
35
+ // ── Row ↔ Record conversion ──────────────────────────────────────────
36
+
37
+ export class SummaryStore {
38
+ private db: Database.Database;
39
+
40
+ constructor() {
41
+ fs.mkdirSync(DB_DIR, { recursive: true });
42
+ this.db = new Database(DB_PATH);
43
+ this.db.pragma('journal_mode = WAL');
44
+ this.db.exec(`
45
+ CREATE TABLE IF NOT EXISTS session_summaries (
46
+ id TEXT PRIMARY KEY,
47
+ terminal_id TEXT,
48
+ session_id TEXT,
49
+ project_name TEXT,
50
+ summary TEXT NOT NULL,
51
+ next_steps TEXT NOT NULL DEFAULT '[]',
52
+ trigger TEXT NOT NULL,
53
+ created_at TEXT NOT NULL
54
+ )
55
+ `);
56
+ }
57
+
58
+ insert(record: SessionSummaryRecord): void {
59
+ const placeholders = COLUMNS.map(() => '?').join(', ');
60
+ this.db
61
+ .prepare(`INSERT INTO session_summaries (${COLUMNS.join(', ')}) VALUES (${placeholders})`)
62
+ .run(
63
+ record.id,
64
+ record.terminalId,
65
+ record.sessionId,
66
+ record.projectName,
67
+ record.summary,
68
+ record.nextSteps,
69
+ record.trigger,
70
+ record.createdAt
71
+ );
72
+ }
73
+
74
+ listRecent(limit: number, sessionId?: string): SessionSummaryRecord[] {
75
+ if (sessionId) {
76
+ const rows = this.db
77
+ .prepare(
78
+ 'SELECT * FROM session_summaries WHERE session_id = ? ORDER BY created_at DESC LIMIT ?'
79
+ )
80
+ .all(sessionId, limit) as Record<string, unknown>[];
81
+ return rows.map(rowToRecord);
82
+ }
83
+ const rows = this.db
84
+ .prepare('SELECT * FROM session_summaries ORDER BY created_at DESC LIMIT ?')
85
+ .all(limit) as Record<string, unknown>[];
86
+ return rows.map(rowToRecord);
87
+ }
88
+ }
89
+
90
+ // ── SummaryStore ─────────────────────────────────────────────────────
91
+
92
+ function rowToRecord(row: Record<string, unknown>): SessionSummaryRecord {
93
+ return {
94
+ createdAt: row.created_at as string,
95
+ id: row.id as string,
96
+ nextSteps: row.next_steps as string,
97
+ projectName: (row.project_name as string) ?? null,
98
+ sessionId: (row.session_id as string) ?? null,
99
+ summary: row.summary as string,
100
+ terminalId: (row.terminal_id as string) ?? null,
101
+ trigger: row.trigger as string,
102
+ };
103
+ }
104
+
105
+ // ── Singleton ────────────────────────────────────────────────────────
106
+ // Shared instance across module loaders (same pattern as terminal-store).
107
+
108
+ const SS_GLOBAL_KEY = '__shooter_summary_store';
109
+ export const summaryStore: SummaryStore =
110
+ ((globalThis as Record<string, unknown>)[SS_GLOBAL_KEY] as SummaryStore) || new SummaryStore();
111
+ (globalThis as Record<string, unknown>)[SS_GLOBAL_KEY] = summaryStore;
@@ -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;
@@ -13,6 +13,18 @@ const eventsClients: Set<WebSocket> =
13
13
  ((globalThis as Record<string, unknown>)[EVENTS_KEY] as Set<WebSocket>) || new Set<WebSocket>();
14
14
  (globalThis as Record<string, unknown>)[EVENTS_KEY] = eventsClients;
15
15
 
16
+ /**
17
+ * Server-side listeners (e.g. the always-on autopilot engine) that observe
18
+ * every broadcast event. Kept on globalThis so the listener registered from the
19
+ * server.ts module graph and the broadcaster called from the bundled SvelteKit
20
+ * handler graph share one set.
21
+ */
22
+ const LISTENERS_KEY = '__shooter_ws_event_listeners';
23
+ const eventListeners: Set<(event: ShooterEvent) => void> =
24
+ ((globalThis as Record<string, unknown>)[LISTENERS_KEY] as Set<(event: ShooterEvent) => void>) ||
25
+ new Set<(event: ShooterEvent) => void>();
26
+ (globalThis as Record<string, unknown>)[LISTENERS_KEY] = eventListeners;
27
+
16
28
  // ── Handlers ────────────────────────────────────────────────────────
17
29
 
18
30
  /**
@@ -31,6 +43,15 @@ export function broadcastEvent(event: ShooterEvent): void {
31
43
  ws.send(data);
32
44
  }
33
45
  }
46
+
47
+ // Fan out to server-side listeners (autopilot engine, etc.).
48
+ for (const listener of eventListeners) {
49
+ try {
50
+ listener(event);
51
+ } catch {
52
+ // a faulty listener must never break the broadcast
53
+ }
54
+ }
34
55
  }
35
56
 
36
57
  /**
@@ -62,3 +83,14 @@ export function handleEventsConnection(ws: WebSocket): void {
62
83
  eventsClients.delete(ws);
63
84
  });
64
85
  }
86
+
87
+ /**
88
+ * Register a server-side listener that receives every broadcast ShooterEvent.
89
+ * Returns an unsubscribe function. Used by the always-on autopilot engine.
90
+ */
91
+ export function onShooterEvent(listener: (event: ShooterEvent) => void): () => void {
92
+ eventListeners.add(listener);
93
+ return (): void => {
94
+ eventListeners.delete(listener);
95
+ };
96
+ }
@@ -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).