sliccy 4.28.2 → 4.29.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 (235) hide show
  1. package/README.md +17 -16
  2. package/dist/node-server/bridge-security.d.ts +26 -0
  3. package/dist/node-server/bridge-security.js +41 -2
  4. package/dist/node-server/chrome-launch.d.ts +62 -1
  5. package/dist/node-server/chrome-launch.js +168 -2
  6. package/dist/node-server/electron-controller.d.ts +115 -5
  7. package/dist/node-server/electron-controller.js +167 -28
  8. package/dist/node-server/electron-main.js +37 -2
  9. package/dist/node-server/electron-runtime.d.ts +0 -2
  10. package/dist/node-server/electron-runtime.js +63 -68
  11. package/dist/node-server/fetch-proxy-headers.d.ts +25 -0
  12. package/dist/node-server/fetch-proxy-headers.js +36 -0
  13. package/dist/node-server/index.js +46 -11
  14. package/dist/node-server/routes/fetch-proxy.js +12 -12
  15. package/dist/ui/assets/account-store-lwRLg8qZ.js +38 -0
  16. package/dist/ui/assets/account-store-o8Dx0iny.js +99 -0
  17. package/dist/ui/assets/adobe-oauth-state-DJ7nvXJJ.js +1 -0
  18. package/dist/ui/assets/adobe-oauth-state-fjk_YXgL.js +1 -0
  19. package/dist/ui/assets/adobe-t0y7yh0d.js +2 -0
  20. package/dist/ui/assets/adobe-w6Q7iSKn.js +1 -0
  21. package/dist/ui/assets/{agent-message-to-chat-DQErB0Zj.js → agent-message-to-chat-yezUzugT.js} +1 -1
  22. package/dist/ui/assets/{anthropic-BaI_sJYa.js → anthropic-DwYAPtk5.js} +1 -1
  23. package/dist/ui/assets/{apps-BAXGsVHu.js → apps-D-6_Des4.js} +1 -1
  24. package/dist/ui/assets/{azure-openai-CKd81QhZ.js → azure-openai-BUQWXp4U.js} +1 -1
  25. package/dist/ui/assets/{azure-openai-DTsNmcxo.js → azure-openai-Cza179on.js} +1 -1
  26. package/dist/ui/assets/{azure-openai-responses-CMjHELHX.js → azure-openai-responses-Bnxym5u-.js} +1 -1
  27. package/dist/ui/assets/backend-local-mrUgrni5.js +9 -0
  28. package/dist/ui/assets/{bedrock-camp-DS1dgU6g.js → bedrock-camp-C1lmRgcD.js} +2 -2
  29. package/dist/ui/assets/bsh-watchdog-aV3rA0w_.js +60 -0
  30. package/dist/ui/assets/cdp-bridge-CUqQUcaF.js +1 -0
  31. package/dist/ui/assets/{cdp-D4JkCTey.js → cdp-z9uzg44m.js} +5 -5
  32. package/dist/ui/assets/composer-speech-BJF-32Qm.js +1 -0
  33. package/dist/ui/assets/{connect-surface-CyQMJFj3.js → connect-surface-v46dKJYB.js} +1 -1
  34. package/dist/ui/assets/dip-Iosy_HJh.js +1 -0
  35. package/dist/ui/assets/{dist-CVvNIY9j.js → dist-BHeXHQU_.js} +1 -1
  36. package/dist/ui/assets/{dist-Ciobg3J4.js → dist-Fdgn9mju.js} +2 -2
  37. package/dist/ui/assets/ensure-speech-assets-CILvdvdL.js +1 -0
  38. package/dist/ui/assets/{es-DjQZltrp.js → es-Z-wNZ0C3.js} +4 -4
  39. package/dist/ui/assets/esp8266-CL1TORn3.js +1 -0
  40. package/dist/ui/assets/{esptool-operations-SV6FPHJC.js → esptool-operations-CYdcX9Mk.js} +2 -2
  41. package/dist/ui/assets/extension-bridge-transport-DYOF2OYC.js +1 -0
  42. package/dist/ui/assets/fetch-body-D2PwksUW.js +1 -0
  43. package/dist/ui/assets/fetch-body-DDu_AU5I.js +1 -0
  44. package/dist/ui/assets/{fs-Bv2avR3Y.js → fs-CruOlxoS.js} +2 -2
  45. package/dist/ui/assets/{fs-B-yB8nmS.js → fs-crZm8xnS.js} +1 -1
  46. package/dist/ui/assets/github-CQtjJB1r.js +2 -0
  47. package/dist/ui/assets/github-FUziE_96.js +1 -0
  48. package/dist/ui/assets/{github-copilot--B7IUER8.js → github-copilot-DOj1kxaL.js} +1 -1
  49. package/dist/ui/assets/{github-copilot-CFeOxiAY.js → github-copilot-yTNPvKEy.js} +1 -1
  50. package/dist/ui/assets/{google-yB8wbiw4.js → google-CSaY-DQE.js} +1 -1
  51. package/dist/ui/assets/{google-shared-CeGHiQ17.js → google-shared-RiqqHuiz.js} +1 -1
  52. package/dist/ui/assets/{google-vertex-Dbruz2uo.js → google-vertex-CBHKHWKe.js} +1 -1
  53. package/dist/ui/assets/hear-COc8-jz9.js +1 -0
  54. package/dist/ui/assets/hear-Ca-dJ63I.js +1 -0
  55. package/dist/ui/assets/hid-picker-DUJBI46p.js +1 -0
  56. package/dist/ui/assets/{intercepted-oauth-tLpKagkf.js → intercepted-oauth-E9BfBX02.js} +1 -1
  57. package/dist/ui/assets/js-realm-worker-BIJifCVd.js +47 -0
  58. package/dist/ui/assets/{json-parse-BIGBjpLL.js → json-parse-CWht3lea.js} +1 -1
  59. package/dist/ui/assets/kernel-worker-CFGgP28u.js +10060 -0
  60. package/dist/ui/assets/{kokoro-IuvgZkZk.js → kokoro-B5jJ2WUJ.js} +2 -2
  61. package/dist/ui/assets/{kokoro-pZaPWf_-.js → kokoro-BdpwC7DT.js} +2 -2
  62. package/dist/ui/assets/kokoro-engine-io4cnyoV.js +1 -0
  63. package/dist/ui/assets/{lick-ws-bridge-CbVQgXpI.js → lick-ws-bridge-s5Y2i_Ua.js} +1 -1
  64. package/dist/ui/assets/{local-llm-FEU-X2iM.js → local-llm-CKo8wJkz.js} +1 -1
  65. package/dist/ui/assets/magick-wasm-oRIuPV7a.js +43 -0
  66. package/dist/ui/assets/main-CY_go8Ex.js +24 -0
  67. package/dist/ui/assets/{main-cherry-B1mI8ArD.js → main-cherry-BRV9Q0tu.js} +1 -1
  68. package/dist/ui/assets/{mistral-CJVvXrOL.js → mistral-BTNTllXI.js} +1 -1
  69. package/dist/ui/assets/mount-DlwSk_tU.js +1 -0
  70. package/dist/ui/assets/mount-bridge-client-DDnAgZEL.js +2 -0
  71. package/dist/ui/assets/mount-id-_7_PCY9F.js +1 -0
  72. package/dist/ui/assets/mount-pc-SiJqJ.js +2 -0
  73. package/dist/ui/assets/mount-picker-popup-Hrvzr69c.js +1 -0
  74. package/dist/ui/assets/{mount-table-store-CQJEV6Ij.js → mount-table-store-ggb440m4.js} +1 -1
  75. package/dist/ui/assets/new-session-CfjGoE_n.js +2 -0
  76. package/dist/ui/assets/oauth-bootstrap-Le_gIjYT.js +2 -0
  77. package/dist/ui/assets/oauth-service-D4iuqFv6.js +1 -0
  78. package/dist/ui/assets/oauth-service-YGl8VFS-.js +1 -0
  79. package/dist/ui/assets/{openai-codex-BpHxok9y.js → openai-codex-CU65yKca.js} +1 -1
  80. package/dist/ui/assets/{openai-codex-DIIqvcoZ.js → openai-codex-Dx5taSaF.js} +1 -1
  81. package/dist/ui/assets/{openai-codex-responses-CLrAtex4.js → openai-codex-responses-7JN1dUxK.js} +1 -1
  82. package/dist/ui/assets/{openai-completions-CyC3I8Ls.js → openai-completions-YNL_qr9N.js} +1 -1
  83. package/dist/ui/assets/{openai-responses-D3BggRn6.js → openai-responses-nczmKECx.js} +1 -1
  84. package/dist/ui/assets/{openai-responses-shared-GJLWYx-P.js → openai-responses-shared-9YVaeX5D.js} +1 -1
  85. package/dist/ui/assets/panel-responder-BKgfV-nl.js +1 -0
  86. package/dist/ui/assets/{panel-rpc-Ck8CVyMh.js → panel-rpc-D0J5pskz.js} +1 -1
  87. package/dist/ui/assets/panel-rpc-handlers-BFGsncpn.js +2 -0
  88. package/dist/ui/assets/{picker-approval-tzbkMw-e.js → picker-approval-Dc390nC7.js} +1 -1
  89. package/dist/ui/assets/picker-popup-CpulHQsi.js +1 -0
  90. package/dist/ui/assets/preview-vfs-responder-CSDC7Oy2.js +1 -0
  91. package/dist/ui/assets/provider-Lb9gUmQt.js +2 -0
  92. package/dist/ui/assets/provider-store-access-COfmBqcb.js +1 -0
  93. package/dist/ui/assets/provider-store-access-CjPYCH1J.js +1 -0
  94. package/dist/ui/assets/{provider-UGTRh0sb.js → provider-vMORCKxQ.js} +1 -1
  95. package/dist/ui/assets/{providers-C-thqKYx.js → providers-CkeJGhLV.js} +1 -1
  96. package/dist/ui/assets/proxied-fetch-CKDBmEMl.js +2 -0
  97. package/dist/ui/assets/proxied-fetch-CM0jk_Mr.js +1 -0
  98. package/dist/ui/assets/py-realm-worker-hOFi8vjM.js +242 -0
  99. package/dist/ui/assets/{pyodide-BvA4c-59.js → pyodide-CqQhZelL.js} +2 -2
  100. package/dist/ui/assets/{quick-llm-pCxnbK4A.js → quick-llm-CksvHcNV.js} +1 -1
  101. package/dist/ui/assets/{remote-cache-BffIqHgi.js → remote-cache-BL6NRenp.js} +1 -1
  102. package/dist/ui/assets/remote-terminal-view-Qv9yhXQm.js +5 -0
  103. package/dist/ui/assets/remote-vfs-client-Z2qM01yP.js +1 -0
  104. package/dist/ui/assets/secret-env-DncfFSea.js +1 -0
  105. package/dist/ui/assets/secret-topology-bwXIzzAK.js +1 -0
  106. package/dist/ui/assets/secrets-bridge-client-BSipj-5f.js +2 -0
  107. package/dist/ui/assets/serial-picker-B6s4NeaB.js +1 -0
  108. package/dist/ui/assets/session-freezer-1tkfzApv.js +1 -0
  109. package/dist/ui/assets/shared-Bn9ltvMZ.js +1 -0
  110. package/dist/ui/assets/speak-Bri-_DZL.js +1 -0
  111. package/dist/ui/assets/speak-D9WnbzxD.js +2 -0
  112. package/dist/ui/assets/speech-assets-bridge-B8NqCAE8.js +1 -0
  113. package/dist/ui/assets/speech-assets-bridge-BUDIvdid.js +1 -0
  114. package/dist/ui/assets/{sprinkle-bridge-DrAUCEC_.js → sprinkle-bridge-Du2dCM8G.js} +1 -1
  115. package/dist/ui/assets/{sprinkle-manager-C3_2AgeR.js → sprinkle-manager-BhhaIwAz.js} +1 -1
  116. package/dist/ui/assets/{sql-wasm-Cu5Uk_6k.js → sql-wasm-BWbkWL23.js} +1 -1
  117. package/dist/ui/assets/{store-DSwnBJEJ.js → store-iS53cbT5.js} +1 -1
  118. package/dist/ui/assets/sudo-DH2Ru5S3.js +5 -0
  119. package/dist/ui/assets/tool-ui-DfXu6quJ.js +1 -0
  120. package/dist/ui/assets/tool-ui-t6ocn6AG.js +1 -0
  121. package/dist/ui/assets/transformers-env-2Uj9Fgd2.js +1 -0
  122. package/dist/ui/assets/transformers-env-CgaV7qpU.js +1 -0
  123. package/dist/ui/assets/transformers.web-BxHOGVSB.js +45 -0
  124. package/dist/ui/assets/transformers.web-CuUo4MLm.js +39 -0
  125. package/dist/ui/assets/{tray-leave-runtime-Dk3OSbMO.js → tray-leave-runtime-ND08HLeb.js} +1 -1
  126. package/dist/ui/assets/{upgrade-detection-Dcc69AtK.js → upgrade-detection-Y3TdGYus.js} +1 -1
  127. package/dist/ui/assets/usb-picker-D_K0weAY.js +1 -0
  128. package/dist/ui/assets/{voice-reply-UcYXGV0I.js → voice-reply-DfKSbOma.js} +1 -1
  129. package/dist/ui/assets/wc-attach-BvytbCpS.js +13 -0
  130. package/dist/ui/assets/{wc-detached-CJFrvQ5w.js → wc-detached-Bw0mLsiP.js} +1 -1
  131. package/dist/ui/assets/wc-extension-DS-jtOJw.js +2 -0
  132. package/dist/ui/assets/wc-live-B3kXelNl.js +253 -0
  133. package/dist/ui/assets/wc-nav-tMI3vvSL.js +2 -0
  134. package/dist/ui/assets/wc-onboarding-Dm8BUVRJ.js +2 -0
  135. package/dist/ui/assets/wc-permissions-mVh0SOdZ.js +3 -0
  136. package/dist/ui/assets/wc-permissions-providers-BufEXbhX.js +2 -0
  137. package/dist/ui/assets/wc-permissions-registry-CR1rTd2H.js +1 -0
  138. package/dist/ui/assets/wc-permissions-registry-Dx-BRiKK.js +1 -0
  139. package/dist/ui/assets/wc-placeholder-DEo1-1V5.js +4 -0
  140. package/dist/ui/assets/wc-settings-D3QCZnur.js +24 -0
  141. package/dist/ui/assets/{wc-shell-BGaEWRGh.js → wc-shell-DgOdmKyR.js} +603 -101
  142. package/dist/ui/assets/wc-sprinkles-D5si03_4.js +2 -0
  143. package/dist/ui/assets/wc-tray-lTqtwMFb.js +5 -0
  144. package/dist/ui/assets/whisper-engine-Emfsg6Pq.js +1 -0
  145. package/dist/ui/assets/whisper-session-Dt-B3KmT.js +2 -0
  146. package/dist/ui/assets/worker-D1KsWRGg.js +1 -1
  147. package/dist/ui/assets/writable-vfs-client-BKZGifKZ.js +1 -0
  148. package/dist/ui/assets/{xai-grok-BX1SWx3y.js → xai-grok-D4a_2yZf.js} +1 -1
  149. package/dist/ui/assets/{xai-grok-CggP425A.js → xai-grok-Dpq_7ZV8.js} +1 -1
  150. package/dist/ui/electron-overlay-entry.css +37 -0
  151. package/dist/ui/electron-overlay-entry.js +5853 -222
  152. package/dist/ui/index.html +7 -3
  153. package/dist/ui/llm-proxy-sw.js +1 -1
  154. package/dist/ui/packages/webapp/index.html +7 -3
  155. package/dist/ui/preview-sw.js +1 -1
  156. package/package.json +6 -1
  157. package/dist/ui/assets/__vite-browser-external-CM3Eme-n.js +0 -1
  158. package/dist/ui/assets/account-store-Dha2nLml.js +0 -38
  159. package/dist/ui/assets/account-store-DmjUSua_.js +0 -99
  160. package/dist/ui/assets/adobe-CpNl5eLk.js +0 -2
  161. package/dist/ui/assets/adobe-CtI7bDCt.js +0 -1
  162. package/dist/ui/assets/backend-local-CMfz45hs.js +0 -9
  163. package/dist/ui/assets/biome_wasm-DEjrJMqV.js +0 -1
  164. package/dist/ui/assets/bsh-watchdog-B27e-Og-.js +0 -74
  165. package/dist/ui/assets/cdn-url-builder-Cq7LLgru.js +0 -1
  166. package/dist/ui/assets/chunk-CMxvf4Kt.js +0 -1
  167. package/dist/ui/assets/common-ewBM5he1.js +0 -1
  168. package/dist/ui/assets/composer-speech-DYshRh0w.js +0 -1
  169. package/dist/ui/assets/dip-De30SVSa.js +0 -1
  170. package/dist/ui/assets/dist-DElqKLgF.js +0 -43
  171. package/dist/ui/assets/esp32-BTVUAltJ.js +0 -1
  172. package/dist/ui/assets/esp32c2-Cvhq9547.js +0 -1
  173. package/dist/ui/assets/esp32c3-D9lOBWYZ.js +0 -1
  174. package/dist/ui/assets/esp32c5-D2poFXyG.js +0 -1
  175. package/dist/ui/assets/esp32c6-Ct3LxBMw.js +0 -1
  176. package/dist/ui/assets/esp32c61-BsudQf3e.js +0 -1
  177. package/dist/ui/assets/esp32h2-CPO25efF.js +0 -1
  178. package/dist/ui/assets/esp32p4-GHiT8voO.js +0 -1
  179. package/dist/ui/assets/esp32s2-DrH4e7iJ.js +0 -1
  180. package/dist/ui/assets/esp32s3-BHbeScNz.js +0 -1
  181. package/dist/ui/assets/esp8266-DNz2CP4r.js +0 -1
  182. package/dist/ui/assets/github-DRvbws57.js +0 -2
  183. package/dist/ui/assets/github-DcwUCUyU.js +0 -1
  184. package/dist/ui/assets/hear-20YdcW5b.js +0 -1
  185. package/dist/ui/assets/hear-BOsXTls8.js +0 -1
  186. package/dist/ui/assets/js-realm-worker-BWFis6jC.js +0 -5
  187. package/dist/ui/assets/kernel-worker-DuaByjq4.js +0 -9820
  188. package/dist/ui/assets/kokoro-engine-CHIq9iyX.js +0 -2
  189. package/dist/ui/assets/kokoro-engine-CjyxHaAM.js +0 -1
  190. package/dist/ui/assets/magick-wasm-BvNy9LFP.js +0 -1
  191. package/dist/ui/assets/main-D-3iO2Pb.js +0 -24
  192. package/dist/ui/assets/mount-B60ugIMY.js +0 -1
  193. package/dist/ui/assets/mount-TPowGzXN.js +0 -2
  194. package/dist/ui/assets/mount-id-DtHfwjbZ.js +0 -1
  195. package/dist/ui/assets/mount-picker-popup-BORxgnGu.js +0 -1
  196. package/dist/ui/assets/new-session-BjrjaQDu.js +0 -2
  197. package/dist/ui/assets/nodejs-BQ9Oov1A.js +0 -1
  198. package/dist/ui/assets/oauth-bootstrap-D15vXH8k.js +0 -2
  199. package/dist/ui/assets/oauth-service-CoJ-pYVD.js +0 -1
  200. package/dist/ui/assets/oauth-service-D49nt7eJ.js +0 -1
  201. package/dist/ui/assets/panel-rpc-handlers-Ca3yEyOL.js +0 -2
  202. package/dist/ui/assets/preview-vfs-responder-dyQBWIl5.js +0 -1
  203. package/dist/ui/assets/provider-CT4MGofi.js +0 -2
  204. package/dist/ui/assets/provider-store-access-BHHITTJF.js +0 -1
  205. package/dist/ui/assets/provider-store-access-BSS9maA7.js +0 -1
  206. package/dist/ui/assets/proxied-fetch-BBO8HwcV.js +0 -1
  207. package/dist/ui/assets/py-realm-worker-BnG_DxBd.js +0 -242
  208. package/dist/ui/assets/remote-terminal-view-CSqiig2G.js +0 -6
  209. package/dist/ui/assets/remote-vfs-client-BwJhhtIV.js +0 -1
  210. package/dist/ui/assets/secret-env-DcfTe28K.js +0 -1
  211. package/dist/ui/assets/session-freezer-NFd-CALu.js +0 -1
  212. package/dist/ui/assets/setup-sudo-KPDfIsQ7.js +0 -1
  213. package/dist/ui/assets/shared-B7TdM-Ik.js +0 -1
  214. package/dist/ui/assets/speak-D511iTBd.js +0 -1
  215. package/dist/ui/assets/speak-EKtIqp6j.js +0 -1
  216. package/dist/ui/assets/sudo-B4yfntyh.js +0 -4
  217. package/dist/ui/assets/tool-ui-N76-EdVc.js +0 -1
  218. package/dist/ui/assets/tool-ui-YOPqzMmL.js +0 -1
  219. package/dist/ui/assets/transformers-env-6vj5CcH9.js +0 -1
  220. package/dist/ui/assets/transformers-env-O5Vtz_xj.js +0 -1
  221. package/dist/ui/assets/transformers.web-C9_H_wuQ.js +0 -37
  222. package/dist/ui/assets/transformers.web-DJfCkvxA.js +0 -43
  223. package/dist/ui/assets/typescript-qBO0Z4wm.js +0 -448
  224. package/dist/ui/assets/wc-attach-B62W7N_T.js +0 -13
  225. package/dist/ui/assets/wc-extension-C1CIBnT2.js +0 -2
  226. package/dist/ui/assets/wc-live-B3Dxlxlf.js +0 -253
  227. package/dist/ui/assets/wc-nav-DWMYNnDA.js +0 -2
  228. package/dist/ui/assets/wc-onboarding-DpKk5u_V.js +0 -2
  229. package/dist/ui/assets/wc-placeholder-CGPP_IEm.js +0 -4
  230. package/dist/ui/assets/wc-settings-DjqUSzpO.js +0 -24
  231. package/dist/ui/assets/wc-sprinkles-B5_aCIM5.js +0 -2
  232. package/dist/ui/assets/wc-tray-kVPhrwG1.js +0 -5
  233. package/dist/ui/assets/web-BMWiUYJ-.js +0 -1
  234. package/dist/ui/assets/whisper-session-D89gZWHf.js +0 -2
  235. package/dist/ui/assets/writable-vfs-client-DMYsajEE.js +0 -1
