@juspay/shooter 1.0.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 (327) hide show
  1. package/.claude/hooks/notifier.cjs +1431 -0
  2. package/.claude/settings.json +162 -0
  3. package/README.md +515 -0
  4. package/bin/shooter.cjs +141 -0
  5. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css +1 -0
  6. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.br +0 -0
  7. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.gz +0 -0
  8. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css +1 -0
  9. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.br +1 -0
  10. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.gz +0 -0
  11. package/build/client/_app/immutable/assets/3.C0uFg0IS.css +1 -0
  12. package/build/client/_app/immutable/assets/3.C0uFg0IS.css.br +0 -0
  13. package/build/client/_app/immutable/assets/3.C0uFg0IS.css.gz +0 -0
  14. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css +1 -0
  15. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.br +0 -0
  16. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.gz +0 -0
  17. package/build/client/_app/immutable/assets/5.DRjApZQW.css +1 -0
  18. package/build/client/_app/immutable/assets/5.DRjApZQW.css.br +0 -0
  19. package/build/client/_app/immutable/assets/5.DRjApZQW.css.gz +0 -0
  20. package/build/client/_app/immutable/assets/6.AraZrY8I.css +1 -0
  21. package/build/client/_app/immutable/assets/6.AraZrY8I.css.br +0 -0
  22. package/build/client/_app/immutable/assets/6.AraZrY8I.css.gz +0 -0
  23. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css +1 -0
  24. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.br +0 -0
  25. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.gz +0 -0
  26. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css +1 -0
  27. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.br +0 -0
  28. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.gz +0 -0
  29. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css +1 -0
  30. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.br +0 -0
  31. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.gz +0 -0
  32. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css +1 -0
  33. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.br +0 -0
  34. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.gz +0 -0
  35. package/build/client/_app/immutable/chunks/BNJphC1q.js +56 -0
  36. package/build/client/_app/immutable/chunks/BNJphC1q.js.br +0 -0
  37. package/build/client/_app/immutable/chunks/BNJphC1q.js.gz +0 -0
  38. package/build/client/_app/immutable/chunks/BTGVxaYV.js +9 -0
  39. package/build/client/_app/immutable/chunks/BTGVxaYV.js.br +0 -0
  40. package/build/client/_app/immutable/chunks/BTGVxaYV.js.gz +0 -0
  41. package/build/client/_app/immutable/chunks/BlxrFPDK.js +1 -0
  42. package/build/client/_app/immutable/chunks/BlxrFPDK.js.br +0 -0
  43. package/build/client/_app/immutable/chunks/BlxrFPDK.js.gz +0 -0
  44. package/build/client/_app/immutable/chunks/Bvk7mfPM.js +1 -0
  45. package/build/client/_app/immutable/chunks/Bvk7mfPM.js.br +0 -0
  46. package/build/client/_app/immutable/chunks/Bvk7mfPM.js.gz +0 -0
  47. package/build/client/_app/immutable/chunks/CAokzuPQ.js +1 -0
  48. package/build/client/_app/immutable/chunks/CAokzuPQ.js.br +0 -0
  49. package/build/client/_app/immutable/chunks/CAokzuPQ.js.gz +0 -0
  50. package/build/client/_app/immutable/chunks/CGLrx-H5.js +1 -0
  51. package/build/client/_app/immutable/chunks/CGLrx-H5.js.br +0 -0
  52. package/build/client/_app/immutable/chunks/CGLrx-H5.js.gz +0 -0
  53. package/build/client/_app/immutable/chunks/CgCpWzEA.js +1 -0
  54. package/build/client/_app/immutable/chunks/CgCpWzEA.js.br +0 -0
  55. package/build/client/_app/immutable/chunks/CgCpWzEA.js.gz +0 -0
  56. package/build/client/_app/immutable/chunks/Cjwk_cGO.js +6 -0
  57. package/build/client/_app/immutable/chunks/Cjwk_cGO.js.br +0 -0
  58. package/build/client/_app/immutable/chunks/Cjwk_cGO.js.gz +0 -0
  59. package/build/client/_app/immutable/chunks/CtQ8EED1.js +11 -0
  60. package/build/client/_app/immutable/chunks/CtQ8EED1.js.br +0 -0
  61. package/build/client/_app/immutable/chunks/CtQ8EED1.js.gz +0 -0
  62. package/build/client/_app/immutable/chunks/DERQCisl.js +1 -0
  63. package/build/client/_app/immutable/chunks/DERQCisl.js.br +0 -0
  64. package/build/client/_app/immutable/chunks/DERQCisl.js.gz +0 -0
  65. package/build/client/_app/immutable/chunks/DKrg8TQs.js +1 -0
  66. package/build/client/_app/immutable/chunks/DKrg8TQs.js.br +0 -0
  67. package/build/client/_app/immutable/chunks/DKrg8TQs.js.gz +0 -0
  68. package/build/client/_app/immutable/chunks/DLu6yJIZ.js +1 -0
  69. package/build/client/_app/immutable/chunks/DLu6yJIZ.js.br +0 -0
  70. package/build/client/_app/immutable/chunks/DLu6yJIZ.js.gz +0 -0
  71. package/build/client/_app/immutable/chunks/Dkkpz_4D.js +126 -0
  72. package/build/client/_app/immutable/chunks/Dkkpz_4D.js.br +0 -0
  73. package/build/client/_app/immutable/chunks/Dkkpz_4D.js.gz +0 -0
  74. package/build/client/_app/immutable/chunks/DoczjQhA.js +1 -0
  75. package/build/client/_app/immutable/chunks/DoczjQhA.js.br +0 -0
  76. package/build/client/_app/immutable/chunks/DoczjQhA.js.gz +0 -0
  77. package/build/client/_app/immutable/chunks/PPVm8Dsz.js +1 -0
  78. package/build/client/_app/immutable/chunks/PPVm8Dsz.js.br +0 -0
  79. package/build/client/_app/immutable/chunks/PPVm8Dsz.js.gz +0 -0
  80. package/build/client/_app/immutable/chunks/RpcNruLP.js +2 -0
  81. package/build/client/_app/immutable/chunks/RpcNruLP.js.br +0 -0
  82. package/build/client/_app/immutable/chunks/RpcNruLP.js.gz +0 -0
  83. package/build/client/_app/immutable/chunks/a-St0Zwo.js +1 -0
  84. package/build/client/_app/immutable/chunks/a-St0Zwo.js.br +0 -0
  85. package/build/client/_app/immutable/chunks/a-St0Zwo.js.gz +0 -0
  86. package/build/client/_app/immutable/chunks/bo70OQUZ.js +1 -0
  87. package/build/client/_app/immutable/chunks/bo70OQUZ.js.br +0 -0
  88. package/build/client/_app/immutable/chunks/bo70OQUZ.js.gz +0 -0
  89. package/build/client/_app/immutable/entry/app.QvGgdvTI.js +2 -0
  90. package/build/client/_app/immutable/entry/app.QvGgdvTI.js.br +0 -0
  91. package/build/client/_app/immutable/entry/app.QvGgdvTI.js.gz +0 -0
  92. package/build/client/_app/immutable/entry/start.BntDNRMC.js +1 -0
  93. package/build/client/_app/immutable/entry/start.BntDNRMC.js.br +0 -0
  94. package/build/client/_app/immutable/entry/start.BntDNRMC.js.gz +0 -0
  95. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js +1 -0
  96. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.br +0 -0
  97. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.gz +0 -0
  98. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js +1 -0
  99. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.br +0 -0
  100. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.gz +0 -0
  101. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js +1 -0
  102. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.br +0 -0
  103. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.gz +0 -0
  104. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js +3 -0
  105. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.br +0 -0
  106. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.gz +0 -0
  107. package/build/client/_app/immutable/nodes/4.D-cIe70D.js +1 -0
  108. package/build/client/_app/immutable/nodes/4.D-cIe70D.js.br +0 -0
  109. package/build/client/_app/immutable/nodes/4.D-cIe70D.js.gz +0 -0
  110. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js +1 -0
  111. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.br +0 -0
  112. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.gz +0 -0
  113. package/build/client/_app/immutable/nodes/6.BB7QE48r.js +2 -0
  114. package/build/client/_app/immutable/nodes/6.BB7QE48r.js.br +0 -0
  115. package/build/client/_app/immutable/nodes/6.BB7QE48r.js.gz +0 -0
  116. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js +2 -0
  117. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.br +0 -0
  118. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.gz +0 -0
  119. package/build/client/_app/version.json +1 -0
  120. package/build/client/_app/version.json.br +0 -0
  121. package/build/client/_app/version.json.gz +0 -0
  122. package/build/client/app-icon.png +0 -0
  123. package/build/client/apple-touch-icon.png +0 -0
  124. package/build/client/favicon.png +0 -0
  125. package/build/client/favicon.svg +10 -0
  126. package/build/client/favicon.svg.br +0 -0
  127. package/build/client/favicon.svg.gz +0 -0
  128. package/build/client/manifest.webmanifest +1 -0
  129. package/build/client/pwa-192x192.png +0 -0
  130. package/build/client/pwa-512x512.png +0 -0
  131. package/build/client/registerSW.js +1 -0
  132. package/build/client/registerSW.js.br +0 -0
  133. package/build/client/registerSW.js.gz +0 -0
  134. package/build/client/sw.js +222 -0
  135. package/build/client/sw.js.br +0 -0
  136. package/build/client/sw.js.gz +0 -0
  137. package/build/client/workbox-5119daf5.js +3395 -0
  138. package/build/client/workbox-5119daf5.js.br +0 -0
  139. package/build/client/workbox-5119daf5.js.gz +0 -0
  140. package/build/env.js +94 -0
  141. package/build/handler.js +1494 -0
  142. package/build/index.js +345 -0
  143. package/build/pty-holder.cjs +510 -0
  144. package/build/server/chunks/0-q2IUp76Y.js +9 -0
  145. package/build/server/chunks/0-q2IUp76Y.js.map +1 -0
  146. package/build/server/chunks/1-CU50G5wZ.js +9 -0
  147. package/build/server/chunks/1-CU50G5wZ.js.map +1 -0
  148. package/build/server/chunks/2-D01t9s8T.js +9 -0
  149. package/build/server/chunks/2-D01t9s8T.js.map +1 -0
  150. package/build/server/chunks/3-5PUQ04wC.js +9 -0
  151. package/build/server/chunks/3-5PUQ04wC.js.map +1 -0
  152. package/build/server/chunks/4-e7gywnSG.js +9 -0
  153. package/build/server/chunks/4-e7gywnSG.js.map +1 -0
  154. package/build/server/chunks/5-CA1SA6KZ.js +9 -0
  155. package/build/server/chunks/5-CA1SA6KZ.js.map +1 -0
  156. package/build/server/chunks/6-71H221sV.js +9 -0
  157. package/build/server/chunks/6-71H221sV.js.map +1 -0
  158. package/build/server/chunks/7-Bo-vmdyz.js +9 -0
  159. package/build/server/chunks/7-Bo-vmdyz.js.map +1 -0
  160. package/build/server/chunks/_layout.svelte-SFHOxs74.js +132 -0
  161. package/build/server/chunks/_layout.svelte-SFHOxs74.js.map +1 -0
  162. package/build/server/chunks/_page.svelte-B4w-2wD-.js +120 -0
  163. package/build/server/chunks/_page.svelte-B4w-2wD-.js.map +1 -0
  164. package/build/server/chunks/_page.svelte-B_qAXjkh.js +213 -0
  165. package/build/server/chunks/_page.svelte-B_qAXjkh.js.map +1 -0
  166. package/build/server/chunks/_page.svelte-CsF1_TRG.js +50 -0
  167. package/build/server/chunks/_page.svelte-CsF1_TRG.js.map +1 -0
  168. package/build/server/chunks/_page.svelte-DJC6U-P0.js +68 -0
  169. package/build/server/chunks/_page.svelte-DJC6U-P0.js.map +1 -0
  170. package/build/server/chunks/_page.svelte-DQ6HBtsz.js +407 -0
  171. package/build/server/chunks/_page.svelte-DQ6HBtsz.js.map +1 -0
  172. package/build/server/chunks/_page.svelte-LbhhjP21.js +148 -0
  173. package/build/server/chunks/_page.svelte-LbhhjP21.js.map +1 -0
  174. package/build/server/chunks/_server.ts-BL2FGb5Z.js +387 -0
  175. package/build/server/chunks/_server.ts-BL2FGb5Z.js.map +1 -0
  176. package/build/server/chunks/_server.ts-BgdjBZco.js +47 -0
  177. package/build/server/chunks/_server.ts-BgdjBZco.js.map +1 -0
  178. package/build/server/chunks/_server.ts-BihKSdj_.js +59 -0
  179. package/build/server/chunks/_server.ts-BihKSdj_.js.map +1 -0
  180. package/build/server/chunks/_server.ts-BjOJsoy4.js +63 -0
  181. package/build/server/chunks/_server.ts-BjOJsoy4.js.map +1 -0
  182. package/build/server/chunks/_server.ts-C29xzfaw.js +77 -0
  183. package/build/server/chunks/_server.ts-C29xzfaw.js.map +1 -0
  184. package/build/server/chunks/_server.ts-CPa6DgIt.js +71 -0
  185. package/build/server/chunks/_server.ts-CPa6DgIt.js.map +1 -0
  186. package/build/server/chunks/_server.ts-CbDRDIoP.js +36 -0
  187. package/build/server/chunks/_server.ts-CbDRDIoP.js.map +1 -0
  188. package/build/server/chunks/_server.ts-Cl1OEWL4.js +54 -0
  189. package/build/server/chunks/_server.ts-Cl1OEWL4.js.map +1 -0
  190. package/build/server/chunks/_server.ts-ColfDHW8.js +60 -0
  191. package/build/server/chunks/_server.ts-ColfDHW8.js.map +1 -0
  192. package/build/server/chunks/_server.ts-Cv_OrRuL.js +494 -0
  193. package/build/server/chunks/_server.ts-Cv_OrRuL.js.map +1 -0
  194. package/build/server/chunks/_server.ts-D4MNi4cD.js +25 -0
  195. package/build/server/chunks/_server.ts-D4MNi4cD.js.map +1 -0
  196. package/build/server/chunks/_server.ts-DRVbgm6k.js +125 -0
  197. package/build/server/chunks/_server.ts-DRVbgm6k.js.map +1 -0
  198. package/build/server/chunks/_server.ts-DfajWaqh.js +39 -0
  199. package/build/server/chunks/_server.ts-DfajWaqh.js.map +1 -0
  200. package/build/server/chunks/_server.ts-y9-WYDMa.js +35 -0
  201. package/build/server/chunks/_server.ts-y9-WYDMa.js.map +1 -0
  202. package/build/server/chunks/auth-CEgFis71.js +32 -0
  203. package/build/server/chunks/auth-CEgFis71.js.map +1 -0
  204. package/build/server/chunks/client-CxCatAKr.js +255 -0
  205. package/build/server/chunks/client-CxCatAKr.js.map +1 -0
  206. package/build/server/chunks/error.svelte-BqdwMWdK.js +26 -0
  207. package/build/server/chunks/error.svelte-BqdwMWdK.js.map +1 -0
  208. package/build/server/chunks/exports-CJ0Q5XmL.js +4081 -0
  209. package/build/server/chunks/exports-CJ0Q5XmL.js.map +1 -0
  210. package/build/server/chunks/index2-DAxIoAO-.js +36 -0
  211. package/build/server/chunks/index2-DAxIoAO-.js.map +1 -0
  212. package/build/server/chunks/jsonl-parser-dmZU_Hyu.js +137 -0
  213. package/build/server/chunks/jsonl-parser-dmZU_Hyu.js.map +1 -0
  214. package/build/server/chunks/library-apns-BHxLmuIx.js +104 -0
  215. package/build/server/chunks/library-apns-BHxLmuIx.js.map +1 -0
  216. package/build/server/chunks/markdown-Bxrl3cCF.js +1241 -0
  217. package/build/server/chunks/markdown-Bxrl3cCF.js.map +1 -0
  218. package/build/server/chunks/pending-requests-D8UiTw7L.js +44 -0
  219. package/build/server/chunks/pending-requests-D8UiTw7L.js.map +1 -0
  220. package/build/server/chunks/pty-manager-C0FhBiVq.js +1697 -0
  221. package/build/server/chunks/pty-manager-C0FhBiVq.js.map +1 -0
  222. package/build/server/chunks/shared-server-BDY8jh20.js +200 -0
  223. package/build/server/chunks/shared-server-BDY8jh20.js.map +1 -0
  224. package/build/server/chunks/stores-D0HorpgL.js +36 -0
  225. package/build/server/chunks/stores-D0HorpgL.js.map +1 -0
  226. package/build/server/index.js +6466 -0
  227. package/build/server/index.js.map +1 -0
  228. package/build/server/manifest.js +184 -0
  229. package/build/server/manifest.js.map +1 -0
  230. package/build/shims.js +32 -0
  231. package/package.json +94 -0
  232. package/scripts/clipboard-shims/wl-paste +48 -0
  233. package/scripts/clipboard-shims/xclip +31 -0
  234. package/scripts/install.sh +477 -0
  235. package/scripts/setup-node-pty.sh +63 -0
  236. package/scripts/setup.cjs +571 -0
  237. package/scripts/test-runner.ts +243 -0
  238. package/scripts/vercel-env-commands.sh +60 -0
  239. package/server.ts +139 -0
  240. package/src/app.css +1835 -0
  241. package/src/app.d.ts +31 -0
  242. package/src/app.html +24 -0
  243. package/src/generated/types/APN.ts +305 -0
  244. package/src/generated/types/CLI.ts +52 -0
  245. package/src/generated/types/JWT.ts +92 -0
  246. package/src/generated/types/Terminal.ts +2736 -0
  247. package/src/generated/types/index.ts +6 -0
  248. package/src/lib/assets/icons/alert-triangle.svg +5 -0
  249. package/src/lib/assets/icons/bell.svg +4 -0
  250. package/src/lib/assets/icons/check-circle.svg +4 -0
  251. package/src/lib/assets/icons/file.svg +4 -0
  252. package/src/lib/assets/icons/folder.svg +3 -0
  253. package/src/lib/assets/icons/play.svg +3 -0
  254. package/src/lib/assets/icons/refresh.svg +4 -0
  255. package/src/lib/assets/icons/settings.svg +4 -0
  256. package/src/lib/assets/icons/terminal.svg +1 -0
  257. package/src/lib/assets/icons/tool.svg +3 -0
  258. package/src/lib/assets/icons/x-circle.svg +5 -0
  259. package/src/lib/modules/client/common/Card.svelte +26 -0
  260. package/src/lib/modules/client/common/EmptyState.svelte +36 -0
  261. package/src/lib/modules/client/common/Icon.svelte +61 -0
  262. package/src/lib/modules/client/common/StatusBadge.svelte +38 -0
  263. package/src/lib/modules/client/common/cache.ts +31 -0
  264. package/src/lib/modules/client/common/config-guard.ts +18 -0
  265. package/src/lib/modules/client/common/index.ts +12 -0
  266. package/src/lib/modules/client/common/markdown.ts +23 -0
  267. package/src/lib/modules/client/common/native-bridge.ts +50 -0
  268. package/src/lib/modules/client/common/time.ts +22 -0
  269. package/src/lib/modules/client/common/tool-title.ts +28 -0
  270. package/src/lib/modules/client/terminal/ChatView.svelte +400 -0
  271. package/src/lib/modules/client/terminal/CommandPalette.svelte +60 -0
  272. package/src/lib/modules/client/terminal/ConnectionStatus.svelte +99 -0
  273. package/src/lib/modules/client/terminal/LaunchSheet.svelte +294 -0
  274. package/src/lib/modules/client/terminal/QuickKeys.svelte +71 -0
  275. package/src/lib/modules/client/terminal/ShortcutsHelp.svelte +79 -0
  276. package/src/lib/modules/client/terminal/keyboard-shortcuts.ts +70 -0
  277. package/src/lib/modules/client/terminal/xterm-wrapper.ts +243 -0
  278. package/src/lib/modules/server/apn/library-apns.ts +137 -0
  279. package/src/lib/modules/server/apn/notification-history.ts +35 -0
  280. package/src/lib/modules/server/apn/notification-sessions.ts +117 -0
  281. package/src/lib/modules/server/apn/pending-requests.ts +65 -0
  282. package/src/lib/modules/server/apn/types.ts +51 -0
  283. package/src/lib/modules/server/auth.ts +34 -0
  284. package/src/lib/modules/server/cli/index.ts +79 -0
  285. package/src/lib/modules/server/cli/runner.ts +162 -0
  286. package/src/lib/modules/server/fcm/fcm-service.ts +72 -0
  287. package/src/lib/modules/server/sessions/jsonl-parser.ts +197 -0
  288. package/src/lib/modules/server/sessions/jsonl-reader.ts +301 -0
  289. package/src/lib/modules/server/sessions/opencode-reader.ts +264 -0
  290. package/src/lib/modules/server/sessions/types.ts +53 -0
  291. package/src/lib/modules/server/terminal/holder-client.ts +273 -0
  292. package/src/lib/modules/server/terminal/opencode-watcher.ts +661 -0
  293. package/src/lib/modules/server/terminal/pty-holder.cjs +510 -0
  294. package/src/lib/modules/server/terminal/pty-manager.ts +1012 -0
  295. package/src/lib/modules/server/terminal/session-watcher.ts +320 -0
  296. package/src/lib/modules/server/terminal/terminal-store.ts +198 -0
  297. package/src/lib/modules/server/ws/events-handler.ts +73 -0
  298. package/src/lib/modules/server/ws/keepalive.ts +108 -0
  299. package/src/lib/modules/server/ws/server.ts +93 -0
  300. package/src/lib/modules/server/ws/session-handler.ts +462 -0
  301. package/src/lib/modules/server/ws/terminal-handler.ts +197 -0
  302. package/src/lib/modules/server/ws/ticket-store.ts +58 -0
  303. package/src/lib/theme.css +529 -0
  304. package/src/lib/types/config.ts +6 -0
  305. package/src/routes/+layout.svelte +218 -0
  306. package/src/routes/+page.svelte +261 -0
  307. package/src/routes/api/debug/+server.ts +33 -0
  308. package/src/routes/api/device-token/+server.ts +85 -0
  309. package/src/routes/api/health/+server.ts +100 -0
  310. package/src/routes/api/notify/+server.ts +418 -0
  311. package/src/routes/api/qr-config/+server.ts +45 -0
  312. package/src/routes/api/response/+server.ts +73 -0
  313. package/src/routes/api/sessions/+server.ts +120 -0
  314. package/src/routes/api/terminals/+server.ts +141 -0
  315. package/src/routes/api/terminals/[id]/+server.ts +75 -0
  316. package/src/routes/api/terminals/[id]/paste-image/+server.ts +61 -0
  317. package/src/routes/api/terminals/[id]/resize/+server.ts +60 -0
  318. package/src/routes/api/webhook/+server.ts +42 -0
  319. package/src/routes/api/ws-status/+server.ts +23 -0
  320. package/src/routes/api/ws-ticket/+server.ts +86 -0
  321. package/src/routes/config/+page.svelte +600 -0
  322. package/src/routes/project/+page.svelte +274 -0
  323. package/src/routes/session/[id]/+page.svelte +434 -0
  324. package/src/routes/terminals/+page.svelte +618 -0
  325. package/src/routes/terminals/[id]/+page.svelte +968 -0
  326. package/svelte.config.js +18 -0
  327. package/tsconfig.json +14 -0
