@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,243 @@
1
+ // IMPORTANT: This module must NEVER be imported at module top level in a Svelte component.
2
+ // Always call createTerminal() inside onMount() only.
3
+
4
+ import type { Terminal } from '@xterm/xterm';
5
+
6
+ interface TerminalInstance {
7
+ dispose: () => void;
8
+ fitAddon: any; // FitAddon type
9
+ sendInput: (data: string) => void;
10
+ term: Terminal;
11
+ }
12
+
13
+ interface TerminalOptions {
14
+ apiKey?: string;
15
+ container: HTMLElement;
16
+ fontSize?: number;
17
+ getTicket: () => Promise<string>;
18
+ onActivity?: (active: boolean) => void;
19
+ onCwd?: (path: string) => void;
20
+ onDisconnect?: () => void;
21
+ onExit?: (code: number) => void;
22
+ onReconnect?: () => void;
23
+ terminalId?: string;
24
+ wsUrl: string;
25
+ }
26
+
27
+ export async function createTerminal(options: TerminalOptions): Promise<TerminalInstance> {
28
+ // Dynamic imports — only loaded client-side
29
+ const { Terminal } = await import('@xterm/xterm');
30
+ const { FitAddon } = await import('@xterm/addon-fit');
31
+ const { WebLinksAddon } = await import('@xterm/addon-web-links');
32
+
33
+ // Also need to import the CSS
34
+ await import('@xterm/xterm/css/xterm.css');
35
+
36
+ const fitAddon = new FitAddon();
37
+
38
+ const term = new Terminal({
39
+ allowTransparency: true,
40
+ cursorBlink: true,
41
+ cursorStyle: 'block',
42
+ fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', 'Menlo', monospace",
43
+ fontSize: options.fontSize || 14,
44
+ theme: {
45
+ background: '#0a0a0f',
46
+ black: '#0a0a0f',
47
+ blue: '#3b82f6',
48
+ brightBlack: '#64748b',
49
+ brightBlue: '#60a5fa',
50
+ brightCyan: '#67e8f9',
51
+ brightGreen: '#4ade80',
52
+ brightMagenta: '#c4b5fd',
53
+ brightRed: '#f87171',
54
+ brightWhite: '#f8fafc',
55
+ brightYellow: '#fbbf24',
56
+ cursor: '#e2e8f0',
57
+ cursorAccent: '#0a0a0f',
58
+ cyan: '#38bdf8',
59
+ foreground: '#e2e8f0',
60
+ green: '#22c55e',
61
+ magenta: '#a78bfa',
62
+ red: '#ef4444',
63
+ selectionBackground: 'rgba(99, 102, 241, 0.3)',
64
+ white: '#e2e8f0',
65
+ yellow: '#f59e0b',
66
+ },
67
+ });
68
+
69
+ term.loadAddon(fitAddon);
70
+ term.loadAddon(new WebLinksAddon());
71
+ term.open(options.container);
72
+ fitAddon.fit();
73
+
74
+ // Block browser-level Cmd/Ctrl shortcuts from reaching the PTY.
75
+ // Allow Ctrl+<letter> terminal signals (Ctrl+C/D/L/R/Z etc.) through.
76
+ const browserShortcuts = new Set(['s', 'w', 't', 'n', 'p', 'q', 'h', 'j', 'f', 'g', 'o', 'u']);
77
+ term.attachCustomKeyEventHandler((e) => {
78
+ // Cmd+key on Mac: block known browser shortcuts, allow the rest
79
+ if (e.metaKey) {
80
+ if (e.key === 'c' || e.key === 'v') { return true; } // allow copy/paste
81
+ if (browserShortcuts.has(e.key)) { return false; } // block browser shortcuts
82
+ return true;
83
+ }
84
+ // Ctrl+key (non-Mac modifier): allow all through to PTY (Ctrl+C/D/L/R/Z etc.)
85
+ return true;
86
+ });
87
+
88
+ // Clipboard image paste interception
89
+ let pasteListener: ((e: ClipboardEvent) => void) | null = null;
90
+ if (options.terminalId && options.apiKey) {
91
+ const pasteTermId = options.terminalId;
92
+ const pasteApiKey = options.apiKey;
93
+
94
+ pasteListener = async (e: ClipboardEvent) => {
95
+ try {
96
+ if (!e.clipboardData) return;
97
+ const items = Array.from(e.clipboardData.items);
98
+ const imageItem = items.find(item => item.type.startsWith('image/'));
99
+ if (!imageItem) return; // No image — let normal paste proceed
100
+
101
+ e.preventDefault();
102
+ const blob = imageItem.getAsFile();
103
+ if (!blob) return;
104
+
105
+ // Read image as base64
106
+ const reader = new FileReader();
107
+ const base64 = await new Promise<string>((resolve, reject) => {
108
+ reader.onload = () => resolve(reader.result as string);
109
+ reader.onerror = reject;
110
+ reader.readAsDataURL(blob);
111
+ });
112
+
113
+ // Upload to server
114
+ const res = await fetch(`/api/terminals/${pasteTermId}/paste-image`, {
115
+ method: 'POST',
116
+ headers: {
117
+ 'Authorization': `Bearer ${pasteApiKey}`,
118
+ 'Content-Type': 'application/json',
119
+ },
120
+ body: JSON.stringify({ image: base64 }),
121
+ });
122
+
123
+ // Send Ctrl+V (0x16) to PTY only after a successful upload
124
+ if (res.ok && ws?.readyState === WebSocket.OPEN) {
125
+ ws.send(JSON.stringify({ data: '\x16', type: 'input' }));
126
+ }
127
+ } catch {
128
+ // Silent failure — don't break text paste
129
+ }
130
+ };
131
+
132
+ options.container.addEventListener('paste', pasteListener as EventListener);
133
+ }
134
+
135
+ // WebSocket connection
136
+ let ws: null | WebSocket = null;
137
+ let reconnectTimer: null | ReturnType<typeof setTimeout> = null;
138
+ let reconnectDelay = 1000;
139
+ let disposed = false;
140
+
141
+ async function connect() {
142
+ if (disposed) {return;}
143
+
144
+ let ticket: string;
145
+ try {
146
+ ticket = await options.getTicket();
147
+ } catch {
148
+ // Ticket fetch failed — schedule a retry
149
+ if (!disposed) {
150
+ options.onDisconnect?.();
151
+ reconnectTimer = setTimeout(() => {
152
+ reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
153
+ void connect();
154
+ }, reconnectDelay);
155
+ }
156
+ return;
157
+ }
158
+
159
+ if (disposed) {return;}
160
+
161
+ ws = new WebSocket(`${options.wsUrl}?ticket=${ticket}`);
162
+
163
+ ws.onopen = () => {
164
+ reconnectDelay = 1000; // Reset backoff
165
+ options.onReconnect?.();
166
+ };
167
+
168
+ ws.onmessage = (event) => {
169
+ const msg = JSON.parse(event.data);
170
+ if (msg.type === 'output') {
171
+ term.write(msg.data);
172
+ } else if (msg.type === 'scrollback') {
173
+ term.write(msg.data);
174
+ } else if (msg.type === 'exit') {
175
+ term.write(`\r\n\x1b[90m[Process exited with code ${msg.code}]\x1b[0m\r\n`);
176
+ // Process exited — stop reconnection and notify parent
177
+ disposed = true;
178
+ if (reconnectTimer) {clearTimeout(reconnectTimer);}
179
+ options.onExit?.(msg.code);
180
+ } else if (msg.type === 'output-dropped') {
181
+ term.write(`\r\n\x1b[33m[${msg.bytes} bytes dropped]\x1b[0m\r\n`);
182
+ } else if (msg.type === 'activity') {
183
+ options.onActivity?.(msg.active);
184
+ } else if (msg.type === 'cwd') {
185
+ options.onCwd?.(msg.path);
186
+ }
187
+ };
188
+
189
+ ws.onclose = () => {
190
+ if (disposed) {return;}
191
+ options.onDisconnect?.();
192
+ // Exponential backoff reconnect
193
+ reconnectTimer = setTimeout(() => {
194
+ reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
195
+ void connect();
196
+ }, reconnectDelay);
197
+ };
198
+ }
199
+
200
+ void connect();
201
+
202
+ // Terminal input -> WebSocket
203
+ term.onData((data) => {
204
+ if (ws?.readyState === WebSocket.OPEN) {
205
+ ws.send(JSON.stringify({ data, type: 'input' }));
206
+ }
207
+ });
208
+
209
+ // Handle resize
210
+ const resizeObserver = new ResizeObserver(() => {
211
+ fitAddon.fit();
212
+ if (ws?.readyState === WebSocket.OPEN) {
213
+ ws.send(JSON.stringify({ cols: term.cols, rows: term.rows, type: 'resize' }));
214
+ }
215
+ });
216
+ resizeObserver.observe(options.container);
217
+
218
+ function dispose() {
219
+ disposed = true;
220
+ if (reconnectTimer) {clearTimeout(reconnectTimer);}
221
+ if (pasteListener) {
222
+ options.container.removeEventListener('paste', pasteListener as EventListener);
223
+ }
224
+ resizeObserver.disconnect();
225
+ ws?.close();
226
+ term.dispose();
227
+ }
228
+
229
+ function sendInput(data: string) {
230
+ if (ws?.readyState === WebSocket.OPEN) {
231
+ ws.send(JSON.stringify({ data, type: 'input' }));
232
+ }
233
+ }
234
+
235
+ return { dispose, fitAddon, sendInput, term };
236
+ }
237
+
238
+ // Helper to send signals
239
+ export function sendSignal(ws: WebSocket, signal: string): void {
240
+ if (ws.readyState === WebSocket.OPEN) {
241
+ ws.send(JSON.stringify({ signal, type: 'signal' }));
242
+ }
243
+ }
@@ -0,0 +1,137 @@
1
+ import { env } from '$env/dynamic/private';
2
+ // APNs service using proven library instead of manual implementation
3
+ import apn from '@parse/node-apn';
4
+
5
+ import type { LibraryResult as APNsLibraryResult, NotificationPayload } from './types';
6
+
7
+ export class LibraryAPNsService {
8
+ private bundleId: string | undefined;
9
+ private configured = false;
10
+ private keyId: string | undefined;
11
+ private privateKey: string | undefined;
12
+ private provider: apn.Provider | null = null;
13
+ private teamId: string | undefined;
14
+
15
+ constructor() {
16
+ this.keyId = env.APNS_KEY_ID;
17
+ this.teamId = env.APNS_TEAM_ID;
18
+ this.bundleId = env.APNS_BUNDLE_ID;
19
+ this.privateKey = env.APNS_KEY;
20
+
21
+ if (!this.keyId || !this.teamId || !this.bundleId || !this.privateKey) {
22
+ console.error(
23
+ '[apns] Missing required configuration (APNS_KEY_ID, APNS_TEAM_ID, APNS_BUNDLE_ID, or APNS_KEY)'
24
+ );
25
+ this.configured = false;
26
+ return;
27
+ }
28
+
29
+ const production = env.APNS_PRODUCTION === 'true';
30
+ const options = {
31
+ production,
32
+ token: {
33
+ key: this.privateKey,
34
+ keyId: this.keyId,
35
+ teamId: this.teamId,
36
+ },
37
+ };
38
+
39
+ try {
40
+ this.provider = new apn.Provider(options);
41
+ this.configured = true;
42
+ console.log(`[apns] Provider initialized (${production ? 'production' : 'sandbox'} mode)`);
43
+ } catch (error) {
44
+ const err = error as Error;
45
+ console.error('[apns] Failed to create provider:', err.message);
46
+ this.configured = false;
47
+ }
48
+ }
49
+
50
+ isConfigured(): boolean {
51
+ return this.configured;
52
+ }
53
+
54
+ async sendNotification(
55
+ deviceToken: string,
56
+ payload: NotificationPayload
57
+ ): Promise<{
58
+ details?: unknown[];
59
+ error?: string;
60
+ failed: number;
61
+ sent: number;
62
+ success: boolean;
63
+ }> {
64
+ if (!this.configured || !this.provider) {
65
+ throw new Error('APNs service not configured properly');
66
+ }
67
+
68
+ if (!deviceToken || !payload) {
69
+ throw new Error('Device token and payload are required');
70
+ }
71
+
72
+ const notification = new apn.Notification();
73
+
74
+ notification.alert = {
75
+ body: payload.body ?? payload.message ?? '',
76
+ title: payload.title,
77
+ };
78
+
79
+ notification.badge = payload.badge ?? 1;
80
+ notification.sound = payload.sound ?? 'default';
81
+ notification.topic = this.bundleId!;
82
+
83
+ if (payload.category) {
84
+ notification.aps.category = payload.category;
85
+ }
86
+
87
+ if (payload.data) {
88
+ notification.payload = payload.data;
89
+ }
90
+
91
+ const result = (await this.provider.send(notification, deviceToken)) as APNsLibraryResult;
92
+
93
+ if (result.failed && result.failed.length > 0) {
94
+ console.error(`[apns] Full failed result: ${JSON.stringify(result.failed[0])}`);
95
+ const failedItem = result.failed[0] as unknown as Record<string, unknown>;
96
+ const rawReason =
97
+ (failedItem?.response as Record<string, unknown>)?.reason ??
98
+ failedItem?.status ??
99
+ failedItem?.error;
100
+ const reason =
101
+ typeof rawReason === 'string' ? rawReason : JSON.stringify(rawReason ?? failedItem);
102
+ console.error(`[apns] Delivery failed: ${reason}`);
103
+
104
+ return {
105
+ error: reason,
106
+ failed: result.failed?.length || 0,
107
+ sent: result.sent?.length || 0,
108
+ success: false,
109
+ };
110
+ }
111
+
112
+ if (result.sent && result.sent.length > 0) {
113
+ return {
114
+ details: result.sent,
115
+ failed: 0,
116
+ sent: result.sent.length,
117
+ success: true,
118
+ };
119
+ }
120
+
121
+ return {
122
+ details: [result as unknown as Record<string, unknown>],
123
+ error: 'Unexpected result format',
124
+ failed: 1,
125
+ sent: 0,
126
+ success: false,
127
+ };
128
+ }
129
+
130
+ shutdown(): void {
131
+ if (this.provider) {
132
+ void this.provider.shutdown();
133
+ }
134
+ }
135
+ }
136
+
137
+ export default LibraryAPNsService;
@@ -0,0 +1,35 @@
1
+ // In-memory notification history store.
2
+ // Same pattern as pending-requests.ts — suitable for single-instance deployments.
3
+
4
+ export interface NotificationRecord {
5
+ category?: string;
6
+ data?: Record<string, unknown>;
7
+ error?: string;
8
+ id: string;
9
+ message: string;
10
+ project?: string;
11
+ source?: string;
12
+ status: 'failed' | 'filtered' | 'sent';
13
+ timestamp: string;
14
+ title: string;
15
+ tool?: string;
16
+ }
17
+
18
+ const MAX_HISTORY = 100;
19
+
20
+ const history: NotificationRecord[] = [];
21
+
22
+ export function addNotification(record: NotificationRecord): void {
23
+ history.unshift(record); // newest first
24
+ if (history.length > MAX_HISTORY) {
25
+ history.length = MAX_HISTORY; // trim old entries
26
+ }
27
+ }
28
+
29
+ export function clearNotifications(): void {
30
+ history.length = 0;
31
+ }
32
+
33
+ export function getNotifications(limit = 50): NotificationRecord[] {
34
+ return history.slice(0, limit);
35
+ }
@@ -0,0 +1,117 @@
1
+ import { getNotifications, type NotificationRecord } from './notification-history';
2
+
3
+ export interface Session {
4
+ duration: number; // seconds
5
+ endTime: string;
6
+ eventCount: number;
7
+ events: NotificationRecord[];
8
+ filesModified: string[];
9
+ id: string;
10
+ project: string;
11
+ runtime: string;
12
+ startTime: string;
13
+ status: 'active' | 'complete';
14
+ toolsUsed: string[];
15
+ }
16
+
17
+ const SESSION_GAP_MS = 30 * 60 * 1000; // 30 min gap = new session
18
+ const ACTIVE_THRESHOLD_MS = 5 * 60 * 1000; // active if last event < 5 min ago
19
+
20
+ export function getSessionById(id: string): null | Session {
21
+ return getSessions().find((s) => s.id === id) || null;
22
+ }
23
+
24
+ export function getSessions(): Session[] {
25
+ const notifications = getNotifications(100);
26
+ if (notifications.length === 0) {
27
+ return [];
28
+ }
29
+
30
+ // Sort by timestamp ascending for grouping
31
+ const sorted = [...notifications].sort(
32
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
33
+ );
34
+
35
+ const sessions: Session[] = [];
36
+ let currentGroup: NotificationRecord[] = [sorted[0]];
37
+
38
+ for (let i = 1; i < sorted.length; i++) {
39
+ const prev = new Date(sorted[i - 1].timestamp).getTime();
40
+ const curr = new Date(sorted[i].timestamp).getTime();
41
+
42
+ if (curr - prev > SESSION_GAP_MS) {
43
+ // Gap too large — start new session
44
+ sessions.push(buildSession(currentGroup, sessions.length + 1));
45
+ currentGroup = [sorted[i]];
46
+ } else {
47
+ currentGroup.push(sorted[i]);
48
+ }
49
+ }
50
+
51
+ // Don't forget last group
52
+ if (currentGroup.length > 0) {
53
+ sessions.push(buildSession(currentGroup, sessions.length + 1));
54
+ }
55
+
56
+ // Return newest first
57
+ return sessions.reverse();
58
+ }
59
+
60
+ function buildSession(events: NotificationRecord[], index: number): Session {
61
+ const startTime = events[0].timestamp;
62
+ const endTime = events[events.length - 1].timestamp;
63
+ const startMs = new Date(startTime).getTime();
64
+ const endMs = new Date(endTime).getTime();
65
+ const now = Date.now();
66
+
67
+ // Extract tools from events
68
+ const tools = new Set<string>();
69
+ const files = new Set<string>();
70
+ let project = 'unknown';
71
+ let runtime = 'claude-code';
72
+
73
+ for (const event of events) {
74
+ if (event.tool) {
75
+ tools.add(event.tool);
76
+ }
77
+ if (event.data?.tool && typeof event.data.tool === 'string') {
78
+ tools.add(event.data.tool);
79
+ }
80
+ if (event.project) {
81
+ project = event.project;
82
+ }
83
+ if (event.data?.project && typeof event.data.project === 'string') {
84
+ project = event.data.project;
85
+ }
86
+ if (event.data?.runtime && typeof event.data.runtime === 'string') {
87
+ runtime = event.data.runtime;
88
+ }
89
+ // Extract files from data
90
+ if (event.data?.file && typeof event.data.file === 'string') {
91
+ files.add(event.data.file);
92
+ }
93
+ if (event.data?.files && typeof event.data.files === 'string') {
94
+ event.data.files.split(',').forEach((f: string) => {
95
+ if (f.trim()) {
96
+ files.add(f.trim());
97
+ }
98
+ });
99
+ }
100
+ }
101
+
102
+ const isActive = now - endMs < ACTIVE_THRESHOLD_MS;
103
+
104
+ return {
105
+ duration: Math.round((endMs - startMs) / 1000),
106
+ endTime,
107
+ eventCount: events.length,
108
+ events, // already sorted ascending
109
+ filesModified: [...files],
110
+ id: `session-${index}`,
111
+ project,
112
+ runtime,
113
+ startTime,
114
+ status: isActive ? 'active' : 'complete',
115
+ toolsUsed: [...tools],
116
+ };
117
+ }
@@ -0,0 +1,65 @@
1
+ // Shared in-memory store for pending permission requests.
2
+ // Used by /api/notify (creates entries) and /api/response (reads/updates).
3
+
4
+ export interface PendingRequest {
5
+ createdAt: number;
6
+ decidedAt: null | number;
7
+ decision: 'allow' | 'deny' | null;
8
+ sessionId: string;
9
+ toolInput: Record<string, unknown>;
10
+ toolName: string;
11
+ }
12
+
13
+ const MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
14
+
15
+ const pendingRequests = new Map<string, PendingRequest>();
16
+
17
+ export function cleanup(maxAgeMs: number = MAX_AGE_MS): void {
18
+ const now = Date.now();
19
+ for (const [id, entry] of pendingRequests.entries()) {
20
+ if (now - entry.createdAt > maxAgeMs) {
21
+ pendingRequests.delete(id);
22
+ }
23
+ }
24
+ }
25
+
26
+ export function createPendingRequest(
27
+ requestId: string,
28
+ data: { sessionId: string; toolInput: Record<string, unknown>; toolName: string }
29
+ ): void {
30
+ cleanup();
31
+ pendingRequests.set(requestId, {
32
+ createdAt: Date.now(),
33
+ decidedAt: null,
34
+ decision: null,
35
+ sessionId: data.sessionId,
36
+ toolInput: data.toolInput,
37
+ toolName: data.toolName,
38
+ });
39
+ }
40
+
41
+ export function getDecision(requestId: string): {
42
+ decision?: 'allow' | 'deny';
43
+ status: 'decided' | 'not_found' | 'pending';
44
+ } {
45
+ const entry = pendingRequests.get(requestId);
46
+ if (!entry) {
47
+ return { status: 'not_found' };
48
+ }
49
+ if (entry.decision) {
50
+ // Clean up after reading a decided response
51
+ pendingRequests.delete(requestId);
52
+ return { decision: entry.decision, status: 'decided' };
53
+ }
54
+ return { status: 'pending' };
55
+ }
56
+
57
+ export function setDecision(requestId: string, decision: 'allow' | 'deny'): boolean {
58
+ const entry = pendingRequests.get(requestId);
59
+ if (!entry) {
60
+ return false;
61
+ }
62
+ entry.decision = decision;
63
+ entry.decidedAt = Date.now();
64
+ return true;
65
+ }
@@ -0,0 +1,51 @@
1
+ import type {
2
+ Error as APNsError,
3
+ ErrorData,
4
+ LibraryFailedItem,
5
+ LibrarySentItem,
6
+ RawResponse,
7
+ SentDetail,
8
+ } from '$generated/types';
9
+
10
+ // Types not yet generated by type-crafter (contain $ref/arrays/oneOf)
11
+ // TODO: Remove these once type-crafter generates them
12
+
13
+ export interface LibraryResult {
14
+ failed: LibraryFailedItem[] | null;
15
+ sent: LibrarySentItem[] | null;
16
+ }
17
+
18
+ export interface NotificationPayload {
19
+ badge: null | number;
20
+ body: null | string;
21
+ category?: null | string;
22
+ data: null | Record<string, unknown>;
23
+ message: null | string;
24
+ sound: null | string;
25
+ title: string;
26
+ }
27
+
28
+ export interface NotificationResult {
29
+ apnsId: null | string;
30
+ details: null | SentDetail[];
31
+ error: ErrorData | null | string;
32
+ errorData: ErrorData | null;
33
+ errors: APNsError[] | null;
34
+ failed: number;
35
+ headers: null | Record<string, unknown>;
36
+ response: null | RawResponse;
37
+ responseBody: null | string;
38
+ sent: number;
39
+ status: null | number;
40
+ statusCode: null | number;
41
+ success: boolean;
42
+ }
43
+
44
+ export type {
45
+ APNsError as Error,
46
+ ErrorData,
47
+ LibraryFailedItem,
48
+ LibrarySentItem,
49
+ RawResponse,
50
+ SentDetail,
51
+ };
@@ -0,0 +1,34 @@
1
+ import { env } from '$env/dynamic/private';
2
+ import { timingSafeEqual } from 'crypto';
3
+
4
+ /**
5
+ * Validate Bearer token authentication against the API_KEY environment variable.
6
+ * Returns a 401 Response if auth fails, or null if auth passed.
7
+ */
8
+ export function validateAuth(request: Request): null | Response {
9
+ const auth = request.headers.get('Authorization') || request.headers.get('authorization');
10
+ if (!auth?.startsWith('Bearer ')) {
11
+ return new Response(JSON.stringify({ error: 'Missing authorization' }), {
12
+ headers: { 'Content-Type': 'application/json' },
13
+ status: 401,
14
+ });
15
+ }
16
+ const token = auth.slice(7);
17
+ const expectedKey = env.API_KEY?.trim();
18
+ if (!expectedKey) {
19
+ return new Response(JSON.stringify({ error: 'Invalid API key' }), {
20
+ headers: { 'Content-Type': 'application/json' },
21
+ status: 401,
22
+ });
23
+ }
24
+ // Timing-safe comparison to prevent timing attacks on the API key.
25
+ const tokenBuf = Buffer.from(token);
26
+ const expectedBuf = Buffer.from(expectedKey);
27
+ if (tokenBuf.length !== expectedBuf.length || !timingSafeEqual(tokenBuf, expectedBuf)) {
28
+ return new Response(JSON.stringify({ error: 'Invalid API key' }), {
29
+ headers: { 'Content-Type': 'application/json' },
30
+ status: 401,
31
+ });
32
+ }
33
+ return null; // auth passed
34
+ }