@@ -6,9 +6,67 @@ import * as https from 'https';
6
6
  import { promisify } from 'util';
7
7
  import { WebSocket } from 'ws';
8
8
  import { inflateSync } from 'zlib';
9
- import { buildElectronAppLaunchSpec, buildElectronOverlayAppUrl, buildElectronOverlayBootstrapScript, buildElectronOverlayEntryUrl, getElectronOverlayEntryDistPath, getElectronServeOrigin, selectBestOverlayTargets, } from './electron-runtime.js';
9
+ import { BRIDGE_TOKEN_QUERY_PARAM, BRIDGE_WS_QUERY_PARAM } from './bridge-security.js';
10
+ import { buildElectronAppLaunchSpec, buildElectronOverlayBootstrapScript, buildElectronOverlayEntryUrl, ELECTRON_OVERLAY_APP_PATH, getElectronOverlayEntryDistPath, getElectronServeOrigin, selectBestOverlayTargets, } from './electron-runtime.js';
10
11
  const execFile = promisify(nodeExecFile);
11
12
  const ELECTRON_OVERLAY_SYNC_INTERVAL_MS = 1500;
13
+ /**
14
+ * Query param name used to mark the role of an overlay tab on the hosted
15
+ * launcher URL. The pinned leader carries `role=leader`; auto-follow
16
+ * followers carry `role=follower`. The hosted webapp interprets these to
17
+ * decide which tab anchors the CDP bridge vs. follows the leader.
18
+ */
19
+ export const BRIDGE_ROLE_QUERY_PARAM = 'role';
20
+ export const BRIDGE_ROLE_LEADER = 'leader';
21
+ export const BRIDGE_ROLE_FOLLOWER = 'follower';
22
+ /**
23
+ * Build the thin-bridge config for the Electron overlay injector. The
24
+ * hosted-leader origin defaults to production `https://www.sliccy.ai`
25
+ * (overridable via `SLICC_HOSTED_LEADER_ORIGIN` / `WORKER_BASE_URL`), so
26
+ * the only genuinely unresolvable case is a missing per-process bridge
27
+ * token — in which case this returns `null` and the caller fails fast
28
+ * rather than falling back to a (now-retired) bundled overlay.
29
+ */
30
+ export function resolveOverlayThinBridge(env, bridgeToken, servePort) {
31
+ if (!bridgeToken)
32
+ return null;
33
+ return {
34
+ hostedLeaderOrigin: resolveHostedLeaderOrigin(env),
35
+ bridgeWsUrl: `ws://localhost:${servePort}/cdp`,
36
+ bridgeToken,
37
+ };
38
+ }
39
+ /**
40
+ * Build the hosted launcher URL for an overlay injection. Mirrors the
41
+ * standalone Path A launch-URL shape (`bridge`, `bridgeToken` query
42
+ * params) with one Electron-specific addition: a `role` param that pins
43
+ * the first injected tab as the leader and marks every subsequent tab as
44
+ * an auto-follow follower.
45
+ */
46
+ export function buildThinOverlayAppUrl(opts) {
47
+ const url = new URL(ELECTRON_OVERLAY_APP_PATH, opts.hostedLeaderOrigin);
48
+ url.searchParams.set(BRIDGE_WS_QUERY_PARAM, opts.bridgeWsUrl);
49
+ url.searchParams.set(BRIDGE_TOKEN_QUERY_PARAM, opts.bridgeToken);
50
+ url.searchParams.set(BRIDGE_ROLE_QUERY_PARAM, opts.role);
51
+ if (opts.activeTab && opts.activeTab !== 'chat') {
52
+ url.searchParams.set('tab', opts.activeTab);
53
+ }
54
+ return url.toString();
55
+ }
56
+ /**
57
+ * Resolve the hosted leader origin Chrome / Electron should open in thin
58
+ * mode. Prefers explicit overrides (`SLICC_HOSTED_LEADER_ORIGIN`, then
59
+ * `WORKER_BASE_URL`) so dev can point at staging; defaults to production
60
+ * `https://www.sliccy.ai`. Trailing slashes are stripped so callers can
61
+ * safely concatenate paths.
62
+ */
63
+ export function resolveHostedLeaderOrigin(env = process.env) {
64
+ const explicit = env['SLICC_HOSTED_LEADER_ORIGIN'] ?? env['WORKER_BASE_URL'];
65
+ if (explicit) {
66
+ return explicit.replace(/\/+$/, '');
67
+ }
68
+ return 'https://www.sliccy.ai';
69
+ }
12
70
  function commandLineExecutableMatchesPattern(commandLine, pattern) {
13
71
  // Extract the executable (first whitespace-separated token) from the command line.
14
72
  // Only match when the target app path is the executable itself, not an argument —
@@ -427,28 +485,72 @@ function buildFulfillResponseHeaders(rawHeaders, contentLength) {
427
485
  }
428
486
  return { responseHeaders, strippedCSP };
429
487
  }