@@ -0,0 +1,218 @@
1
+ <script lang="ts">
2
+ import '../app.css';
3
+ import '$lib/theme.css';
4
+ import { goto } from '$app/navigation';
5
+ import { page } from '$app/stores';
6
+ import { Button } from '@juspay/svelte-ui-components';
7
+ import { StatusBadge } from '$lib/modules/client/common';
8
+ import { onMount, type Snippet } from 'svelte';
9
+
10
+ const { children }: { children: Snippet } = $props();
11
+
12
+ let systemStatus = $state<'degraded' | 'error' | 'healthy' | 'unknown'>('unknown');
13
+
14
+ onMount(() => {
15
+ void checkSystemStatus();
16
+ const interval = setInterval(() => {
17
+ void checkSystemStatus();
18
+ }, 30000);
19
+ return (): void => {
20
+ clearInterval(interval);
21
+ };
22
+ });
23
+
24
+ async function checkSystemStatus(): Promise<void> {
25
+ try {
26
+ const response = await fetch('/api/health');
27
+ if (!response.ok) {
28
+ systemStatus = 'error';
29
+ return;
30
+ }
31
+ const data = (await response.json()) as { status?: string };
32
+ systemStatus =
33
+ data.status === 'healthy' || data.status === 'degraded' || data.status === 'error'
34
+ ? data.status
35
+ : 'unknown';
36
+ } catch {
37
+ systemStatus = 'error';
38
+ }
39
+ }
40
+ </script>
41
+
42
+ <div class="app">
43
+ <!-- Top bar: Logo + Status + Gear -->
44
+ <header class="header">
45
+ <div class="header-content">
46
+ <a href="/" class="logo">
47
+ <img src="/app-icon.png" alt="Shooter" class="logo-icon" width="24" height="24" />
48
+ <span class="logo-text">Shooter</span>
49
+ </a>
50
+
51
+ <div class="nav-right">
52
+ <StatusBadge status={systemStatus} />
53
+ <Button
54
+ classes="btn-gear {$page.url.pathname === '/config' ? 'btn-gear-active' : ''}"
55
+ onclick={() => goto('/config')}
56
+ aria-label="Settings"
57
+ >
58
+ {#snippet children()}
59
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
60
+ <circle cx="12" cy="12" r="3"></circle>
61
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
62
+ </svg>
63
+ {/snippet}
64
+ </Button>
65
+ </div>
66
+ </div>
67
+ </header>
68
+
69
+ <!-- Scrollable content area -->
70
+ <div class="content-area">
71
+ {@render children()}
72
+ </div>
73
+
74
+ <!-- Bottom tab bar: Projects + Terminals -->
75
+ <nav class="bottom-tabs">
76
+ <div class="bottom-tabs-inner">
77
+ <a
78
+ href="/"
79
+ class="tab-item"
80
+ class:active={$page.url.pathname === '/' || $page.url.pathname.startsWith('/project') || $page.url.pathname.startsWith('/session')}
81
+ >
82
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
83
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
84
+ </svg>
85
+ <span>Projects</span>
86
+ </a>
87
+ <a
88
+ href="/terminals"
89
+ class="tab-item"
90
+ class:active={$page.url.pathname.startsWith('/terminals')}
91
+ >
92
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
93
+ <polyline points="4 17 10 11 4 5"></polyline>
94
+ <line x1="12" y1="19" x2="20" y2="19"></line>
95
+ </svg>
96
+ <span>Terminals</span>
97
+ </a>
98
+ </div>
99
+ </nav>
100
+ </div>
101
+
102
+ <style>
103
+ .nav-right {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: var(--space-2);
107
+ }
108
+
109
+ :global(.btn-gear) {
110
+ --button-color: transparent;
111
+ --button-text-color: var(--text-muted);
112
+ --button-hover-color: var(--component-bg);
113
+ --button-hover-text-color: var(--text-primary);
114
+ --button-border: none;
115
+ --button-hover-border: none;
116
+ --button-padding: 0;
117
+ --button-height: 36px;
118
+ --button-width: 36px;
119
+ --button-border-radius: var(--radius-md);
120
+ }
121
+ :global(.btn-gear-active) {
122
+ --button-color: var(--component-bg);
123
+ --button-text-color: var(--text-primary);
124
+ }
125
+
126
+ .content-area {
127
+ flex: 1;
128
+ overflow-y: auto;
129
+ -webkit-overflow-scrolling: touch;
130
+ }
131
+
132
+ .bottom-tabs {
133
+ height: 64px;
134
+ background: var(--background);
135
+ border-top: 1px solid var(--border);
136
+ flex-shrink: 0;
137
+ z-index: 100;
138
+ padding-bottom: env(safe-area-inset-bottom, 0);
139
+ }
140
+ .bottom-tabs-inner {
141
+ max-width: 600px;
142
+ margin: 0 auto;
143
+ padding: 6px var(--space-4) 4px;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: space-around;
147
+ height: 100%;
148
+ box-sizing: border-box;
149
+ }
150
+ .tab-item {
151
+ display: flex;
152
+ flex-direction: column;
153
+ align-items: center;
154
+ justify-content: center;
155
+ gap: 4px;
156
+ color: var(--text-muted);
157
+ font-size: 11px;
158
+ font-weight: 500;
159
+ text-decoration: none;
160
+ padding: 6px 36px;
161
+ border-radius: var(--radius-md);
162
+ transition: color var(--transition-fast);
163
+ user-select: none;
164
+ -webkit-tap-highlight-color: transparent;
165
+ min-height: 48px;
166
+ }
167
+ .tab-item:hover {
168
+ color: var(--text-secondary);
169
+ }
170
+ .tab-item.active {
171
+ color: var(--ds-green-700);
172
+ }
173
+ .tab-item :global(svg) {
174
+ width: 26px;
175
+ height: 26px;
176
+ flex-shrink: 0;
177
+ }
178
+
179
+ /* Mobile: page headers stack vertically, buttons wrap */
180
+ @media (max-width: 480px) {
181
+ :global(.btn-gear) {
182
+ --button-height: 44px;
183
+ --button-width: 44px;
184
+ }
185
+ .bottom-tabs {
186
+ height: 60px;
187
+ }
188
+ .tab-item {
189
+ padding: 6px 28px;
190
+ font-size: 10px;
191
+ gap: 3px;
192
+ min-height: 44px;
193
+ }
194
+ .tab-item :global(svg) {
195
+ width: 24px;
196
+ height: 24px;
197
+ }
198
+
199
+ :global(.status-badge) {
200
+ font-size: 10px;
201
+ padding: 0 8px;
202
+ height: 22px;
203
+ }
204
+
205
+ /* Fix squashed buttons on mobile */
206
+ :global(.page-header) {
207
+ flex-direction: column !important;
208
+ gap: var(--space-3) !important;
209
+ }
210
+ :global(.page-header .btn-group) {
211
+ width: 100%;
212
+ }
213
+ :global(.page-header .btn-group .btn) {
214
+ flex: 1;
215
+ justify-content: center;
216
+ }
217
+ }
218
+ </style>
@@ -0,0 +1,261 @@
1
+ <script lang="ts">
2
+ import type { ShooterConfig } from '$lib/types/config';
3
+
4
+ import { goto } from '$app/navigation';
5
+ import { Button, Pill, Shimmer } from '@juspay/svelte-ui-components';
6
+ import {
7
+ EmptyState,
8
+ formatRelativeTime,
9
+ getCached,
10
+ Icon,
11
+ isShooterConfig,
12
+ setCache,
13
+ } from '$lib/modules/client/common';
14
+ import { onDestroy, onMount } from 'svelte';
15
+
16
+ interface ProjectGroup {
17
+ fullPath: string;
18
+ id: string;
19
+ lastModified: string;
20
+ name: string;
21
+ sessionCount: number;
22
+ }
23
+
24
+ const POLL_INTERVAL_MS = 30_000; // 30s - avoid heavy reflows
25
+ const PAGE_SIZE = 20;
26
+
27
+ let projects = $state<ProjectGroup[]>([]);
28
+ let loading = $state(false);
29
+ let fetching = false; // non-reactive guard to prevent overlapping fetches
30
+ let config = $state<null | ShooterConfig>(null);
31
+ let pollTimer: null | ReturnType<typeof setInterval> = null;
32
+ let hasMore = $state(false);
33
+ let currentOffset = $state(0);
34
+
35
+ onMount(() => {
36
+ loadConfiguration();
37
+
38
+ // Show cached data immediately
39
+ const cached = getCached('shooter_projects') as null | ProjectGroup[];
40
+ if (cached) {
41
+ projects = cached;
42
+ loading = false;
43
+ }
44
+
45
+ // Then fetch fresh data in background
46
+ void fetchSessions();
47
+
48
+ pollTimer = setInterval(() => {
49
+ if (config?.apiKey) {
50
+ void fetchSessions();
51
+ }
52
+ }, POLL_INTERVAL_MS);
53
+ });
54
+
55
+ onDestroy(() => {
56
+ if (pollTimer) {
57
+ clearInterval(pollTimer);
58
+ pollTimer = null;
59
+ }
60
+ });
61
+
62
+ function loadConfiguration(): void {
63
+ try {
64
+ const saved = localStorage.getItem('shooter_config');
65
+ if (saved) {
66
+ const parsed: unknown = JSON.parse(saved);
67
+ if (isShooterConfig(parsed)) {
68
+ config = parsed;
69
+ } else {
70
+ localStorage.removeItem('shooter_config');
71
+ config = null;
72
+ }
73
+ }
74
+ } catch {
75
+ // No configuration found — expected on first visit
76
+ }
77
+ }
78
+
79
+ async function fetchSessions(append = false): Promise<void> {
80
+ if (!config?.apiKey || fetching) {
81
+ return;
82
+ }
83
+ fetching = true;
84
+
85
+ // Don't show loading spinner if we already have cached data
86
+ if (projects.length === 0) {
87
+ loading = true;
88
+ }
89
+
90
+ const offset = append ? currentOffset : 0;
91
+
92
+ try {
93
+ const response = await fetch(`/api/sessions?limit=${PAGE_SIZE}&offset=${offset}`, {
94
+ headers: {
95
+ Authorization: `Bearer ${config.apiKey}`,
96
+ },
97
+ });
98
+
99
+ if (!response.ok) {
100
+ return;
101
+ }
102
+
103
+ const result: { projects: ProjectGroup[]; total?: number } = await response.json();
104
+ if (append) {
105
+ projects = [...projects, ...result.projects];
106
+ } else {
107
+ projects = result.projects;
108
+ }
109
+ currentOffset = projects.length;
110
+ hasMore = result.total !== undefined ? projects.length < result.total : false;
111
+ setCache('shooter_projects', projects);
112
+ } catch (error) {
113
+ console.error('Failed to fetch sessions:', error);
114
+ } finally {
115
+ loading = false;
116
+ fetching = false;
117
+ }
118
+ }
119
+
120
+ async function loadMore(): Promise<void> {
121
+ await fetchSessions(true);
122
+ }
123
+
124
+ async function forceRefresh(): Promise<void> {
125
+ loading = true;
126
+ sessionStorage.removeItem('shooter_projects');
127
+ await fetchSessions();
128
+ }
129
+
130
+ function navigateToConfig(): void {
131
+ void goto('/config');
132
+ }
133
+
134
+ function totalSessionCount(): number {
135
+ return projects.reduce((sum, p) => sum + p.sessionCount, 0);
136
+ }
137
+ </script>
138
+
139
+ <svelte:head>
140
+ <title>Projects - Shooter</title>
141
+ <meta name="description" content="Claude Code sessions across all projects" />
142
+ </svelte:head>
143
+
144
+ <main class="main">
145
+ <div class="page-header">
146
+ <div class="page-header-content">
147
+ <div>
148
+ <h1 class="page-title">Projects</h1>
149
+ <p class="page-description">Claude Code sessions across all projects</p>
150
+ </div>
151
+ <div class="page-actions">
152
+ <Button classes="btn-secondary" onclick={forceRefresh} disabled={loading}>
153
+ {#snippet children()}
154
+ <Icon name="refresh" size={14} />
155
+ Refresh
156
+ {/snippet}
157
+ </Button>
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ {#if loading && projects.length === 0}
163
+ <div class="loading-container">
164
+ {#each Array(5) as _, i (i)}
165
+ <Shimmer classes="shimmer-card" />
166
+ {/each}
167
+ </div>
168
+ {:else if !config?.apiKey}
169
+ <EmptyState
170
+ icon="settings"
171
+ title="Configuration Required"
172
+ description="Set up your API credentials to start tracking sessions"
173
+ >
174
+ <Button classes="btn-primary" onclick={navigateToConfig} text="Configure Settings" />
175
+ </EmptyState>
176
+ {:else if totalSessionCount() === 0}
177
+ <EmptyState
178
+ icon="bell"
179
+ title="No sessions yet"
180
+ description="Claude Code sessions will appear here once JSONL files are found"
181
+ />
182
+ {:else}
183
+ <div class="projects-container">
184
+ {#each projects as project (project.id)}
185
+ <a href="/project?id={project.id}" class="session-card">
186
+ <div class="session-card-header">
187
+ <div>
188
+ <h3 class="session-card-title">{project.name}</h3>
189
+ <div class="session-card-subtitle">{project.fullPath}</div>
190
+ </div>
191
+ <Pill text={formatRelativeTime(project.lastModified)} classes="pill-session-time" />
192
+ </div>
193
+ <div class="session-stats">
194
+ <span><strong>{project.sessionCount}</strong> {project.sessionCount === 1 ? 'session' : 'sessions'}</span>
195
+ </div>
196
+ </a>
197
+ {/each}
198
+ </div>
199
+ {#if hasMore}
200
+ <div style="text-align: center; padding: 1rem;">
201
+ <Button classes="btn-secondary" onclick={loadMore} text="Load More" />
202
+ </div>
203
+ {/if}
204
+ {/if}
205
+ </main>
206
+
207
+ <style>
208
+ .page-header {
209
+ margin-bottom: var(--space-6);
210
+ }
211
+
212
+ .page-header-content {
213
+ display: flex;
214
+ justify-content: space-between;
215
+ align-items: flex-start;
216
+ gap: var(--space-4);
217
+ }
218
+
219
+ .page-title {
220
+ font-size: var(--text-2xl);
221
+ font-weight: 600;
222
+ letter-spacing: -0.03em;
223
+ color: var(--text-primary);
224
+ margin-bottom: var(--space-1);
225
+ }
226
+
227
+ .page-description {
228
+ font-size: var(--text-sm);
229
+ color: var(--text-secondary);
230
+ }
231
+
232
+ .page-actions {
233
+ display: flex;
234
+ gap: var(--space-2);
235
+ flex-shrink: 0;
236
+ }
237
+
238
+ .projects-container {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: var(--space-4);
242
+ animation: fadeIn 0.2s ease;
243
+ }
244
+
245
+ @media (max-width: 768px) {
246
+ .page-header-content {
247
+ flex-direction: column;
248
+ gap: var(--space-4);
249
+ }
250
+ }
251
+
252
+ @media (max-width: 480px) {
253
+ .page-actions {
254
+ width: 100%;
255
+ }
256
+
257
+ .page-actions :global(button) {
258
+ flex: 1;
259
+ }
260
+ }
261
+ </style>
@@ -0,0 +1,33 @@
1
+ import { env } from '$env/dynamic/private';
2
+ import { LibraryAPNsService } from '$lib/modules/server/apn/library-apns';
3
+ import { validateAuth } from '$lib/modules/server/auth';
4
+ import { json } from '@sveltejs/kit';
5
+
6
+ import type { RequestHandler } from './$types';
7
+
8
+ export const GET: RequestHandler = ({ request }) => {
9
+ const authError = validateAuth(request);
10
+ if (authError) {return authError;}
11
+
12
+ const apnsClient = new LibraryAPNsService();
13
+ const deviceToken = env.DEVICE_TOKEN?.trim();
14
+
15
+ return json({
16
+ apns: {
17
+ configured: apnsClient.isConfigured(),
18
+ environment: env.APNS_PRODUCTION === 'true' ? 'production' : 'sandbox',
19
+ hasBundleId: !!env.APNS_BUNDLE_ID,
20
+ hasKey: !!env.APNS_KEY,
21
+ hasKeyId: !!env.APNS_KEY_ID,
22
+ hasTeamId: !!env.APNS_TEAM_ID,
23
+ },
24
+ deviceToken: {
25
+ exists: !!deviceToken,
26
+ length: deviceToken ? deviceToken.length : 0,
27
+ valid: deviceToken ? /^[a-f0-9]{64}$/i.test(deviceToken) : false,
28
+ },
29
+ environment: env.NODE_ENV || 'development',
30
+ hasApiKey: !!env.API_KEY,
31
+ timestamp: new Date().toISOString(),
32
+ });
33
+ };
@@ -0,0 +1,85 @@
1
+ import { env } from '$env/dynamic/private';
2
+ import { validateAuth } from '$lib/modules/server/auth';
3
+ import { json } from '@sveltejs/kit';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
5
+ import { homedir } from 'os';
6
+ import { join } from 'path';
7
+
8
+ import type { RequestHandler } from './$types';
9
+
10
+ interface DeviceTokens {
11
+ android?: string;
12
+ ios?: string;
13
+ }
14
+
15
+ interface DeviceTokenRequest {
16
+ bundleId?: string;
17
+ deviceToken?: string;
18
+ platform: string;
19
+ token?: string;
20
+ }
21
+
22
+ const TOKENS_DIR = join(homedir(), '.shooter');
23
+ const TOKENS_FILE = join(TOKENS_DIR, 'device-tokens.json');
24
+
25
+ function readTokens(): DeviceTokens {
26
+ try {
27
+ if (existsSync(TOKENS_FILE)) {
28
+ return JSON.parse(readFileSync(TOKENS_FILE, 'utf-8'));
29
+ }
30
+ } catch {
31
+ // Corrupt file — start fresh
32
+ }
33
+ return {};
34
+ }
35
+
36
+ function writeTokens(tokens: DeviceTokens): void {
37
+ if (!existsSync(TOKENS_DIR)) {
38
+ mkdirSync(TOKENS_DIR, { recursive: true });
39
+ }
40
+ writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2), 'utf-8');
41
+ }
42
+
43
+ export const POST: RequestHandler = async ({ request }) => {
44
+ const authError = validateAuth(request);
45
+ if (authError) return authError;
46
+
47
+ let body: DeviceTokenRequest;
48
+ try {
49
+ body = await request.json();
50
+ } catch {
51
+ return json({ error: 'Invalid JSON body' }, { status: 400 });
52
+ }
53
+
54
+ const platform = body.platform;
55
+ if (!platform || (platform !== 'ios' && platform !== 'android')) {
56
+ return json(
57
+ { error: 'Missing or invalid platform (must be "ios" or "android")' },
58
+ { status: 400 },
59
+ );
60
+ }
61
+
62
+ // iOS sends "deviceToken", Android sends "token"
63
+ const token = body.deviceToken || body.token;
64
+ if (!token || typeof token !== 'string' || token.trim().length === 0) {
65
+ return json({ error: 'Missing device token (deviceToken or token)' }, { status: 400 });
66
+ }
67
+
68
+ // Persist to ~/.shooter/device-tokens.json
69
+ const tokens = readTokens();
70
+ tokens[platform] = token;
71
+ writeTokens(tokens);
72
+
73
+ // Update in-memory env so APNs can use it immediately (iOS is the primary APNs target)
74
+ if (platform === 'ios') {
75
+ (env as Record<string, string>).DEVICE_TOKEN = token;
76
+ }
77
+
78
+ console.log(`[device-token] Registered ${platform} token (length: ${token.length})`);
79
+
80
+ return json({
81
+ platform,
82
+ success: true,
83
+ timestamp: new Date().toISOString(),
84
+ });
85
+ };
@@ -0,0 +1,100 @@
1
+ import { env } from '$env/dynamic/private';
2
+ import { validateAuth } from '$lib/modules/server/auth';
3
+ import { json } from '@sveltejs/kit';
4
+
5
+ import type { RequestHandler } from './$types';
6
+
7
+ interface FCMConfiguration {
8
+ configured: boolean;
9
+ hasClientEmail: boolean;
10
+ hasPrivateKey: boolean;
11
+ hasProjectId: boolean;
12
+ }
13
+
14
+ interface HealthChecks {
15
+ hasApiKey: boolean;
16
+ hasAPNsConfig: boolean;
17
+ hasBundleId: boolean;
18
+ hasDeviceToken: boolean;
19
+ hasFCMConfig: boolean;
20
+ }
21
+
22
+ interface HealthConfiguration {
23
+ apnsKeyId: null | string;
24
+ bundleId: null | string;
25
+ deviceTokenLength: number;
26
+ fcm: FCMConfiguration;
27
+ production: boolean;
28
+ }
29
+
30
+ interface HealthResponse {
31
+ checks: HealthChecks;
32
+ configuration: HealthConfiguration;
33
+ environment: string;
34
+ status: 'degraded' | 'healthy';
35
+ timestamp: string;
36
+ version: string;
37
+ }
38
+
39
+ export const GET: RequestHandler = ({ request, url }) => {
40
+ // Basic status check is public (used by layout status badge).
41
+ // Detailed config requires auth (L12 security fix).
42
+ const wantsDetails = url.searchParams.get('details') === 'true';
43
+ if (wantsDetails) {
44
+ const authError = validateAuth(request);
45
+ if (authError) return authError;
46
+ }
47
+
48
+ const hasProjectId = !!env.FCM_PROJECT_ID;
49
+ const hasClientEmail = !!env.FCM_CLIENT_EMAIL;
50
+ const hasPrivateKey = !!env.FCM_PRIVATE_KEY;
51
+
52
+ const health: HealthResponse = {
53
+ checks: {
54
+ hasApiKey: !!env.API_KEY,
55
+ hasAPNsConfig: !!(
56
+ env.APNS_KEY_ID &&
57
+ env.APNS_TEAM_ID &&
58
+ env.APNS_KEY
59
+ ),
60
+ hasBundleId: !!env.APNS_BUNDLE_ID,
61
+ hasDeviceToken: !!env.DEVICE_TOKEN,
62
+ hasFCMConfig: hasProjectId && hasClientEmail && hasPrivateKey,
63
+ },
64
+ configuration: {
65
+ apnsKeyId: env.APNS_KEY_ID ? `${env.APNS_KEY_ID.substring(0, 4)}...` : null,
66
+ bundleId: env.APNS_BUNDLE_ID || null,
67
+ deviceTokenLength: env.DEVICE_TOKEN ? env.DEVICE_TOKEN.length : 0,
68
+ fcm: {
69
+ configured: hasProjectId && hasClientEmail && hasPrivateKey,
70
+ hasClientEmail,
71
+ hasPrivateKey,
72
+ hasProjectId,
73
+ },
74
+ production: env.APNS_PRODUCTION === 'true',
75
+ },
76
+ environment: env.NODE_ENV || 'development',
77
+ status: 'healthy',
78
+ timestamp: new Date().toISOString(),
79
+ version: '1.1.0',
80
+ };
81
+
82
+ // Determine overall health status
83
+ // FCM is not a critical check - only APNs is required for core functionality
84
+ const criticalChecks = [
85
+ health.checks.hasDeviceToken,
86
+ health.checks.hasAPNsConfig,
87
+ health.checks.hasBundleId,
88
+ ];
89
+
90
+ if (criticalChecks.some((check) => !check)) {
91
+ health.status = 'degraded';
92
+ }
93
+
94
+ // Public response: status only. Authenticated: full details.
95
+ if (!wantsDetails) {
96
+ return json({ status: health.status, timestamp: health.timestamp });
97
+ }
98
+
99
+ return json(health);
100
+ };