488
+ /**
489
+ * JS probe that reports whether the overlay iframe actually loaded. Walks the
490
+ * `<slicc-launcher>` host's (open) shadow root to find the iframe
491
+ * depth-agnostically, then classifies by cross-origin reachability: the
492
+ * thin-bridge overlay is ALWAYS a different origin (hosted webapp) than the
493
+ * app document, so a committed cross-origin navigation makes
494
+ * `iframe.contentWindow.location.href` THROW — that throw is the ONLY success
495
+ * signal. Any READABLE href (`about:blank`, `''`, or a CSP-blocked swap to
496
+ * `chrome-error://chromewebdata/`) means the cross-origin nav did NOT commit,
497
+ * so the overlay did not load and the setBypassCSP escalation must fire.
498
+ * Returns `'ok'` only from the catch; otherwise `'no-host' / 'no-iframe' /
499
+ * 'no-src' / 'blank:<href>'`.
500
+ */
501
+ export const OVERLAY_LOADED_PROBE_EXPRESSION = `(function() {
502
+ var host = document.getElementById('slicc-electron-overlay-root');
503
+ if (!host || !host.shadowRoot) return 'no-host';
504
+ var iframe = host.shadowRoot.querySelector('iframe');
505
+ if (!iframe) return 'no-iframe';
506
+ if (!iframe.src) return 'no-src';
507
+ try {
508
+ // Thin-bridge overlay is ALWAYS cross-origin (hosted webapp) vs the app
509
+ // document. A committed cross-origin navigation makes this access THROW.
510
+ // Any READABLE href means the cross-origin nav did NOT commit — still
511
+ // about:blank, or swapped to chrome-error://chromewebdata/ by a CSP block —
512
+ // so the overlay did NOT load and the setBypassCSP escalation must fire.
513
+ var href = iframe.contentWindow && iframe.contentWindow.location ? iframe.contentWindow.location.href : '';
514
+ return 'blank:' + href;
515
+ } catch (e) {
516
+ return 'ok';
517
+ }
518
+ })()`;
430
519
  export class ElectronOverlayInjector {
431
520
  cdpPort;
432
521
  servePort;
433
- bootstrapScript;
522
+ /** Thin-mode bootstrap pair — the only overlay path. */
523
+ thinBootstraps;
434
524
  probeDelayMs;
435
525
  connections = new Map();
436
526
  cspBypassedTargets = new Set();
527
+ /**
528
+ * URL of the target currently elected as the pinned leader. Cleared by
529
+ * `syncTargets` when that target disappears so the next injection
530
+ * re-elects a fresh leader.
531
+ */
532
+ leaderTargetUrl = null;
437
533
  syncTimer = null;
438
534
  syncing = false;
439
- constructor(cdpPort, servePort, bootstrapScript, probeDelayMs = 1500) {
535
+ constructor(cdpPort, servePort, thinBootstraps, probeDelayMs = 1500) {
440
536
  this.cdpPort = cdpPort;
441
537
  this.servePort = servePort;
442
- this.bootstrapScript = bootstrapScript;
538
+ this.thinBootstraps = thinBootstraps;
443
539
  this.probeDelayMs = probeDelayMs;
444
540
  }
445
541
  static async create(options) {
446
542
  const bundleSource = await loadElectronOverlayBundleSource(options);
447
- const bootstrapScript = buildElectronOverlayBootstrapScript({
448
- bundleSource,
449
- appUrl: buildElectronOverlayAppUrl(getElectronServeOrigin(options.servePort)),
450
- });
451
- return new ElectronOverlayInjector(options.cdpPort, options.servePort, bootstrapScript);
543
+ const thinBootstraps = {
544
+ leader: buildElectronOverlayBootstrapScript({
545
+ bundleSource,
546
+ appUrl: buildThinOverlayAppUrl({ ...options.thinBridge, role: BRIDGE_ROLE_LEADER }),
547
+ }),
548
+ follower: buildElectronOverlayBootstrapScript({
549
+ bundleSource,
550
+ appUrl: buildThinOverlayAppUrl({ ...options.thinBridge, role: BRIDGE_ROLE_FOLLOWER }),
551
+ }),
552
+ };
553
+ return new ElectronOverlayInjector(options.cdpPort, options.servePort, thinBootstraps);
452
554
  }
453
555
  /**
454
556
  * Test-only factory: skips bundle loading and lets tests drive the per-target
@@ -456,7 +558,15 @@ export class ElectronOverlayInjector {
456
558
  * `_testing_*` hooks on `ElectronOverlayInjector`.
457
559
  */
458
560
  static _createForTesting(options) {
459
- return new ElectronOverlayInjector(options.cdpPort ?? 9223, options.servePort, options.bootstrapScript ?? '/* test-noop */', options.probeDelayMs ?? 1500);
561
+ return new ElectronOverlayInjector(options.cdpPort ?? 9223, options.servePort, options.thinBootstraps ?? { leader: '/* test-leader */', follower: '/* test-follower */' }, options.probeDelayMs ?? 1500);
562
+ }
563
+ /** Test-only: snapshot the elected leader target URL (null when no leader). */
564
+ _testingLeaderTargetUrl() {
565
+ return this.leaderTargetUrl;
566
+ }
567
+ /** Test-only: seed the elected leader (drives the follower-election path). */
568
+ _testingSeedLeaderTargetUrl(url) {
569
+ this.leaderTargetUrl = url;
460
570
  }
461
571
  /** Test-only: drive the per-target connect flow without going through `start`. */
462
572
  _testingConnectToTarget(target) {
@@ -470,6 +580,10 @@ export class ElectronOverlayInjector {
470
580
  _testingBypassedTargets() {
471
581
  return new Set(this.cspBypassedTargets);
472
582
  }
583
+ /** Test-only: drive a single `syncTargets` pass without `start()`'s interval. */
584
+ async _testingSyncTargets() {
585
+ await this.syncTargets();
586
+ }
473
587
  /** Test-only: close any sockets opened by `_testingConnectToTarget`. */
474
588
  _testingCloseConnections() {
475
589
  for (const connection of this.connections.values()) {
@@ -522,6 +636,16 @@ export class ElectronOverlayInjector {
522
636
  }
523
637
  }
524
638
  const liveConnectionIds = new Set(injectableTargets.map((target) => target.webSocketDebuggerUrl));
639
+ // Thin mode: drop the elected leader if its target is no longer
640
+ // present so the next injection re-elects. Without this a stale
641
+ // leaderTargetUrl would block every future tab from becoming the
642
+ // pinned leader after the original leader closed.
643
+ if (this.leaderTargetUrl !== null) {
644
+ const liveTargetUrls = new Set(injectableTargets.map((target) => target.url));
645
+ if (!liveTargetUrls.has(this.leaderTargetUrl)) {
646
+ this.leaderTargetUrl = null;
647
+ }
648
+ }
525
649
  for (const [targetId, connection] of this.connections.entries()) {
526
650
  if (liveConnectionIds.has(targetId))
527
651
  continue;
@@ -549,22 +673,20 @@ export class ElectronOverlayInjector {
549
673
  }
550
674
  }
551
675
  /**
552
- * Check if the overlay iframe loaded successfully by evaluating a probe script.
553
- * Returns true if the iframe element exists and has started loading content.
676
+ * Check if the overlay iframe loaded successfully by evaluating a probe
677
+ * script. Walks the `<slicc-launcher>` host's (open) shadow root to find the
678
+ * iframe depth-agnostically and classifies by cross-origin reachability: the
679
+ * thin-bridge overlay is ALWAYS a different origin than the app document, so
680
+ * only a THROW on `iframe.contentWindow.location.href` (a committed
681
+ * cross-origin navigation) counts as loaded. Any readable href — including a
682
+ * CSP-blocked swap to `chrome-error://chromewebdata/` — means the nav did not
683
+ * commit, so the Fetch-proxy escalation must still fire.
684
+ * See {@link OVERLAY_LOADED_PROBE_EXPRESSION}.
554
685
  */
555
686
  async probeOverlayIframeLoaded(ws, send) {
556
687
  return new Promise((resolve) => {
557
688
  const probeId = send('Runtime.evaluate', {
558
- expression: `(function() {
559
- var host = document.getElementById('slicc-electron-overlay-root');
560
- if (!host || !host.shadowRoot) return 'no-host';
561
- var sidebar = host.shadowRoot.querySelector('slicc-electron-sidebar');
562
- if (!sidebar || !sidebar.shadowRoot) return 'no-sidebar';
563
- var iframe = sidebar.shadowRoot.querySelector('iframe');
564
- if (!iframe) return 'no-iframe';
565
- if (!iframe.src) return 'no-src';
566
- return 'ok';
567
- })()`,
689
+ expression: OVERLAY_LOADED_PROBE_EXPRESSION,
568
690
  awaitPromise: false,
569
691
  returnByValue: true,
570
692
  });
@@ -592,13 +714,30 @@ export class ElectronOverlayInjector {
592
714
  ws.on('message', onMessage);
593
715
  });
594
716
  }
717
+ /**
718
+ * Pick the bootstrap script for `target`, electing the leader on first
719
+ * use when thin-mode is active. Same target URL ↔ same role across
720
+ * reconnects so a page that bounces its CDP session stays the leader
721
+ * (no re-election on transient drops, only on `syncTargets` cleanup).
722
+ */
723
+ resolveBootstrapForTarget(target) {
724
+ if (this.leaderTargetUrl === target.url) {
725
+ return this.thinBootstraps.leader;
726
+ }
727
+ if (this.leaderTargetUrl === null) {
728
+ this.leaderTargetUrl = target.url;
729
+ return this.thinBootstraps.leader;
730
+ }
731
+ return this.thinBootstraps.follower;
732
+ }
595
733
  /**
596
734
  * Build a script that sets the SLICC theme preference in localStorage to
597
- * match the target app's detected theme, then runs the bootstrap.
735
+ * match the target app's detected theme, then runs the bootstrap. The
736
+ * bootstrap is target-specific (leader vs. follower).
598
737
  */
599
- buildThemedBootstrap(theme) {
738
+ buildThemedBootstrap(theme, target) {
600
739
  const themeScript = `try{localStorage.setItem('slicc-theme',${JSON.stringify(theme)})}catch(e){}`;
601
- return `${themeScript}\n${this.bootstrapScript}`;
740
+ return `${themeScript}\n${this.resolveBootstrapForTarget(target)}`;
602
741
  }
603
742
  /**
604
743
  * Handle the initial CDP `ws.on('open', ...)` event for a target: enable
@@ -625,7 +764,7 @@ export class ElectronOverlayInjector {
625
764
  if (ws.readyState !== WebSocket.OPEN)
626
765
  return;
627
766
  send('Runtime.evaluate', {
628
- expression: this.buildThemedBootstrap(theme),
767
+ expression: this.buildThemedBootstrap(theme, target),
629
768
  awaitPromise: false,
630
769
  });
631
770
  });
@@ -642,7 +781,7 @@ export class ElectronOverlayInjector {
642
781
  return;
643
782
  console.log(`[electron-float] Injecting overlay (first attempt, theme=${theme})...`);
644
783
  send('Runtime.evaluate', {
645
- expression: this.buildThemedBootstrap(theme),
784
+ expression: this.buildThemedBootstrap(theme, target),
646
785
  awaitPromise: false,
647
786
  });
648
787
  // After a short delay, probe whether the overlay iframe loaded.
@@ -690,7 +829,7 @@ export class ElectronOverlayInjector {
690
829
  if (ws.readyState !== WebSocket.OPEN)
691
830
  return;
692
831
  send('Runtime.evaluate', {
693
- expression: this.buildThemedBootstrap(theme),
832
+ expression: this.buildThemedBootstrap(theme, target),
694
833
  awaitPromise: false,
695
834
  });
696
835
  });
@@ -3,13 +3,38 @@ import { app, BrowserWindow, nativeTheme, session } from 'electron';
3
3
  import { readFile } from 'fs/promises';
4
4
  import { resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { buildElectronOverlayAppUrl, buildElectronOverlayEntryUrl, buildElectronOverlayInjectionCall, buildElectronServerSpawnConfig, getElectronOverlayEntryDistPath, getElectronServeOrigin, parseElectronFloatFlags, } from './electron-runtime.js';
6
+ import { mintBridgeToken } from './bridge-security.js';
7
+ import { BRIDGE_ROLE_LEADER, buildThinOverlayAppUrl, resolveHostedLeaderOrigin, } from './electron-controller.js';
8
+ import { buildElectronOverlayEntryUrl, buildElectronOverlayInjectionCall, buildElectronServerSpawnConfig, getElectronOverlayEntryDistPath, getElectronServeOrigin, parseElectronFloatFlags, } from './electron-runtime.js';
7
9
  const Dirname = fileURLToPath(new URL('.', import.meta.url));
8
10
  const PROJECT_ROOT = resolve(Dirname, '..', '..');
9
11
  const FLAGS = parseElectronFloatFlags(process.argv.slice(2));
10
12
  const SERVE_ORIGIN = getElectronServeOrigin(FLAGS.servePort);
11
- const OVERLAY_APP_URL = buildElectronOverlayAppUrl(SERVE_ORIGIN);
12
13
  const ELECTRON_PARTITION = 'persist:slicc-electron-float';
14
+ /**
15
+ * Per-process thin-bridge token. The Electron float opens its overlay on
16
+ * the sliccy.ai-hosted launcher (Path B) and the launcher dials back to
17
+ * the spawned CLI server's `/cdp` WebSocket using this token via the
18
+ * `Sec-WebSocket-Protocol` header. The token is also forwarded to the
19
+ * child server as `SLICC_BRIDGE_TOKEN` so a future index.ts gate
20
+ * activation can enforce it without another mint.
21
+ */
22
+ const BRIDGE_TOKEN = mintBridgeToken();
23
+ const HOSTED_LEADER_ORIGIN = resolveHostedLeaderOrigin(process.env);
24
+ const BRIDGE_WS_URL = `ws://localhost:${FLAGS.servePort}/cdp`;
25
+ /**
26
+ * Hosted launcher URL injected into every Electron float `BrowserWindow`.
27
+ * The float owns a single window so its overlay is always the pinned
28
+ * leader; external Electron apps (Slack/Teams etc.) attached via
29
+ * `--electron <app>` go through `ElectronOverlayInjector` which handles
30
+ * its own leader/follower election.
31
+ */
32
+ const OVERLAY_APP_URL = buildThinOverlayAppUrl({
33
+ hostedLeaderOrigin: HOSTED_LEADER_ORIGIN,
34
+ bridgeWsUrl: BRIDGE_WS_URL,
35
+ bridgeToken: BRIDGE_TOKEN,
36
+ role: BRIDGE_ROLE_LEADER,
37
+ });
13
38
  app.commandLine.appendSwitch('remote-debugging-port', String(FLAGS.cdpPort));
14
39
  let cliServerProcess = null;
15
40
  let quitting = false;
@@ -116,6 +141,12 @@ function startCliServer() {
116
141
  env: {
117
142
  ...process.env,
118
143
  PORT: String(FLAGS.servePort),
144
+ // Forward the per-process bridge token to the child server. The
145
+ // current --serve-only mode doesn't gate /cdp on it (THIN_BRIDGE_MODE
146
+ // is false), but the env var is in place so a follow-up index.ts
147
+ // change can flip the gate on without retouching this file.
148
+ SLICC_BRIDGE_TOKEN: BRIDGE_TOKEN,
149
+ SLICC_HOSTED_LEADER_ORIGIN: HOSTED_LEADER_ORIGIN,
119
150
  },
120
151
  stdio: ['ignore', 'pipe', 'pipe'],
121
152
  });
@@ -161,6 +192,10 @@ async function main() {
161
192
  configureElectronSession();
162
193
  cliServerProcess = startCliServer();
163
194
  await waitForServerReady(SERVE_ORIGIN);
195
+ // Print the overlay URL with the bridge token redacted — it's a
196
+ // session capability and must never appear in logs / process listings.
197
+ const sanitizedOverlayUrl = OVERLAY_APP_URL.replace(/([?&])bridgeToken=[^&]+/, '$1bridgeToken=<redacted>');
198
+ console.log(`[electron-float] Hosted overlay URL: ${sanitizedOverlayUrl}`);
164
199
  await createFloatWindow(FLAGS.targetUrl);
165
200
  app.on('activate', () => {
166
201
  if (BrowserWindow.getAllWindows().length === 0) {
@@ -25,7 +25,6 @@ export declare const DEFAULT_ELECTRON_SERVE_PORT = 5710;
25
25
  export declare const DEFAULT_ELECTRON_SERVE_HOST = "localhost";
26
26
  export declare const DEFAULT_ELECTRON_CDP_PORT = 9223;
27
27
  export declare const DEFAULT_ELECTRON_TARGET_URL = "about:blank";
28
- export declare const DEFAULT_ELECTRON_OVERLAY_TAB = "chat";
29
28
  export declare const ELECTRON_OVERLAY_APP_PATH = "/electron";
30
29
  export declare const PORT_HASH_RANGE = 40;
31
30
  /**
@@ -61,7 +60,6 @@ export declare function buildElectronServerSpawnConfig(projectRoot: string, opti
61
60
  nodePath?: string;
62
61
  }): ElectronServerSpawnConfig;
63
62
  export declare function getElectronServeOrigin(servePort: number): string;
64
- export declare function buildElectronOverlayAppUrl(serveOrigin: string, activeTab?: string): string;
65
63
  export declare function buildElectronOverlayEntryUrl(serveOrigin: string): string;
66
64
  export declare function getElectronOverlayEntryDistPath(projectRoot: string): string;
67
65
  export declare function buildElectronOverlayInjectionCall(options: {
@@ -4,7 +4,6 @@ export const DEFAULT_ELECTRON_SERVE_PORT = 5710;
4
4
  export const DEFAULT_ELECTRON_SERVE_HOST = 'localhost';
5
5
  export const DEFAULT_ELECTRON_CDP_PORT = 9223;
6
6
  export const DEFAULT_ELECTRON_TARGET_URL = 'about:blank';
7
- export const DEFAULT_ELECTRON_OVERLAY_TAB = 'chat';
8
7
  export const ELECTRON_OVERLAY_APP_PATH = '/electron';
9
8
  // Port allocation constants
10
9
  export const PORT_HASH_RANGE = 40;
@@ -100,74 +99,77 @@ export function getElectronAppDisplayName(appPath) {
100
99
  }
101
100
  return fileName || trimmedPath;
102
101
  }
103
- export function resolveElectronAppExecutablePath(appPath, platform = process.platform) {
104
- const resolvedAppPath = resolve(appPath);
105
- if (platform === 'darwin' && resolvedAppPath.toLowerCase().endsWith('.app')) {
106
- const macOSDir = join(resolvedAppPath, 'Contents', 'MacOS');
107
- // First try the expected name (app name without .app)
108
- const expectedName = getElectronAppDisplayName(resolvedAppPath);
109
- const expectedPath = join(macOSDir, expectedName);
110
- try {
111
- const stat = statSync(expectedPath);
112
- if (stat.isFile()) {
113
- return expectedPath;
114
- }
115
- }
116
- catch {
117
- // Expected path doesn't exist, scan the directory
102
+ function isExecutableFile(path) {
103
+ try {
104
+ accessSync(path, constants.X_OK);
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ function findMacOSExecutable(macOSDir, expectedName) {
112
+ // First try the expected name (app name without .app)
113
+ const expectedPath = join(macOSDir, expectedName);
114
+ try {
115
+ const stat = statSync(expectedPath);
116
+ if (stat.isFile()) {
117
+ return expectedPath;
118
118
  }
119
- // Scan MacOS directory for the main executable
120
- // Many Electron apps use "Electron" as the executable name
121
- // Prefer known main executable names, filter out helper processes
122
- const helperPatterns = [/helper/i, /crash/i, /gpu/i, /renderer/i, /plugin/i, /utility/i];
123
- try {
124
- const entries = readdirSync(macOSDir);
125
- // Helper to check if a file is executable
126
- const isExecutable = (path) => {
127
- try {
128
- accessSync(path, constants.X_OK);
129
- return true;
130
- }
131
- catch {
132
- return false;
133
- }
134
- };
135
- // First pass: look for "Electron" executable (common in Electron apps)
136
- if (entries.includes('Electron')) {
137
- const electronPath = join(macOSDir, 'Electron');
138
- try {
139
- const stat = statSync(electronPath);
140
- if (stat.isFile() && isExecutable(electronPath)) {
141
- return electronPath;
142
- }
143
- }
144
- catch {
145
- // Continue to next fallback
119
+ }
120
+ catch {
121
+ // Expected path doesn't exist, scan the directory
122
+ }
123
+ // Scan MacOS directory for the main executable
124
+ // Many Electron apps use "Electron" as the executable name
125
+ // Prefer known main executable names, filter out helper processes
126
+ const helperPatterns = [/helper/i, /crash/i, /gpu/i, /renderer/i, /plugin/i, /utility/i];
127
+ try {
128
+ const entries = readdirSync(macOSDir);
129
+ // First pass: look for "Electron" executable (common in Electron apps)
130
+ if (entries.includes('Electron')) {
131
+ const electronPath = join(macOSDir, 'Electron');
132
+ try {
133
+ const stat = statSync(electronPath);
134
+ if (stat.isFile() && isExecutableFile(electronPath)) {
135
+ return electronPath;
146
136
  }
147
137
  }
148
- // Second pass: find first non-helper executable with execute permission
149
- for (const entry of entries) {
150
- // Skip hidden files, scripts, and helper executables
151
- if (entry.startsWith('.') || entry.endsWith('.sh'))
152
- continue;
153
- if (helperPatterns.some((p) => p.test(entry)))
154
- continue;
155
- const entryPath = join(macOSDir, entry);
156
- try {
157
- const stat = statSync(entryPath);
158
- if (stat.isFile() && isExecutable(entryPath)) {
159
- return entryPath;
160
- }
161
- }
162
- catch { }
138
+ catch {
139
+ // Continue to next fallback
163
140
  }
164
141
  }
165
- catch {
166
- // Can't read directory, fall back to expected path
142
+ // Second pass: find first non-helper executable with execute permission
143
+ for (const entry of entries) {
144
+ // Skip hidden files, scripts, and helper executables
145
+ if (entry.startsWith('.') || entry.endsWith('.sh'))
146
+ continue;
147
+ if (helperPatterns.some((p) => p.test(entry)))
148
+ continue;
149
+ const entryPath = join(macOSDir, entry);
150
+ try {
151
+ const stat = statSync(entryPath);
152
+ if (stat.isFile() && isExecutableFile(entryPath)) {
153
+ return entryPath;
154
+ }
155
+ }
156
+ catch { }
167
157
  }
158
+ }
159
+ catch {
160
+ // Can't read directory, fall back to expected path
161
+ }
162
+ return null;
163
+ }
164
+ export function resolveElectronAppExecutablePath(appPath, platform = process.platform) {
165
+ const resolvedAppPath = resolve(appPath);
166
+ if (platform === 'darwin' && resolvedAppPath.toLowerCase().endsWith('.app')) {
167
+ const macOSDir = join(resolvedAppPath, 'Contents', 'MacOS');
168
+ const expectedName = getElectronAppDisplayName(resolvedAppPath);
169
+ const expectedPath = join(macOSDir, expectedName);
168
170
  // Fall back to expected path even if it doesn't exist
169
171
  // (error will be caught later)
170
- return expectedPath;
172
+ return findMacOSExecutable(macOSDir, expectedName) ?? expectedPath;
171
173
  }
172
174
  return resolvedAppPath;
173
175
  }
@@ -247,13 +249,6 @@ export function buildElectronServerSpawnConfig(projectRoot, options) {
247
249
  export function getElectronServeOrigin(servePort) {
248
250
  return `http://${DEFAULT_ELECTRON_SERVE_HOST}:${servePort}`;
249
251
  }
250
- export function buildElectronOverlayAppUrl(serveOrigin, activeTab = DEFAULT_ELECTRON_OVERLAY_TAB) {
251
- const url = new URL(ELECTRON_OVERLAY_APP_PATH, serveOrigin);
252
- if (activeTab && activeTab !== DEFAULT_ELECTRON_OVERLAY_TAB) {
253
- url.searchParams.set('tab', activeTab);
254
- }
255
- return url.toString();
256
- }
257
252
  export function buildElectronOverlayEntryUrl(serveOrigin) {
258
253
  return new URL('/electron-overlay-entry.js', serveOrigin).toString();
259
254
  }
@@ -18,3 +18,28 @@
18
18
  * module load.
19
19
  */
20
20
  export declare const FETCH_PROXY_SKIP_HEADERS: ReadonlySet<string>;
21
+ /**
22
+ * Response-side header names the proxy must NOT copy from the upstream
23
+ * response onto the browser-facing response. The bridge's
24
+ * `createThinBridgeCorsMiddleware` / `buildCorsHeaders` set the
25
+ * authoritative CORS headers for the browser→bridge hop BEFORE this
26
+ * route runs; an upstream that emits its own `access-control-*`
27
+ * (e.g. `huggingface.co` → `*`, GitHub Pages → its own origin) would
28
+ * otherwise `res.setHeader`-clobber the bridge's value, leaving the
29
+ * browser with a CORS mismatch (`*` + `Allow-Credentials: true` is
30
+ * forbidden; a foreign origin obviously doesn't match localhost) and
31
+ * surfacing as an opaque `TypeError: Failed to fetch`. Stripping the
32
+ * full `access-control-*` family keeps the bridge as the sole CORS
33
+ * authority on the local hop. Names are lowercased to match
34
+ * `upstream.headers.forEach` key casing.
35
+ */
36
+ export declare const FETCH_PROXY_SKIP_RESPONSE_HEADERS: ReadonlySet<string>;
37
+ /**
38
+ * Lowercased prefixes whose upstream response headers are also skipped.
39
+ * `access-control-` covers the entire CORS-response family (allow-origin,
40
+ * allow-credentials, allow-methods, allow-headers, expose-headers,
41
+ * max-age, allow-private-network, plus any future entries) so the bridge
42
+ * middleware remains the sole CORS authority. `x-proxy-` is the proxy's
43
+ * own response-marker namespace — never echo an upstream value.
44
+ */
45
+ export declare const FETCH_PROXY_SKIP_RESPONSE_PREFIXES: readonly string[];
@@ -27,5 +27,41 @@ export const FETCH_PROXY_SKIP_HEADERS = new Set([
27
27
  'x-proxy-cookie',
28
28
  'x-proxy-origin',
29
29
  'x-proxy-referer',
30
+ // Kept in sync with `BRIDGE_TOKEN_HEADER` in `bridge-security.ts`
31
+ // (lowercased because Node lowercases incoming request header keys).
30
32
  'x-bridge-token',
31
33
  ]);
34
+ /**
35
+ * Response-side header names the proxy must NOT copy from the upstream
36
+ * response onto the browser-facing response. The bridge's
37
+ * `createThinBridgeCorsMiddleware` / `buildCorsHeaders` set the
38
+ * authoritative CORS headers for the browser→bridge hop BEFORE this
39
+ * route runs; an upstream that emits its own `access-control-*`
40
+ * (e.g. `huggingface.co` → `*`, GitHub Pages → its own origin) would
41
+ * otherwise `res.setHeader`-clobber the bridge's value, leaving the
42
+ * browser with a CORS mismatch (`*` + `Allow-Credentials: true` is
43
+ * forbidden; a foreign origin obviously doesn't match localhost) and
44
+ * surfacing as an opaque `TypeError: Failed to fetch`. Stripping the
45
+ * full `access-control-*` family keeps the bridge as the sole CORS
46
+ * authority on the local hop. Names are lowercased to match
47
+ * `upstream.headers.forEach` key casing.
48
+ */
49
+ export const FETCH_PROXY_SKIP_RESPONSE_HEADERS = new Set([
50
+ 'transfer-encoding',
51
+ 'content-encoding',
52
+ 'content-length',
53
+ 'www-authenticate',
54
+ 'set-cookie',
55
+ ]);
56
+ /**
57
+ * Lowercased prefixes whose upstream response headers are also skipped.
58
+ * `access-control-` covers the entire CORS-response family (allow-origin,
59
+ * allow-credentials, allow-methods, allow-headers, expose-headers,
60
+ * max-age, allow-private-network, plus any future entries) so the bridge
61
+ * middleware remains the sole CORS authority. `x-proxy-` is the proxy's
62
+ * own response-marker namespace — never echo an upstream value.
63
+ */
64
+ export const FETCH_PROXY_SKIP_RESPONSE_PREFIXES = [
65
+ 'access-control-',
66
+ 'x-proxy-',
67
+ ];