@xopcai/xopc 0.0.367 → 0.0.369

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 (263) hide show
  1. package/dist/browser-ext/_locales/en/messages.json +30 -1
  2. package/dist/browser-ext/_locales/zh_CN/messages.json +30 -1
  3. package/dist/browser-ext/dist/assets/{auth-beGtbLPS.js → page-context-BeworyNB.js} +1331 -1355
  4. package/dist/browser-ext/dist/assets/{sidepanel-BT0ojEzG.css → sidepanel-Ccl0eSa1.css} +43 -0
  5. package/dist/browser-ext/dist/background.js +680 -182
  6. package/dist/browser-ext/dist/sidepanel.html +2 -2
  7. package/dist/browser-ext/dist/sidepanel.js +372 -193
  8. package/dist/browser-ext/manifest.json +2 -8
  9. package/dist/extensions/telegram/xopc.extension.json +1 -1
  10. package/dist/gateway/static/root/assets/{activity-store-Cuf3JFFJ.js → activity-store-BQ3PMp2q.js} +1 -1
  11. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-DyATPNTe.js → agent-avatar-dicebear-Stj17S9u.js} +1 -1
  12. package/dist/gateway/static/root/assets/agent-defaults-settings-panel-CCRfvuov.js +1 -0
  13. package/dist/gateway/static/root/assets/agent-plugin-dialog-CRlzd8El.js +1 -0
  14. package/dist/gateway/static/root/assets/agents-_ug2LCQb.js +66 -0
  15. package/dist/gateway/static/root/assets/{agents-admin-api-D842OcNo.js → agents-admin-api-HBZvVyzF.js} +1 -1
  16. package/dist/gateway/static/root/assets/app-management-settings-panel-pJFe6M6c.js +1 -0
  17. package/dist/gateway/static/root/assets/{appearance-settings-DQr0o6Lm.js → appearance-settings-BrxSpBWa.js} +1 -1
  18. package/dist/gateway/static/root/assets/{apps-page-CltMV4Xp.js → apps-page-DegSl5Lh.js} +1 -1
  19. package/dist/gateway/static/root/assets/{archive-plugin-DT8Nio-r.js → archive-plugin-BO3I9zRP.js} +1 -1
  20. package/dist/gateway/static/root/assets/{automations-page-BIfWS_3Y.js → automations-page-BZQ_VHjp.js} +1 -1
  21. package/dist/gateway/static/root/assets/automations-workspace-BQywN6nG.js +6 -0
  22. package/dist/gateway/static/root/assets/{binary-plugins-7qs3qcW9.js → binary-plugins-gFD7vH9R.js} +1 -1
  23. package/dist/gateway/static/root/assets/{block-editor-BFPUlxJ6.js → block-editor-3XjFv3kK.js} +28 -28
  24. package/dist/gateway/static/root/assets/browser-automation-inputs-DxRoKFun.js +1 -0
  25. package/dist/gateway/static/root/assets/browser-automations-page-yiEuG1v7.js +3 -0
  26. package/dist/gateway/static/root/assets/browser-settings-page-ch2_bQF0.js +3 -0
  27. package/dist/gateway/static/root/assets/{capabilities-K0Zb_lH0.js → capabilities-B48r0DDa.js} +1 -1
  28. package/dist/gateway/static/root/assets/{capabilities-BzWFO_x_.js → capabilities-DO-vfNKI.js} +1 -1
  29. package/dist/gateway/static/root/assets/capabilities-page-BEnFt_dj.js +2 -0
  30. package/dist/gateway/static/root/assets/capabilities-settings-panel-NckMsJ5N.js +8 -0
  31. package/dist/gateway/static/root/assets/{capability-header-actions-AMn_vTNv.js → capability-header-actions-CY8KCO9j.js} +1 -1
  32. package/dist/gateway/static/root/assets/channels-CHOS2CGa.js +1 -0
  33. package/dist/gateway/static/root/assets/{chat-terminal-dock-C7nAROhF.js → chat-terminal-dock-Bwknnb4c.js} +2 -2
  34. package/dist/gateway/static/root/assets/computer-settings-page-B2xeFCSt.js +1 -0
  35. package/dist/gateway/static/root/assets/{connector-service-page-CTx1B8wx.js → connector-service-page-C83DnAqK.js} +1 -1
  36. package/dist/gateway/static/root/assets/connectors-page-D6wTmWA6.js +2 -0
  37. package/dist/gateway/static/root/assets/{date-picker-Cx_xIKvN.js → date-picker-Bq9GR-DU.js} +1 -1
  38. package/dist/gateway/static/root/assets/{dependency-picker-DYm96PDu.js → dependency-picker-BTERkA9X.js} +1 -1
  39. package/dist/gateway/static/root/assets/{desktop-pet-D90OzgeR.js → desktop-pet-BwsbQw8V.js} +1 -1
  40. package/dist/gateway/static/root/assets/{desktop-pet-settings-CmdCecjm.js → desktop-pet-settings-sL9i44pP.js} +1 -1
  41. package/dist/gateway/static/root/assets/device-pairing-wizard-DI2bqaKO.js +1 -0
  42. package/dist/gateway/static/root/assets/{directory-picker-path-field-DZpaoeNa.js → directory-picker-path-field-BFlDmsS5.js} +1 -1
  43. package/dist/gateway/static/root/assets/{extension-debug-page-Bv6QE8kC.js → extension-debug-page-7Z1Tp7-w.js} +1 -1
  44. package/dist/gateway/static/root/assets/{extension-page-CEsq71Cm.js → extension-page-uGqvb8QX.js} +1 -1
  45. package/dist/gateway/static/root/assets/{extension-provider-CX-sK0Tw.js → extension-provider-xlSyz4Z6.js} +1 -1
  46. package/dist/gateway/static/root/assets/{extension-settings-page-UyOHF39P.js → extension-settings-page-CKHgrjOs.js} +1 -1
  47. package/dist/gateway/static/root/assets/{fallback-models-CKv_Wqg8.js → fallback-models-BwQg0G1D.js} +1 -1
  48. package/dist/gateway/static/root/assets/{gateway-config-swr-Cjnkle5J.js → gateway-config-swr-7RKqgU_U.js} +1 -1
  49. package/dist/gateway/static/root/assets/{gateway-realtime-DCCLdyQD.js → gateway-realtime-DDWymi-o.js} +1 -1
  50. package/dist/gateway/static/root/assets/gateway-realtime-store-BheKaO6d.js +1 -0
  51. package/dist/gateway/static/root/assets/gateway-settings-BAQnSEMJ.js +1 -0
  52. package/dist/gateway/static/root/assets/{gateway-startup-retry-CXKmdVzN.js → gateway-startup-retry-jiV5sbEX.js} +1 -1
  53. package/dist/gateway/static/root/assets/home-page-BZDjvwet.js +2 -0
  54. package/dist/gateway/static/root/assets/{image-generation-api-NLHzm4Nc.js → image-generation-api-DO4mXb6I.js} +1 -1
  55. package/dist/gateway/static/root/assets/{imports-page-C7qvlBTQ.js → imports-page-re1_Nh64.js} +1 -1
  56. package/dist/gateway/static/root/assets/index-BB1z8QxA.js +106 -0
  57. package/dist/gateway/static/root/assets/index-D9R6tU7r.css +1 -0
  58. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-pkgFWJb-.js → keyboard-shortcuts-settings-B_xja9LN.js} +1 -1
  59. package/dist/gateway/static/root/assets/{local-app-workbench-page-dMDhZXAi.js → local-app-workbench-page-CErdCrPX.js} +1 -1
  60. package/dist/gateway/static/root/assets/{local-apps-page-TpUAfOn2.js → local-apps-page-BPL2RXdK.js} +1 -1
  61. package/dist/gateway/static/root/assets/{local-session-drafts-Cvv7I0fw.js → local-session-drafts-DRPsDCj1.js} +1 -1
  62. package/dist/gateway/static/root/assets/{locale-store-C8TMD2gn.js → locale-store-COVscAqC.js} +1 -1
  63. package/dist/gateway/static/root/assets/logs-page-DL-BM0xq.js +2 -0
  64. package/dist/gateway/static/root/assets/management-settings-BOVRHhXk.js +1 -0
  65. package/dist/gateway/static/root/assets/{markdown-view-Dse7rkio.js → markdown-view-B_qnAcW2.js} +1 -1
  66. package/dist/gateway/static/root/assets/{media-plugins-BZrnzaBM.js → media-plugins-C0Pc-wSY.js} +1 -1
  67. package/dist/gateway/static/root/assets/messages-Bb2GK-Nj.js +3 -0
  68. package/dist/gateway/static/root/assets/models-hub-cache-DyPPksMP.js +1 -0
  69. package/dist/gateway/static/root/assets/note-detail-panel-DJJK2yL4.js +5 -0
  70. package/dist/gateway/static/root/assets/{notes-api-DKiVeQKG.js → notes-api-sBgBcyLW.js} +1 -1
  71. package/dist/gateway/static/root/assets/notes-page-oCxO7ZTv.js +8 -0
  72. package/dist/gateway/static/root/assets/{page-context-capture-button-Cxzd4QPB.js → page-context-capture-button-tf77xIz3.js} +1 -1
  73. package/dist/gateway/static/root/assets/personal-page-DOk9gUK5.js +1 -0
  74. package/dist/gateway/static/root/assets/{product-open-page-D4hoEWiu.js → product-open-page-Dc3Y-Y7J.js} +1 -1
  75. package/dist/gateway/static/root/assets/{product-preview-content-aR38El2E.js → product-preview-content-MG_hmVA3.js} +1 -1
  76. package/dist/gateway/static/root/assets/{project-detail-page-D20QH4YB.js → project-detail-page-3uAzbW4T.js} +1 -1
  77. package/dist/gateway/static/root/assets/{projects-page-Bf3BIcT6.js → projects-page-MllfNLGA.js} +1 -1
  78. package/dist/gateway/static/root/assets/{registry-api-AdnxowbH.js → registry-api-Bh9UBb-f.js} +1 -1
  79. package/dist/gateway/static/root/assets/remote-access-hub-Bs-C70el.js +1 -0
  80. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-CBN3raz1.js → runtime-tools-settings-panel-DpJ5jNzR.js} +1 -1
  81. package/dist/gateway/static/root/assets/scenes-page-Dpxamkw6.js +2 -0
  82. package/dist/gateway/static/root/assets/{schema-form-CxrMGsT9.js → schema-form-DUeyQ1zh.js} +1 -1
  83. package/dist/gateway/static/root/assets/{send-session-input-CgOzhDZ0.js → send-session-input-Dgn4BRk_.js} +1 -1
  84. package/dist/gateway/static/root/assets/{session-api-_HIN8or3.js → session-api-yjpEz4pP.js} +1 -1
  85. package/dist/gateway/static/root/assets/{session-manager-CzCto5zT.js → session-manager-LInoZ_Yi.js} +1 -1
  86. package/dist/gateway/static/root/assets/sessions-page-BCVlNHtq.js +1 -0
  87. package/dist/gateway/static/root/assets/settings-advanced-gate-Bz-ue9fZ.js +1 -0
  88. package/dist/gateway/static/root/assets/{settings-page-D8Q3H3-o.js → settings-page-DouHuBXt.js} +1 -1
  89. package/dist/gateway/static/root/assets/{setup-status-panel-DvLMMiuS.js → setup-status-panel-B5BoZ-1y.js} +1 -1
  90. package/dist/gateway/static/root/assets/{share-link-dialog-Dg5os066.js → share-link-dialog-DJTabLAM.js} +1 -1
  91. package/dist/gateway/static/root/assets/{share-preview-page-DMvohAyr.js → share-preview-page-jw7h4t_B.js} +1 -1
  92. package/dist/gateway/static/root/assets/{shares-api-C96IO4qJ.js → shares-api-c0dfWz6h.js} +1 -1
  93. package/dist/gateway/static/root/assets/shares-settings-BlnlzGjH.js +1 -0
  94. package/dist/gateway/static/root/assets/{skill-api-C10ML0CH.js → skill-api-CfyX_0zN.js} +1 -1
  95. package/dist/gateway/static/root/assets/{skill-reload-api-DNbJzSfB.js → skill-reload-api-Ge5XymHa.js} +1 -1
  96. package/dist/gateway/static/root/assets/{skills-page-DObMKvA6.js → skills-page-DjdTeYzm.js} +1 -1
  97. package/dist/gateway/static/root/assets/system-settings-panel-DWf256TE.js +1 -0
  98. package/dist/gateway/static/root/assets/{task-detail-page-BR2L1L8l.js → task-detail-page-DE6N1ZQe.js} +2 -2
  99. package/dist/gateway/static/root/assets/{task-expandable-content-DL2Q2gre.js → task-expandable-content-hYcgCEE3.js} +1 -1
  100. package/dist/gateway/static/root/assets/{theme-store-DQ6KJa4l.js → theme-store-BOfCKgl6.js} +1 -1
  101. package/dist/gateway/static/root/assets/{tracing-settings-page-Ed8_GYUT.js → tracing-settings-page-DDWFXcST.js} +1 -1
  102. package/dist/gateway/static/root/assets/{tunnel-api-D3X8jDwo.js → tunnel-api-BBm9esrY.js} +1 -1
  103. package/dist/gateway/static/root/assets/{usage-page-BqzRyyb9.js → usage-page-C93x89uW.js} +1 -1
  104. package/dist/gateway/static/root/assets/{use-autosave-CRFefpzO.js → use-autosave-CSckcjFx.js} +1 -1
  105. package/dist/gateway/static/root/assets/{use-composer-attachments-OLrvSjn5.js → use-composer-attachments-C7Gaaai1.js} +1 -1
  106. package/dist/gateway/static/root/assets/{use-file-preview-expanded-CJvzfQvw.js → use-file-preview-expanded-Db2VYg1b.js} +2 -2
  107. package/dist/gateway/static/root/assets/user-model-page-B74LkVbp.js +2 -0
  108. package/dist/gateway/static/root/assets/{voice-api-key-field-R9zNWYR_.js → voice-api-key-field-GEw5x2fU.js} +1 -1
  109. package/dist/gateway/static/root/assets/{voice-config-api-DLudE9Rc.js → voice-config-api-3yyVwXln.js} +1 -1
  110. package/dist/gateway/static/root/assets/{work-discovery-overlay-DVmQrolV.js → work-discovery-overlay-Bs2MKiTv.js} +1 -1
  111. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-DEkmuD3T.js → workflow-run-setup-panel-BR_U_snV.js} +2 -2
  112. package/dist/gateway/static/root/assets/{workflows-page-pqCB3sh4.js → workflows-page-Crk4SxtC.js} +3 -3
  113. package/dist/gateway/static/root/assets/{workspace-api-CdVUhyPo.js → workspace-api-Si2o3inw.js} +2 -2
  114. package/dist/gateway/static/root/index.html +27 -28
  115. package/dist/package.js +1 -1
  116. package/dist/packages/browser-control-contract/src/automation.js +213 -0
  117. package/dist/packages/browser-control-contract/src/index.js +43 -12
  118. package/dist/packages/gateway-contract/src/gateway-scopes.js +3 -1
  119. package/dist/packages/gateway-contract/src/notifications.js +1 -0
  120. package/dist/packages/realtime-protocol/src/voice-catalog.js +7 -1
  121. package/dist/packages/realtime-protocol/src/voice.js +9 -3
  122. package/dist/src/agent/coding/repository-instructions.js +8 -1
  123. package/dist/src/agent/tool-manuals/browser.d.ts +1 -1
  124. package/dist/src/agent/tool-manuals/browser.js +3 -3
  125. package/dist/src/agent/tools/browser/tool/browser-use-tool.js +1 -1
  126. package/dist/src/agent/tools/browser/tool/schemas.d.ts +0 -13
  127. package/dist/src/agent/tools/browser/tool/schemas.js +0 -4
  128. package/dist/src/agent/tools/browser-automation-tool.d.ts +74 -1
  129. package/dist/src/agent/tools/browser-automation-tool.js +52 -6
  130. package/dist/src/agent-config/schema.d.ts +1 -1
  131. package/dist/src/agent-config/schema.js +1 -1
  132. package/dist/src/automations/capabilities/write.d.ts +2 -0
  133. package/dist/src/automations/domain/types.d.ts +11 -0
  134. package/dist/src/automations/domain/validation.d.ts +4 -0
  135. package/dist/src/automations/domain/validation.js +1 -0
  136. package/dist/src/automations/draft/automation-draft-validator.d.ts +1 -0
  137. package/dist/src/automations/service/action-executor.js +11 -3
  138. package/dist/src/automations/service/automation-service.d.ts +1 -0
  139. package/dist/src/automations/service/automation-service.js +13 -1
  140. package/dist/src/bootstrap/application-state.js +3 -1
  141. package/dist/src/browser/automations/repository.d.ts +4 -0
  142. package/dist/src/browser/automations/repository.js +27 -6
  143. package/dist/src/browser/automations/runner.js +101 -9
  144. package/dist/src/browser/automations/runtime-service.js +4 -3
  145. package/dist/src/browser/automations/schema.d.ts +84 -1
  146. package/dist/src/browser/automations/schema.js +68 -2
  147. package/dist/src/browser/automations/service.d.ts +78 -1
  148. package/dist/src/browser/automations/service.js +36 -4
  149. package/dist/src/browser/automations/types.d.ts +19 -4
  150. package/dist/src/browser/drivers/extension-driver.d.ts +5 -5
  151. package/dist/src/browser/drivers/extension-driver.js +10 -10
  152. package/dist/src/browser/drivers/playwright-driver.js +46 -0
  153. package/dist/src/browser/index.d.ts +1 -2
  154. package/dist/src/browser/index.js +2 -3
  155. package/dist/src/browser/policy/browser-policy.d.ts +0 -1
  156. package/dist/src/browser/policy/browser-policy.js +1 -4
  157. package/dist/src/browser/providers/index.d.ts +1 -1
  158. package/dist/src/browser/providers/index.js +2 -2
  159. package/dist/src/browser/providers/realtime-extension.d.ts +1 -1
  160. package/dist/src/browser/providers/realtime-extension.js +5 -1
  161. package/dist/src/browser/recordings/service.d.ts +22 -0
  162. package/dist/src/browser/recordings/service.js +160 -0
  163. package/dist/src/browser/runtime/browser-runtime.d.ts +1 -1
  164. package/dist/src/browser/runtime/browser-runtime.js +19 -56
  165. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  166. package/dist/src/computer/runtime.js +2 -2
  167. package/dist/src/config/schema.d.ts +12 -12
  168. package/dist/src/config/schema.js +12 -12
  169. package/dist/src/endpoint-tools/policy.js +4 -3
  170. package/dist/src/gateway/hono/routes/browser-automations.js +23 -7
  171. package/dist/src/gateway/hono/routes/browser-recordings.d.ts +3 -0
  172. package/dist/src/gateway/hono/routes/browser-recordings.js +78 -0
  173. package/dist/src/gateway/hono/routes/browser.js +1 -34
  174. package/dist/src/gateway/hono/routes/device-push.js +1 -0
  175. package/dist/src/gateway/hono/routes/home.js +1 -1
  176. package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
  177. package/dist/src/gateway/hono/routes/personal-agent.js +35 -0
  178. package/dist/src/gateway/hono/routes/voice.js +5 -2
  179. package/dist/src/gateway/security/gateway-scopes.js +3 -1
  180. package/dist/src/gateway/service/agent-runner.js +1 -0
  181. package/dist/src/home-intelligence/generator.d.ts +2 -0
  182. package/dist/src/home-intelligence/generator.js +53 -4
  183. package/dist/src/home-intelligence/host.d.ts +2 -1
  184. package/dist/src/home-intelligence/host.js +23 -3
  185. package/dist/src/migrations/cloud-public-models.d.ts +7 -0
  186. package/dist/src/migrations/cloud-public-models.js +157 -0
  187. package/dist/src/notifications/clarification.d.ts +5 -0
  188. package/dist/src/notifications/clarification.js +66 -0
  189. package/dist/src/notifications/device-store.js +1 -0
  190. package/dist/src/notifications/harmony-push.d.ts +1 -0
  191. package/dist/src/notifications/harmony-push.js +3 -1
  192. package/dist/src/notifications/planner.d.ts +1 -0
  193. package/dist/src/notifications/service.d.ts +2 -2
  194. package/dist/src/notifications/service.js +20 -4
  195. package/dist/src/notifications/store.d.ts +2 -0
  196. package/dist/src/notifications/store.js +7 -1
  197. package/dist/src/notifications/types.d.ts +1 -0
  198. package/dist/src/notifications/types.js +1 -0
  199. package/dist/src/providers/model-catalog-persistence.js +2 -2
  200. package/dist/src/providers/presentation.js +1 -0
  201. package/dist/src/providers/xopc-cloud-model-source.js +1 -1
  202. package/dist/src/storage/sqlite/clarification-wait-repository.js +1 -0
  203. package/dist/src/storage/sqlite/migrations/238_browser_record_replay.sql +45 -0
  204. package/dist/src/storage/sqlite/migrations/239_clarification_notifications.sql +26 -0
  205. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  206. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  207. package/dist/src/storage/sqlite/schema.js +1 -1
  208. package/dist/src/tasks/home-query-service.d.ts +1 -1
  209. package/dist/src/tasks/home-query-service.js +6 -2
  210. package/dist/src/voice/dashscope/streaming-tts-stream.d.ts +2 -1
  211. package/dist/src/voice/dashscope/streaming-tts-stream.js +135 -79
  212. package/dist/src/voice/metadata/types.d.ts +1 -0
  213. package/dist/src/voice/platform-catalog.d.ts +4 -0
  214. package/dist/src/voice/platform-settings.js +1 -0
  215. package/dist/src/voice/realtime/agentEngine.js +78 -1
  216. package/dist/src/voice/realtime/speakable-segmenter.d.ts +2 -0
  217. package/dist/src/voice/realtime/speakable-segmenter.js +30 -0
  218. package/dist/src/voice/tts/providers/alibaba-speech.js +17 -1
  219. package/dist/src/voice/tts/providers/xopc-cloud-speech.js +3 -2
  220. package/dist/src/voice/tts/speak-core.d.ts +3 -0
  221. package/dist/src/voice/tts/speak-core.js +21 -1
  222. package/dist/src/voice/tts/speech-provider-types.d.ts +8 -0
  223. package/package.json +3 -3
  224. package/dist/gateway/static/root/assets/agent-defaults-settings-panel-gT9_j4tr.js +0 -1
  225. package/dist/gateway/static/root/assets/agent-plugin-dialog-CLXCJ6JN.js +0 -1
  226. package/dist/gateway/static/root/assets/agents-BAUfOMsq.js +0 -66
  227. package/dist/gateway/static/root/assets/app-management-settings-panel-CoIFxQKH.js +0 -1
  228. package/dist/gateway/static/root/assets/automations-workspace-BCGPjsdU.js +0 -6
  229. package/dist/gateway/static/root/assets/browser-automation-inputs-BUAIKIX0.js +0 -1
  230. package/dist/gateway/static/root/assets/browser-automations-page-CSwu1U1F.js +0 -3
  231. package/dist/gateway/static/root/assets/browser-settings-page-C1ReMNqX.js +0 -3
  232. package/dist/gateway/static/root/assets/capabilities-page-BdTSqloI.js +0 -2
  233. package/dist/gateway/static/root/assets/capabilities-settings-panel-BUblBMZC.js +0 -8
  234. package/dist/gateway/static/root/assets/channels-CXCf5qO6.js +0 -1
  235. package/dist/gateway/static/root/assets/computer-settings-page-B5SuSZ3G.js +0 -1
  236. package/dist/gateway/static/root/assets/connectors-page-DfnK1crf.js +0 -2
  237. package/dist/gateway/static/root/assets/device-pairing-wizard-s2ogSa4l.js +0 -1
  238. package/dist/gateway/static/root/assets/gateway-realtime-store-C_Th8kGg.js +0 -1
  239. package/dist/gateway/static/root/assets/gateway-settings-CZ73IpbP.js +0 -1
  240. package/dist/gateway/static/root/assets/home-page-BQWq4kYi.js +0 -2
  241. package/dist/gateway/static/root/assets/index-BQgqzhQj.js +0 -106
  242. package/dist/gateway/static/root/assets/index-COB4QIJp.css +0 -1
  243. package/dist/gateway/static/root/assets/logs-page-B-Y9oZLh.js +0 -2
  244. package/dist/gateway/static/root/assets/management-settings-B2lzsw6X.js +0 -1
  245. package/dist/gateway/static/root/assets/messages-Dy26npMy.js +0 -3
  246. package/dist/gateway/static/root/assets/models-hub-cache-B1C86nRp.js +0 -1
  247. package/dist/gateway/static/root/assets/note-detail-panel-DL__qP6U.js +0 -5
  248. package/dist/gateway/static/root/assets/notes-page-DTBbg9Hs.js +0 -8
  249. package/dist/gateway/static/root/assets/oauth-api-BwpzpRwC.js +0 -1
  250. package/dist/gateway/static/root/assets/personal-page-CzaTnHeD.js +0 -1
  251. package/dist/gateway/static/root/assets/remote-access-hub-CKxqncqW.js +0 -1
  252. package/dist/gateway/static/root/assets/scenes-page-Cv_5ozfX.js +0 -2
  253. package/dist/gateway/static/root/assets/sessions-page-DZcu6NLJ.js +0 -1
  254. package/dist/gateway/static/root/assets/settings-advanced-gate-DazqeDQ7.js +0 -1
  255. package/dist/gateway/static/root/assets/shares-settings-CsDATuCw.js +0 -1
  256. package/dist/gateway/static/root/assets/system-settings-panel-Bn3khdzb.js +0 -1
  257. package/dist/gateway/static/root/assets/user-model-page-CG6wqIKP.js +0 -2
  258. package/dist/src/browser/policy/approval-store.d.ts +0 -17
  259. package/dist/src/browser/policy/approval-store.js +0 -62
  260. package/dist/src/browser/providers/extension-ws-acquire.d.ts +0 -19
  261. package/dist/src/browser/providers/extension-ws-acquire.js +0 -78
  262. package/dist/src/browser/providers/extension.d.ts +0 -61
  263. package/dist/src/browser/providers/extension.js +0 -378
@@ -5107,6 +5107,149 @@ function superRefine(fn, params) {
5107
5107
  return _superRefine(fn, params);
5108
5108
  }
5109
5109
 
5110
+ const MAX_BROWSER_CONTEXTS_PER_TURN = 2;
5111
+ const MAX_BROWSER_SELECTION_BYTES = 32 * 1024;
5112
+ const MAX_BROWSER_PAGE_TEXT_BYTES = 128 * 1024;
5113
+ const MAX_BROWSER_CONTEXT_BODY_BYTES = 256 * 1024;
5114
+ const utf8Bytes = (value) => new TextEncoder().encode(value).byteLength;
5115
+ const browserPageContextInputSchema = strictObject({
5116
+ kind: literal("browser_page"),
5117
+ sourceId: uuid(),
5118
+ version: string().regex(/^[a-f0-9]{64}$/),
5119
+ title: string().trim().min(1).max(512),
5120
+ url: url(),
5121
+ capturedAt: number().int().nonnegative(),
5122
+ documentId: string().trim().min(1).max(256),
5123
+ selection: string().optional(),
5124
+ text: string().optional(),
5125
+ truncated: boolean()
5126
+ }).superRefine((value, ctx) => {
5127
+ const url = new URL(value.url);
5128
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash) {
5129
+ ctx.addIssue({ code: "custom", path: ["url"], message: "URL must be a sanitized HTTP(S) URL" });
5130
+ }
5131
+ if (!value.selection?.trim() && !value.text?.trim()) {
5132
+ ctx.addIssue({ code: "custom", message: "A browser context must include selection or page text" });
5133
+ }
5134
+ if (value.selection && utf8Bytes(value.selection) > MAX_BROWSER_SELECTION_BYTES) {
5135
+ ctx.addIssue({ code: "custom", path: ["selection"], message: "Selection is too large" });
5136
+ }
5137
+ if (value.text && utf8Bytes(value.text) > MAX_BROWSER_PAGE_TEXT_BYTES) {
5138
+ ctx.addIssue({ code: "custom", path: ["text"], message: "Page text is too large" });
5139
+ }
5140
+ });
5141
+ const browserPageContextsInputSchema = array(browserPageContextInputSchema).max(MAX_BROWSER_CONTEXTS_PER_TURN).superRefine((value, ctx) => {
5142
+ if (utf8Bytes(JSON.stringify(value)) > MAX_BROWSER_CONTEXT_BODY_BYTES) {
5143
+ ctx.addIssue({ code: "custom", message: "Browser contexts are too large" });
5144
+ }
5145
+ });
5146
+
5147
+ const DEVICE_PAIRING_VERSION = 3;
5148
+ const MOBILE_PAIRING_INVITATION_VERSION = 4;
5149
+ const BROWSER_PAIRING_INVITATION_VERSION = 1;
5150
+ const BROWSER_PAIRING_INVITATION_PREFIX = `XOPC-BROWSER-INVITE-V${BROWSER_PAIRING_INVITATION_VERSION}:`;
5151
+ _enum(["mobile", "browser"]);
5152
+ const devicePlatformSchema = _enum(["ios", "android", "harmonyos", "chrome"]);
5153
+ const devicePairingRouteSchema = strictObject({
5154
+ id: string().trim().min(1).max(80),
5155
+ kind: _enum(["xopc-secure-link", "tailscale", "custom-https", "local-browser"]),
5156
+ url: string().url()
5157
+ }).superRefine((route, context) => {
5158
+ const url = new URL(route.url);
5159
+ const isOrigin = !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash;
5160
+ const valid = route.kind === "local-browser" ? url.protocol === "http:" && url.hostname === "127.0.0.1" && isOrigin : url.protocol === "https:" && isOrigin;
5161
+ if (!valid) context.addIssue({ code: "custom", path: ["url"], message: "Pairing route must be a secure origin" });
5162
+ });
5163
+ const browserPairingInvitationPayloadSchema = strictObject({
5164
+ version: literal(DEVICE_PAIRING_VERSION),
5165
+ targetKind: literal("browser"),
5166
+ pairingToken: string().regex(/^xopc_pair_[0-9a-f-]{36}_[A-Za-z0-9_-]{43}$/i),
5167
+ gatewayId: string().uuid(),
5168
+ gatewayName: string().trim().min(1).max(120),
5169
+ gatewayPublicKey: string().regex(/^[A-Za-z0-9_-]{43}$/),
5170
+ routes: array(devicePairingRouteSchema).min(1).max(8),
5171
+ expiresAt: number().int().positive()
5172
+ });
5173
+ const secureMobileOriginSchema = string().url().superRefine((origin, context) => {
5174
+ const url = new URL(origin);
5175
+ if (url.protocol !== "https:" || url.username || url.password || origin !== url.origin) {
5176
+ context.addIssue({ code: "custom", message: "Mobile pairing origins must be canonical HTTPS origins" });
5177
+ }
5178
+ });
5179
+ strictObject({
5180
+ version: literal(MOBILE_PAIRING_INVITATION_VERSION),
5181
+ pairingToken: string().regex(/^xopc_pair_[0-9a-f-]{36}_[A-Za-z0-9_-]{43}$/i),
5182
+ gatewayId: string().uuid(),
5183
+ gatewayPublicKey: string().regex(/^[A-Za-z0-9_-]{43}$/),
5184
+ origins: array(secureMobileOriginSchema).min(1).max(8),
5185
+ expiresAt: number().int().positive()
5186
+ }).superRefine((payload, context) => {
5187
+ if (new Set(payload.origins).size !== payload.origins.length) {
5188
+ context.addIssue({ code: "custom", path: ["origins"], message: "Mobile pairing origins must be unique" });
5189
+ }
5190
+ });
5191
+ const devicePairingKeySchema = strictObject({
5192
+ kty: literal("EC"),
5193
+ crv: literal("P-256"),
5194
+ x: string().regex(/^[A-Za-z0-9_-]{43}$/),
5195
+ y: string().regex(/^[A-Za-z0-9_-]{43}$/)
5196
+ });
5197
+ const devicePairingDeviceSchema = strictObject({
5198
+ displayName: string().trim().min(1).max(80),
5199
+ platform: devicePlatformSchema,
5200
+ publicKeyJwk: devicePairingKeySchema,
5201
+ extensionId: string().regex(/^[a-p]{32}$/).optional()
5202
+ }).superRefine((device, context) => {
5203
+ if (device.platform === "chrome" && !device.extensionId) {
5204
+ context.addIssue({ code: "custom", path: ["extensionId"], message: "Chrome devices require an extension id" });
5205
+ }
5206
+ if (device.platform !== "chrome" && device.extensionId) {
5207
+ context.addIssue({ code: "custom", path: ["extensionId"], message: "Only Chrome devices may set an extension id" });
5208
+ }
5209
+ });
5210
+ const initialDeviceRefreshTokenSchema = string().regex(
5211
+ /^xopc_rt_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_[A-Za-z0-9_-]{43}$/i
5212
+ );
5213
+ const devicePairingProofSchema = strictObject({
5214
+ gatewayId: string().uuid(),
5215
+ requestId: string().uuid(),
5216
+ pairingToken: string().min(1).max(256),
5217
+ timestamp: number().int().positive(),
5218
+ nonce: string().regex(/^[A-Za-z0-9_-]{24,128}$/),
5219
+ signature: string().regex(/^[A-Za-z0-9_-]{86}$/)
5220
+ });
5221
+ devicePairingProofSchema.extend({ device: devicePairingDeviceSchema });
5222
+ devicePairingProofSchema.extend({
5223
+ idempotencyKey: string().uuid(),
5224
+ initialRefreshToken: initialDeviceRefreshTokenSchema
5225
+ });
5226
+ function isRetryableDevicePairingHttpStatus(status) {
5227
+ return status >= 500 || [404, 408, 425, 429].includes(status);
5228
+ }
5229
+ function readBrowserPairingInvitation(value) {
5230
+ const invitation = value.trim();
5231
+ if (!invitation.startsWith(BROWSER_PAIRING_INVITATION_PREFIX)) {
5232
+ throw new Error("Browser pairing invitation is invalid");
5233
+ }
5234
+ const encodedPayload = invitation.slice(BROWSER_PAIRING_INVITATION_PREFIX.length);
5235
+ if (!/^[A-Za-z0-9_-]+$/.test(encodedPayload)) throw new Error("Browser pairing invitation is invalid");
5236
+ return encodedPayload;
5237
+ }
5238
+ function canonical(value) {
5239
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
5240
+ if (value !== null && typeof value === "object") {
5241
+ return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b, "en")).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
5242
+ }
5243
+ return JSON.stringify(value);
5244
+ }
5245
+ function buildDevicePairingProof(action, body) {
5246
+ const { signature: _signature, ...payload } = body;
5247
+ return `xopc-device-pairing-v3
5248
+ POST
5249
+ ${action}
5250
+ ${canonical(payload)}`;
5251
+ }
5252
+
5110
5253
  const ENDPOINT_PROTOCOL_VERSION = 2;
5111
5254
  const endpointKindSchema = _enum(["web", "desktop", "mobile", "browser"]);
5112
5255
  const endpointAvailabilitySchema = _enum(["foreground", "background"]);
@@ -5300,575 +5443,18 @@ function endpointHelloSigningPayload(payload) {
5300
5443
  return canonicalJson(unsigned);
5301
5444
  }
5302
5445
 
5303
- const REALTIME_PROTOCOL_VERSION = 2;
5304
- const REALTIME_MAX_CLIENT_FRAME_BYTES = 256 * 1024;
5305
- const REALTIME_HEARTBEAT_INTERVAL_MS = 15e3;
5306
- const REALTIME_HEARTBEAT_TIMEOUT_MS = 45e3;
5307
- const REALTIME_CAPABILITIES = ["realtime.capability-negotiation.v1"];
5308
- const realtimeClientKindSchema = _enum([
5309
- "web",
5310
- "desktop",
5311
- "mobile",
5312
- "browser_extension",
5313
- "tui",
5314
- "mcp"
5315
- ]);
5316
- const realtimeTopicSchema = string().min(1).max(512);
5317
- const realtimeEventNameSchema = string().regex(/^[a-z][a-z0-9_.-]*$/).max(160);
5318
- const messageIdSchema = uuid();
5319
- const timestampSchema = number().int().nonnegative();
5320
- const sequenceSchema = number().int().positive();
5321
- const capabilitySchema = string().min(1).max(160);
5322
- const envelope = (kind, payload) => object({
5323
- protocolVersion: literal(REALTIME_PROTOCOL_VERSION),
5324
- messageId: messageIdSchema,
5325
- kind: literal(kind),
5326
- sentAt: timestampSchema,
5327
- payload
5328
- });
5329
- const realtimeSubscriptionSchema = object({
5330
- topic: realtimeTopicSchema,
5331
- afterSeq: number().int().nonnegative().optional(),
5332
- view: literal("compact").optional()
5333
- });
5334
- discriminatedUnion("kind", [
5335
- envelope("realtime.hello", object({
5336
- ticket: string().min(32).max(512),
5337
- clientId: string().min(1).max(160),
5338
- clientKind: realtimeClientKindSchema,
5339
- subscriptions: array(realtimeSubscriptionSchema).max(100).default([]),
5340
- capabilities: array(capabilitySchema).max(64).optional(),
5341
- endpoint: endpointHelloPayloadSchema.optional()
5342
- })),
5343
- envelope("realtime.subscribe", object({
5344
- subscriptions: array(realtimeSubscriptionSchema).min(1).max(100)
5345
- })),
5346
- envelope("realtime.unsubscribe", object({
5347
- topics: array(realtimeTopicSchema).min(1).max(100)
5348
- })),
5349
- envelope("realtime.ping", object({})),
5350
- envelope("endpoint.message", clientEndpointMessageSchema)
5351
- ]);
5352
- const realtimeEventPayloadSchema = object({
5353
- topic: realtimeTopicSchema,
5354
- seq: sequenceSchema,
5355
- event: realtimeEventNameSchema,
5356
- data: unknown()
5357
- });
5358
- const serverRealtimeMessageSchema = discriminatedUnion("kind", [
5359
- envelope("realtime.ready", object({
5360
- connectionId: uuid(),
5361
- heartbeatIntervalMs: literal(REALTIME_HEARTBEAT_INTERVAL_MS),
5362
- heartbeatTimeoutMs: literal(REALTIME_HEARTBEAT_TIMEOUT_MS),
5363
- negotiatedCapabilities: array(capabilitySchema).max(64).optional(),
5364
- endpoint: object({
5365
- endpointId: string().min(1).max(160),
5366
- turnToken: string().min(32).max(160)
5367
- }).optional()
5368
- })),
5369
- envelope("realtime.event", realtimeEventPayloadSchema),
5370
- envelope("realtime.subscribed", object({
5371
- topic: realtimeTopicSchema,
5372
- cursor: number().int().nonnegative()
5373
- })),
5374
- envelope("realtime.gap", object({
5375
- topic: realtimeTopicSchema,
5376
- requestedSeq: number().int().nonnegative(),
5377
- earliestSeq: sequenceSchema,
5378
- recoverable: boolean()
5379
- })),
5380
- envelope("realtime.pong", object({})),
5381
- envelope("realtime.error", object({
5382
- code: string().min(1).max(80),
5383
- message: string().min(1).max(500)
5384
- })),
5385
- envelope("endpoint.message", serverEndpointMessageSchema)
5386
- ]);
5387
- function parseServerRealtimeMessage(value) {
5388
- return serverRealtimeMessageSchema.parse(value);
5446
+ const LOCALE_STORAGE_KEY = "xopc.sidepanel.locale";
5447
+ const LOCALE_PATHS = {
5448
+ en: "_locales/en/messages.json",
5449
+ "zh-CN": "_locales/zh_CN/messages.json"
5450
+ };
5451
+ let activeLocale;
5452
+ let activeCatalog;
5453
+ function isExtensionLocale(value) {
5454
+ return value === "en" || value === "zh-CN";
5389
5455
  }
5390
-
5391
- class RealtimeConnectionError extends Error {
5392
- constructor(message, retryable) {
5393
- super(message);
5394
- this.retryable = retryable;
5395
- this.name = "RealtimeConnectionError";
5396
- }
5397
- retryable;
5398
- }
5399
- class RealtimeFrameTooLargeError extends Error {
5400
- constructor(bytes, maxBytes) {
5401
- super(`Realtime client frame is ${bytes} bytes; maximum is ${maxBytes} bytes`);
5402
- this.bytes = bytes;
5403
- this.maxBytes = maxBytes;
5404
- this.name = "RealtimeFrameTooLargeError";
5405
- }
5406
- bytes;
5407
- maxBytes;
5408
- }
5409
- function frameText(data) {
5410
- if (typeof data === "string") return data;
5411
- if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
5412
- if (ArrayBuffer.isView(data)) {
5413
- const bytes = Uint8Array.from(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
5414
- return new TextDecoder().decode(bytes);
5415
- }
5416
- throw new Error("Realtime server sent a non-text frame");
5417
- }
5418
- function isNonRetryableCloseCode(code) {
5419
- return code === 1009 || code === 4400 || code === 4401 || code === 4409 || code === 4413;
5420
- }
5421
- class RealtimeClient {
5422
- constructor(options) {
5423
- this.options = options;
5424
- }
5425
- options;
5426
- desiredSubscriptions = /* @__PURE__ */ new Map();
5427
- cursors = /* @__PURE__ */ new Map();
5428
- socket;
5429
- reconnectTimer;
5430
- heartbeatTimer;
5431
- connectionTimer;
5432
- ticketAbort;
5433
- generation = 0;
5434
- reconnectAttempts = 0;
5435
- shouldReconnect = false;
5436
- ready = false;
5437
- lastServerMessageAt = 0;
5438
- endpointBinding;
5439
- reconnectWhenSocketOpens = false;
5440
- offeredCapabilities = /* @__PURE__ */ new Set();
5441
- connect() {
5442
- if (this.shouldReconnect) return;
5443
- this.shouldReconnect = true;
5444
- void this.open(false);
5445
- }
5446
- disconnect() {
5447
- this.shouldReconnect = false;
5448
- this.reconnectWhenSocketOpens = false;
5449
- this.generation += 1;
5450
- this.clearTimers();
5451
- this.ready = false;
5452
- this.offeredCapabilities.clear();
5453
- this.endpointBinding?.onDisconnected?.();
5454
- this.socket?.close(1e3, "Client disconnected");
5455
- this.socket = void 0;
5456
- this.options.onStateChange?.("disconnected");
5457
- }
5458
- reconnect() {
5459
- this.reconnectWhenSocketOpens = false;
5460
- this.generation += 1;
5461
- this.clearTimers();
5462
- this.ready = false;
5463
- this.endpointBinding?.onDisconnected?.();
5464
- this.socket?.close(1e3, "Client reconnecting");
5465
- this.socket = void 0;
5466
- this.reconnectAttempts = 0;
5467
- this.shouldReconnect = true;
5468
- void this.open(true);
5469
- }
5470
- subscribe(topic, afterSeq) {
5471
- this.desiredSubscriptions.set(topic, afterSeq);
5472
- if (afterSeq !== void 0) this.cursors.set(topic, afterSeq);
5473
- if (this.ready) {
5474
- this.sendSubscriptions([{ topic, afterSeq: this.cursors.get(topic) ?? afterSeq }]);
5475
- }
5476
- }
5477
- unsubscribe(topic) {
5478
- this.desiredSubscriptions.delete(topic);
5479
- if (this.ready) this.send(this.clientMessage("realtime.unsubscribe", { topics: [topic] }));
5480
- }
5481
- sendEndpointMessage(message) {
5482
- if (!this.ready) throw new Error("Realtime connection is not ready");
5483
- this.send(this.clientMessage("endpoint.message", message));
5484
- }
5485
- setEndpoint(binding) {
5486
- this.endpointBinding = binding;
5487
- this.refreshEndpointConnection();
5488
- }
5489
- clearEndpoint(binding) {
5490
- if (binding && this.endpointBinding !== binding) return;
5491
- const previous = this.endpointBinding;
5492
- this.endpointBinding = void 0;
5493
- previous?.onDisconnected?.();
5494
- this.refreshEndpointConnection();
5495
- }
5496
- refreshEndpointConnection() {
5497
- if (!this.shouldReconnect) return;
5498
- if (this.socket?.readyState === 0) {
5499
- this.reconnectWhenSocketOpens = true;
5500
- return;
5501
- }
5502
- this.reconnect();
5503
- }
5504
- async open(reconnecting) {
5505
- const generation = ++this.generation;
5506
- const timeoutMs = this.options.connectionTimeoutMs ?? 15e3;
5507
- const ticketAbort = new AbortController();
5508
- this.ticketAbort?.abort();
5509
- this.ticketAbort = ticketAbort;
5510
- let socket;
5511
- let closed = false;
5512
- let messageQueue;
5513
- const failAttempt = (error, retry = true) => {
5514
- if (closed || generation !== this.generation) return;
5515
- closed = true;
5516
- this.clearConnectionTimer();
5517
- ticketAbort.abort();
5518
- if (this.ticketAbort === ticketAbort) this.ticketAbort = void 0;
5519
- this.ready = false;
5520
- this.offeredCapabilities.clear();
5521
- this.endpointBinding?.onDisconnected?.();
5522
- this.clearHeartbeat();
5523
- if (this.socket === socket) this.socket = void 0;
5524
- if (socket) {
5525
- socket.onopen = null;
5526
- socket.onmessage = null;
5527
- socket.onerror = null;
5528
- socket.onclose = null;
5529
- socket.close(4e3, error.slice(0, 120));
5530
- }
5531
- if (this.shouldReconnect && retry) this.scheduleReconnect(error);
5532
- else if (this.shouldReconnect) {
5533
- this.shouldReconnect = false;
5534
- this.options.onStateChange?.("error", error);
5535
- } else this.options.onStateChange?.("disconnected");
5536
- };
5537
- this.connectionTimer = setTimeout(() => failAttempt("Realtime connection timed out"), timeoutMs);
5538
- this.options.onStateChange?.(reconnecting ? "reconnecting" : "connecting");
5539
- try {
5540
- const [issuedTicket, endpoint] = await Promise.all([
5541
- this.options.issueTicket(ticketAbort.signal),
5542
- this.endpointBinding?.createHello()
5543
- ]);
5544
- const ticket = issuedTicket.ticket;
5545
- if (REALTIME_PROTOCOL_VERSION < issuedTicket.realtime.minVersion) {
5546
- throw new RealtimeConnectionError("CLIENT_UPDATE_REQUIRED", false);
5547
- }
5548
- if (REALTIME_PROTOCOL_VERSION > issuedTicket.realtime.maxVersion) {
5549
- throw new RealtimeConnectionError("GATEWAY_UPDATE_REQUIRED", false);
5550
- }
5551
- const capabilities = REALTIME_CAPABILITIES.filter((capability) => issuedTicket.realtime.capabilities.includes(capability));
5552
- if (closed || !this.shouldReconnect || generation !== this.generation) return;
5553
- this.offeredCapabilities = new Set(capabilities ?? []);
5554
- socket = this.options.createWebSocket(this.options.getWebSocketUrl());
5555
- this.socket = socket;
5556
- socket.onopen = () => {
5557
- if (generation !== this.generation) return;
5558
- if (this.reconnectWhenSocketOpens) {
5559
- this.reconnect();
5560
- return;
5561
- }
5562
- const subscriptions = [...this.desiredSubscriptions].map(([topic, afterSeq]) => ({
5563
- topic,
5564
- afterSeq: this.cursors.get(topic) ?? afterSeq
5565
- }));
5566
- this.send(this.clientMessage("realtime.hello", {
5567
- ticket,
5568
- clientId: this.options.clientId,
5569
- clientKind: this.options.clientKind,
5570
- subscriptions,
5571
- ...capabilities?.length ? { capabilities } : {},
5572
- ...endpoint ? { endpoint } : {}
5573
- }));
5574
- };
5575
- socket.onmessage = (event) => {
5576
- if (generation !== this.generation) return;
5577
- const process = () => {
5578
- if (closed || generation !== this.generation) return;
5579
- return this.handleServerMessage(frameText(event.data));
5580
- };
5581
- if (!messageQueue) {
5582
- try {
5583
- const pending = process();
5584
- if (!pending) return;
5585
- const current2 = pending.catch((error) => failAttempt(error instanceof Error ? error.message : "Invalid realtime server message")).then(() => void 0);
5586
- messageQueue = current2;
5587
- void current2.finally(() => {
5588
- if (messageQueue === current2) messageQueue = void 0;
5589
- });
5590
- } catch (error) {
5591
- failAttempt(error instanceof Error ? error.message : "Invalid realtime server message");
5592
- }
5593
- return;
5594
- }
5595
- const current = messageQueue.then(process).catch((error) => failAttempt(error instanceof Error ? error.message : "Invalid realtime server message")).then(() => void 0);
5596
- messageQueue = current;
5597
- void current.finally(() => {
5598
- if (messageQueue === current) messageQueue = void 0;
5599
- });
5600
- };
5601
- socket.onerror = () => {
5602
- };
5603
- socket.onclose = (event) => {
5604
- failAttempt(
5605
- event.reason || `WebSocket closed (${event.code ?? 0})`,
5606
- !isNonRetryableCloseCode(event.code)
5607
- );
5608
- };
5609
- } catch (error) {
5610
- failAttempt(
5611
- error instanceof Error ? error.message : "Realtime connection failed",
5612
- !(error instanceof RealtimeConnectionError) || error.retryable
5613
- );
5614
- }
5615
- }
5616
- handleServerMessage(text) {
5617
- const message = parseServerRealtimeMessage(JSON.parse(text));
5618
- this.lastServerMessageAt = Date.now();
5619
- if (message.kind === "realtime.ready") {
5620
- const negotiated = message.payload.negotiatedCapabilities ?? [];
5621
- if (negotiated.some((capability) => !this.offeredCapabilities.has(capability))) {
5622
- throw new Error("Realtime server negotiated a capability the client did not offer");
5623
- }
5624
- this.ready = true;
5625
- this.reconnectAttempts = 0;
5626
- this.clearConnectionTimer();
5627
- this.ticketAbort = void 0;
5628
- this.options.onStateChange?.("connected");
5629
- this.options.onCapabilities?.(negotiated);
5630
- if (message.payload.endpoint) this.endpointBinding?.onReady(message.payload.endpoint);
5631
- this.clearHeartbeat();
5632
- this.heartbeatTimer = setInterval(() => {
5633
- if (!this.ready) return;
5634
- if (Date.now() - this.lastServerMessageAt >= message.payload.heartbeatTimeoutMs) {
5635
- this.reconnect();
5636
- return;
5637
- }
5638
- this.send(this.clientMessage("realtime.ping", {}));
5639
- }, message.payload.heartbeatIntervalMs);
5640
- } else if (message.kind === "realtime.event") {
5641
- const cursor = this.cursors.get(message.payload.topic);
5642
- if (cursor !== void 0 && message.payload.seq <= cursor) return;
5643
- this.options.onEvent?.(message.payload);
5644
- this.cursors.set(message.payload.topic, message.payload.seq);
5645
- } else if (message.kind === "realtime.subscribed") {
5646
- this.cursors.set(message.payload.topic, message.payload.cursor);
5647
- this.options.onSubscribed?.(message.payload);
5648
- } else if (message.kind === "realtime.gap") {
5649
- this.cursors.set(message.payload.topic, message.payload.earliestSeq - 1);
5650
- return this.options.onGap?.(message.payload);
5651
- } else if (message.kind === "realtime.error") {
5652
- this.options.onStateChange?.("error", message.payload.message);
5653
- } else if (message.kind === "endpoint.message") {
5654
- this.endpointBinding?.onMessage(message.payload);
5655
- this.options.onEndpointMessage?.(message.payload);
5656
- }
5657
- }
5658
- sendSubscriptions(subscriptions) {
5659
- this.send(this.clientMessage("realtime.subscribe", { subscriptions }));
5660
- }
5661
- clientMessage(kind, payload) {
5662
- const messageId = this.options.createMessageId?.() ?? globalThis.crypto?.randomUUID?.();
5663
- if (!messageId) throw new Error("RealtimeClient requires createMessageId when crypto.randomUUID is unavailable");
5664
- return {
5665
- protocolVersion: REALTIME_PROTOCOL_VERSION,
5666
- messageId,
5667
- kind,
5668
- sentAt: Date.now(),
5669
- payload
5670
- };
5671
- }
5672
- send(message) {
5673
- if (!this.socket || this.socket.readyState !== 1) return;
5674
- const data = JSON.stringify(message);
5675
- const bytes = new TextEncoder().encode(data).byteLength;
5676
- if (bytes > REALTIME_MAX_CLIENT_FRAME_BYTES) {
5677
- throw new RealtimeFrameTooLargeError(bytes, REALTIME_MAX_CLIENT_FRAME_BYTES);
5678
- }
5679
- this.socket.send(data);
5680
- }
5681
- scheduleReconnect(error) {
5682
- if (!this.shouldReconnect) return;
5683
- this.reconnectAttempts += 1;
5684
- const max = this.options.maxReconnectAttempts;
5685
- if (max !== void 0 && this.reconnectAttempts > max) {
5686
- this.shouldReconnect = false;
5687
- this.options.onStateChange?.("error", error);
5688
- return;
5689
- }
5690
- this.options.onStateChange?.("reconnecting", error);
5691
- const cap = Math.min(3e4, 1e3 * 2 ** Math.min(this.reconnectAttempts - 1, 5));
5692
- const delay = Math.round(cap * (0.5 + Math.random() * 0.5));
5693
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
5694
- this.reconnectTimer = setTimeout(() => {
5695
- this.reconnectTimer = void 0;
5696
- void this.open(true);
5697
- }, delay);
5698
- }
5699
- clearHeartbeat() {
5700
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
5701
- this.heartbeatTimer = void 0;
5702
- }
5703
- clearTimers() {
5704
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
5705
- this.reconnectTimer = void 0;
5706
- this.clearConnectionTimer();
5707
- this.ticketAbort?.abort();
5708
- this.ticketAbort = void 0;
5709
- this.clearHeartbeat();
5710
- }
5711
- clearConnectionTimer() {
5712
- if (this.connectionTimer) clearTimeout(this.connectionTimer);
5713
- this.connectionTimer = void 0;
5714
- }
5715
- }
5716
-
5717
- const MAX_BROWSER_CONTEXTS_PER_TURN = 2;
5718
- const MAX_BROWSER_SELECTION_BYTES = 32 * 1024;
5719
- const MAX_BROWSER_PAGE_TEXT_BYTES = 128 * 1024;
5720
- const MAX_BROWSER_CONTEXT_BODY_BYTES = 256 * 1024;
5721
- const utf8Bytes = (value) => new TextEncoder().encode(value).byteLength;
5722
- const browserPageContextInputSchema = strictObject({
5723
- kind: literal("browser_page"),
5724
- sourceId: uuid(),
5725
- version: string().regex(/^[a-f0-9]{64}$/),
5726
- title: string().trim().min(1).max(512),
5727
- url: url(),
5728
- capturedAt: number().int().nonnegative(),
5729
- documentId: string().trim().min(1).max(256),
5730
- selection: string().optional(),
5731
- text: string().optional(),
5732
- truncated: boolean()
5733
- }).superRefine((value, ctx) => {
5734
- const url = new URL(value.url);
5735
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash) {
5736
- ctx.addIssue({ code: "custom", path: ["url"], message: "URL must be a sanitized HTTP(S) URL" });
5737
- }
5738
- if (!value.selection?.trim() && !value.text?.trim()) {
5739
- ctx.addIssue({ code: "custom", message: "A browser context must include selection or page text" });
5740
- }
5741
- if (value.selection && utf8Bytes(value.selection) > MAX_BROWSER_SELECTION_BYTES) {
5742
- ctx.addIssue({ code: "custom", path: ["selection"], message: "Selection is too large" });
5743
- }
5744
- if (value.text && utf8Bytes(value.text) > MAX_BROWSER_PAGE_TEXT_BYTES) {
5745
- ctx.addIssue({ code: "custom", path: ["text"], message: "Page text is too large" });
5746
- }
5747
- });
5748
- const browserPageContextsInputSchema = array(browserPageContextInputSchema).max(MAX_BROWSER_CONTEXTS_PER_TURN).superRefine((value, ctx) => {
5749
- if (utf8Bytes(JSON.stringify(value)) > MAX_BROWSER_CONTEXT_BODY_BYTES) {
5750
- ctx.addIssue({ code: "custom", message: "Browser contexts are too large" });
5751
- }
5752
- });
5753
-
5754
- const DEVICE_PAIRING_VERSION = 3;
5755
- const MOBILE_PAIRING_INVITATION_VERSION = 4;
5756
- const BROWSER_PAIRING_INVITATION_VERSION = 1;
5757
- const BROWSER_PAIRING_INVITATION_PREFIX = `XOPC-BROWSER-INVITE-V${BROWSER_PAIRING_INVITATION_VERSION}:`;
5758
- _enum(["mobile", "browser"]);
5759
- const devicePlatformSchema = _enum(["ios", "android", "harmonyos", "chrome"]);
5760
- const devicePairingRouteSchema = strictObject({
5761
- id: string().trim().min(1).max(80),
5762
- kind: _enum(["xopc-secure-link", "tailscale", "custom-https", "local-browser"]),
5763
- url: string().url()
5764
- }).superRefine((route, context) => {
5765
- const url = new URL(route.url);
5766
- const isOrigin = !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash;
5767
- const valid = route.kind === "local-browser" ? url.protocol === "http:" && url.hostname === "127.0.0.1" && isOrigin : url.protocol === "https:" && isOrigin;
5768
- if (!valid) context.addIssue({ code: "custom", path: ["url"], message: "Pairing route must be a secure origin" });
5769
- });
5770
- const browserPairingInvitationPayloadSchema = strictObject({
5771
- version: literal(DEVICE_PAIRING_VERSION),
5772
- targetKind: literal("browser"),
5773
- pairingToken: string().regex(/^xopc_pair_[0-9a-f-]{36}_[A-Za-z0-9_-]{43}$/i),
5774
- gatewayId: string().uuid(),
5775
- gatewayName: string().trim().min(1).max(120),
5776
- gatewayPublicKey: string().regex(/^[A-Za-z0-9_-]{43}$/),
5777
- routes: array(devicePairingRouteSchema).min(1).max(8),
5778
- expiresAt: number().int().positive()
5779
- });
5780
- const secureMobileOriginSchema = string().url().superRefine((origin, context) => {
5781
- const url = new URL(origin);
5782
- if (url.protocol !== "https:" || url.username || url.password || origin !== url.origin) {
5783
- context.addIssue({ code: "custom", message: "Mobile pairing origins must be canonical HTTPS origins" });
5784
- }
5785
- });
5786
- strictObject({
5787
- version: literal(MOBILE_PAIRING_INVITATION_VERSION),
5788
- pairingToken: string().regex(/^xopc_pair_[0-9a-f-]{36}_[A-Za-z0-9_-]{43}$/i),
5789
- gatewayId: string().uuid(),
5790
- gatewayPublicKey: string().regex(/^[A-Za-z0-9_-]{43}$/),
5791
- origins: array(secureMobileOriginSchema).min(1).max(8),
5792
- expiresAt: number().int().positive()
5793
- }).superRefine((payload, context) => {
5794
- if (new Set(payload.origins).size !== payload.origins.length) {
5795
- context.addIssue({ code: "custom", path: ["origins"], message: "Mobile pairing origins must be unique" });
5796
- }
5797
- });
5798
- const devicePairingKeySchema = strictObject({
5799
- kty: literal("EC"),
5800
- crv: literal("P-256"),
5801
- x: string().regex(/^[A-Za-z0-9_-]{43}$/),
5802
- y: string().regex(/^[A-Za-z0-9_-]{43}$/)
5803
- });
5804
- const devicePairingDeviceSchema = strictObject({
5805
- displayName: string().trim().min(1).max(80),
5806
- platform: devicePlatformSchema,
5807
- publicKeyJwk: devicePairingKeySchema,
5808
- extensionId: string().regex(/^[a-p]{32}$/).optional()
5809
- }).superRefine((device, context) => {
5810
- if (device.platform === "chrome" && !device.extensionId) {
5811
- context.addIssue({ code: "custom", path: ["extensionId"], message: "Chrome devices require an extension id" });
5812
- }
5813
- if (device.platform !== "chrome" && device.extensionId) {
5814
- context.addIssue({ code: "custom", path: ["extensionId"], message: "Only Chrome devices may set an extension id" });
5815
- }
5816
- });
5817
- const initialDeviceRefreshTokenSchema = string().regex(
5818
- /^xopc_rt_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_[A-Za-z0-9_-]{43}$/i
5819
- );
5820
- const devicePairingProofSchema = strictObject({
5821
- gatewayId: string().uuid(),
5822
- requestId: string().uuid(),
5823
- pairingToken: string().min(1).max(256),
5824
- timestamp: number().int().positive(),
5825
- nonce: string().regex(/^[A-Za-z0-9_-]{24,128}$/),
5826
- signature: string().regex(/^[A-Za-z0-9_-]{86}$/)
5827
- });
5828
- devicePairingProofSchema.extend({ device: devicePairingDeviceSchema });
5829
- devicePairingProofSchema.extend({
5830
- idempotencyKey: string().uuid(),
5831
- initialRefreshToken: initialDeviceRefreshTokenSchema
5832
- });
5833
- function isRetryableDevicePairingHttpStatus(status) {
5834
- return status >= 500 || [404, 408, 425, 429].includes(status);
5835
- }
5836
- function readBrowserPairingInvitation(value) {
5837
- const invitation = value.trim();
5838
- if (!invitation.startsWith(BROWSER_PAIRING_INVITATION_PREFIX)) {
5839
- throw new Error("Browser pairing invitation is invalid");
5840
- }
5841
- const encodedPayload = invitation.slice(BROWSER_PAIRING_INVITATION_PREFIX.length);
5842
- if (!/^[A-Za-z0-9_-]+$/.test(encodedPayload)) throw new Error("Browser pairing invitation is invalid");
5843
- return encodedPayload;
5844
- }
5845
- function canonical(value) {
5846
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
5847
- if (value !== null && typeof value === "object") {
5848
- return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b, "en")).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
5849
- }
5850
- return JSON.stringify(value);
5851
- }
5852
- function buildDevicePairingProof(action, body) {
5853
- const { signature: _signature, ...payload } = body;
5854
- return `xopc-device-pairing-v3
5855
- POST
5856
- ${action}
5857
- ${canonical(payload)}`;
5858
- }
5859
-
5860
- const LOCALE_STORAGE_KEY = "xopc.sidepanel.locale";
5861
- const LOCALE_PATHS = {
5862
- en: "_locales/en/messages.json",
5863
- "zh-CN": "_locales/zh_CN/messages.json"
5864
- };
5865
- let activeLocale;
5866
- let activeCatalog;
5867
- function isExtensionLocale(value) {
5868
- return value === "en" || value === "zh-CN";
5869
- }
5870
- function browserLocale() {
5871
- return chrome.i18n.getUILanguage().replaceAll("_", "-").toLowerCase().startsWith("zh") ? "zh-CN" : "en";
5456
+ function browserLocale() {
5457
+ return chrome.i18n.getUILanguage().replaceAll("_", "-").toLowerCase().startsWith("zh") ? "zh-CN" : "en";
5872
5458
  }
5873
5459
  function messageWithSubstitutions(entry, substitutions) {
5874
5460
  const values = substitutions === void 0 ? [] : Array.isArray(substitutions) ? substitutions : [substitutions];
@@ -5883,887 +5469,1277 @@ function messageWithSubstitutions(entry, substitutions) {
5883
5469
  return values[index] ?? "";
5884
5470
  }).replace(/\$(\d+)/g, (_match, index) => values[Number(index) - 1] ?? "");
5885
5471
  }
5886
- function t(key, substitutions) {
5887
- const entry = activeCatalog?.[key];
5888
- return entry ? messageWithSubstitutions(entry, substitutions) : chrome.i18n.getMessage(key, substitutions) || key;
5472
+ function t(key, substitutions) {
5473
+ const entry = activeCatalog?.[key];
5474
+ return entry ? messageWithSubstitutions(entry, substitutions) : chrome.i18n.getMessage(key, substitutions) || key;
5475
+ }
5476
+ function extensionLocale() {
5477
+ if (activeLocale) return activeLocale;
5478
+ return typeof chrome === "undefined" ? "en" : chrome.i18n.getUILanguage().replaceAll("_", "-");
5479
+ }
5480
+ async function loadExtensionLocalePreference() {
5481
+ const stored = await chrome.storage.local.get(LOCALE_STORAGE_KEY);
5482
+ const locale = isExtensionLocale(stored[LOCALE_STORAGE_KEY]) ? stored[LOCALE_STORAGE_KEY] : browserLocale();
5483
+ await applyExtensionLocale(locale);
5484
+ return locale;
5485
+ }
5486
+ async function saveExtensionLocalePreference(locale) {
5487
+ await chrome.storage.local.set({ [LOCALE_STORAGE_KEY]: locale });
5488
+ await applyExtensionLocale(locale);
5489
+ }
5490
+ async function applyExtensionLocale(locale) {
5491
+ const response = await fetch(chrome.runtime.getURL(LOCALE_PATHS[locale]));
5492
+ if (!response.ok) throw new Error(`Could not load extension locale: ${locale}`);
5493
+ activeCatalog = await response.json();
5494
+ activeLocale = locale;
5495
+ initializeDocumentLocale();
5496
+ }
5497
+ function initializeDocumentLocale() {
5498
+ document.documentElement.lang = extensionLocale();
5499
+ document.documentElement.dir = chrome.i18n.getMessage("@@bidi_dir") || "ltr";
5500
+ document.title = t("appName");
5501
+ }
5502
+
5503
+ const DATABASE_NAME = "xopc-browser-chat-state";
5504
+ const DATABASE_VERSION = 2;
5505
+ function openDatabase() {
5506
+ return new Promise((resolve, reject) => {
5507
+ const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
5508
+ request.onupgradeneeded = () => {
5509
+ if (!request.result.objectStoreNames.contains("drafts")) request.result.createObjectStore("drafts");
5510
+ if (!request.result.objectStoreNames.contains("outbox")) {
5511
+ request.result.createObjectStore("outbox");
5512
+ }
5513
+ };
5514
+ request.onsuccess = () => resolve(request.result);
5515
+ request.onerror = () => reject(request.error ?? new Error(t("errorOpenOutbox")));
5516
+ });
5517
+ }
5518
+ async function clearBrowserChatState() {
5519
+ const db = await openDatabase();
5520
+ try {
5521
+ await new Promise((resolve, reject) => {
5522
+ const tx = db.transaction(["outbox", "drafts"], "readwrite");
5523
+ tx.objectStore("outbox").clear();
5524
+ tx.objectStore("drafts").clear();
5525
+ tx.oncomplete = () => resolve();
5526
+ tx.onerror = () => reject(tx.error);
5527
+ tx.onabort = () => reject(tx.error);
5528
+ });
5529
+ } finally {
5530
+ db.close();
5531
+ }
5532
+ }
5533
+
5534
+ const PROFILE_KEY = "xopc.browser.profile";
5535
+ const PAIRING_JOURNAL_KEY = "xopc.browser.pairing";
5536
+ const REFRESH_JOURNAL_KEY = "xopc.browser.auth-refresh";
5537
+ const AUTO_CONNECT_KEY = "xopc.browser.auto-connect";
5538
+ const KEY_DATABASE = "xopc-browser-identity";
5539
+ const KEY_STORE = "identity";
5540
+ const KEY_NAME = "device-key";
5541
+ const NATIVE_HOST_NAME = "ai.xopc.browser";
5542
+ const GATEWAY_REQUEST_TIMEOUT_MS = 8e3;
5543
+ const ACCESS_TOKEN_EXPIRY_MARGIN_MS = 3e4;
5544
+ const REFRESH_LOCK_NAME = "xopc.browser.auth-refresh";
5545
+ let refreshTask;
5546
+ function assertProfileIdentity(profile, expected) {
5547
+ if (!profile || profile.gatewayId !== expected.gatewayId || profile.deviceId !== expected.deviceId || profile.gatewayPublicKey !== expected.gatewayPublicKey) throw new Error("Gateway identity changed");
5548
+ }
5549
+ async function browserPublicKeyThumbprint(jwk) {
5550
+ const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
5551
+ return encodeBase64Url(new Uint8Array(await crypto.subtle.digest(
5552
+ "SHA-256",
5553
+ new TextEncoder().encode(canonical)
5554
+ )));
5555
+ }
5556
+ async function isAutoConnectEnabled() {
5557
+ const stored = await chrome.storage.local.get(AUTO_CONNECT_KEY);
5558
+ return stored[AUTO_CONNECT_KEY] !== false;
5559
+ }
5560
+ async function setAutoConnectEnabled(enabled) {
5561
+ await chrome.storage.local.set({ [AUTO_CONNECT_KEY]: enabled });
5562
+ }
5563
+ async function discoverLocalGateway(options) {
5564
+ try {
5565
+ if (!options?.force && !await isAutoConnectEnabled()) return void 0;
5566
+ const pair = await getOrCreateKeyPair();
5567
+ const key = await publicKeyJwk(pair);
5568
+ const nonce = randomNonce();
5569
+ const response = await chrome.runtime.sendNativeMessage(
5570
+ NATIVE_HOST_NAME,
5571
+ {
5572
+ type: "bootstrap",
5573
+ extensionId: chrome.runtime.id,
5574
+ publicKeyJwk: key,
5575
+ publicKeyThumbprint: await browserPublicKeyThumbprint(key),
5576
+ nonce
5577
+ }
5578
+ );
5579
+ if (response.ok !== true || typeof response.gatewayUrl !== "string" || typeof response.invitation !== "string" || typeof response.expiresAt !== "number") {
5580
+ if (options?.force) throw new Error(response.error || t("errorInvalidLocalHostResponse"));
5581
+ return void 0;
5582
+ }
5583
+ return {
5584
+ gatewayUrl: response.gatewayUrl,
5585
+ invitation: response.invitation,
5586
+ expiresAt: response.expiresAt
5587
+ };
5588
+ } catch (cause) {
5589
+ if (options?.force) throw cause;
5590
+ return void 0;
5591
+ }
5592
+ }
5593
+ function encodeBase64Url(bytes) {
5594
+ let binary = "";
5595
+ bytes.forEach((byte) => {
5596
+ binary += String.fromCharCode(byte);
5597
+ });
5598
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
5599
+ }
5600
+ function decodeBase64Url(value) {
5601
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - value.length % 4) % 4);
5602
+ const binary = atob(padded);
5603
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
5604
+ }
5605
+ function decodeJson(value) {
5606
+ return JSON.parse(new TextDecoder().decode(decodeBase64Url(value)));
5607
+ }
5608
+ function randomNonce(bytes = 24) {
5609
+ return encodeBase64Url(crypto.getRandomValues(new Uint8Array(bytes)));
5610
+ }
5611
+ function createRefreshToken() {
5612
+ return `xopc_rt_${crypto.randomUUID()}_${randomNonce(32)}`;
5613
+ }
5614
+ function parseBrowserPairingInvitation(value, allowExpired = false) {
5615
+ const encoded = readBrowserPairingInvitation(value);
5616
+ const parsed = browserPairingInvitationPayloadSchema.safeParse(decodeJson(encoded));
5617
+ if (!parsed.success) throw new Error("Pairing invitation version is not supported");
5618
+ const payload = parsed.data;
5619
+ if (!allowExpired && payload.expiresAt <= Date.now()) throw new Error("Pairing invitation has expired");
5620
+ return payload;
5621
+ }
5622
+ function openIdentityDatabase() {
5623
+ return new Promise((resolve, reject) => {
5624
+ const request = indexedDB.open(KEY_DATABASE, 1);
5625
+ request.onupgradeneeded = () => request.result.createObjectStore(KEY_STORE);
5626
+ request.onsuccess = () => resolve(request.result);
5627
+ request.onerror = () => reject(request.error);
5628
+ });
5629
+ }
5630
+ async function storedKeyPair() {
5631
+ const database = await openIdentityDatabase();
5632
+ try {
5633
+ return await new Promise((resolve, reject) => {
5634
+ const request = database.transaction(KEY_STORE).objectStore(KEY_STORE).get(KEY_NAME);
5635
+ request.onsuccess = () => resolve(request.result);
5636
+ request.onerror = () => reject(request.error);
5637
+ });
5638
+ } finally {
5639
+ database.close();
5640
+ }
5889
5641
  }
5890
- function extensionLocale() {
5891
- if (activeLocale) return activeLocale;
5892
- return typeof chrome === "undefined" ? "en" : chrome.i18n.getUILanguage().replaceAll("_", "-");
5642
+ async function getOrCreateKeyPair() {
5643
+ const existing = await storedKeyPair();
5644
+ if (existing) return existing;
5645
+ const pair = await crypto.subtle.generateKey(
5646
+ { name: "ECDSA", namedCurve: "P-256" },
5647
+ false,
5648
+ ["sign", "verify"]
5649
+ );
5650
+ const database = await openIdentityDatabase();
5651
+ await new Promise((resolve, reject) => {
5652
+ const request = database.transaction(KEY_STORE, "readwrite").objectStore(KEY_STORE).put(pair, KEY_NAME);
5653
+ request.onsuccess = () => resolve();
5654
+ request.onerror = () => reject(request.error);
5655
+ }).finally(() => database.close());
5656
+ return pair;
5893
5657
  }
5894
- async function loadExtensionLocalePreference() {
5895
- const stored = await chrome.storage.local.get(LOCALE_STORAGE_KEY);
5896
- const locale = isExtensionLocale(stored[LOCALE_STORAGE_KEY]) ? stored[LOCALE_STORAGE_KEY] : browserLocale();
5897
- await applyExtensionLocale(locale);
5898
- return locale;
5658
+ async function forgetIdentity() {
5659
+ await new Promise((resolve, reject) => {
5660
+ const request = indexedDB.deleteDatabase(KEY_DATABASE);
5661
+ request.onsuccess = () => resolve();
5662
+ request.onerror = () => reject(request.error);
5663
+ request.onblocked = () => reject(new Error(t("errorIdentityInUse")));
5664
+ });
5899
5665
  }
5900
- async function saveExtensionLocalePreference(locale) {
5901
- await chrome.storage.local.set({ [LOCALE_STORAGE_KEY]: locale });
5902
- await applyExtensionLocale(locale);
5666
+ async function publicKeyJwk(pair) {
5667
+ const exported = await crypto.subtle.exportKey("jwk", pair.publicKey);
5668
+ if (!exported.x || !exported.y) throw new Error(t("errorIdentityExport"));
5669
+ return { kty: "EC", crv: "P-256", x: exported.x, y: exported.y };
5903
5670
  }
5904
- async function applyExtensionLocale(locale) {
5905
- const response = await fetch(chrome.runtime.getURL(LOCALE_PATHS[locale]));
5906
- if (!response.ok) throw new Error(`Could not load extension locale: ${locale}`);
5907
- activeCatalog = await response.json();
5908
- activeLocale = locale;
5909
- initializeDocumentLocale();
5671
+ async function sign(pair, text) {
5672
+ const signature = await crypto.subtle.sign(
5673
+ { name: "ECDSA", hash: "SHA-256" },
5674
+ pair.privateKey,
5675
+ new TextEncoder().encode(text)
5676
+ );
5677
+ return encodeBase64Url(new Uint8Array(signature));
5910
5678
  }
5911
- function initializeDocumentLocale() {
5912
- document.documentElement.lang = extensionLocale();
5913
- document.documentElement.dir = chrome.i18n.getMessage("@@bidi_dir") || "ltr";
5914
- document.title = t("appName");
5679
+ async function verifyGateway(publicKey, payload, signature) {
5680
+ try {
5681
+ const key = await crypto.subtle.importKey(
5682
+ "raw",
5683
+ decodeBase64Url(publicKey),
5684
+ { name: "Ed25519" },
5685
+ false,
5686
+ ["verify"]
5687
+ );
5688
+ return crypto.subtle.verify(
5689
+ { name: "Ed25519" },
5690
+ key,
5691
+ decodeBase64Url(signature),
5692
+ new TextEncoder().encode(payload)
5693
+ );
5694
+ } catch {
5695
+ return false;
5696
+ }
5915
5697
  }
5916
-
5917
- const PENDING_CONTEXT_KEY = "xopc.browser.pending-context";
5918
- const TAB_BINDING_PREFIX = "xopc.browser.tab-binding.";
5919
- async function buildPageContext(raw, pageUrl) {
5920
- const url = sanitizedUrl(pageUrl);
5921
- const selected = raw.selection ? truncateUtf8(raw.selection, MAX_BROWSER_SELECTION_BYTES) : void 0;
5922
- const page = raw.text ? truncateUtf8(raw.text, MAX_BROWSER_PAGE_TEXT_BYTES) : void 0;
5923
- const canonical = {
5924
- title: raw.title.trim() || new URL(url).hostname,
5925
- url,
5926
- capturedAt: Date.now(),
5927
- documentId: await sha256(`${url}
5928
- ${raw.timeOrigin}`),
5929
- selection: selected?.value,
5930
- text: page?.value
5931
- };
5932
- return {
5933
- kind: "browser_page",
5934
- sourceId: crypto.randomUUID(),
5935
- version: await sha256(JSON.stringify(canonical)),
5936
- ...canonical,
5937
- truncated: selected?.truncated === true || page?.truncated === true
5938
- };
5698
+ async function hasGatewayOriginPermission(origin) {
5699
+ const url = new URL(origin);
5700
+ if (url.protocol === "http:" && ["127.0.0.1", "localhost"].includes(url.hostname)) return true;
5701
+ return chrome.permissions.contains({ origins: [`${url.origin}/*`] });
5939
5702
  }
5940
- function truncateUtf8(value, maxBytes) {
5941
- const encoder = new TextEncoder();
5942
- if (encoder.encode(value).byteLength <= maxBytes) return { value, truncated: false };
5943
- let low = 0;
5944
- let high = value.length;
5945
- while (low < high) {
5946
- const middle = Math.ceil((low + high) / 2);
5947
- if (encoder.encode(value.slice(0, middle)).byteLength <= maxBytes) low = middle;
5948
- else high = middle - 1;
5703
+ class GatewayHttpError extends Error {
5704
+ constructor(code, status, serverMessage) {
5705
+ super(code);
5706
+ this.code = code;
5707
+ this.status = status;
5708
+ this.serverMessage = serverMessage;
5709
+ this.name = "GatewayHttpError";
5949
5710
  }
5950
- return { value: value.slice(0, low), truncated: true };
5711
+ code;
5712
+ status;
5713
+ serverMessage;
5951
5714
  }
5952
- function sanitizedUrl(raw) {
5953
- let url;
5715
+ async function post(origin, path, body) {
5716
+ const controller = new AbortController();
5717
+ const timeout = setTimeout(() => controller.abort(), GATEWAY_REQUEST_TIMEOUT_MS);
5718
+ let response;
5954
5719
  try {
5955
- url = new URL(raw);
5956
- } catch {
5957
- throw new Error(t("errorInvalidWebAddress"));
5720
+ response = await fetch(`${origin}${path}`, {
5721
+ method: "POST",
5722
+ headers: { "Content-Type": "application/json" },
5723
+ body: JSON.stringify(body),
5724
+ signal: controller.signal
5725
+ });
5726
+ } catch (cause) {
5727
+ if (controller.signal.aborted) throw new Error(t("errorGatewayTimeout", String(GATEWAY_REQUEST_TIMEOUT_MS / 1e3)));
5728
+ throw cause;
5729
+ } finally {
5730
+ clearTimeout(timeout);
5958
5731
  }
5959
- if (url.protocol !== "http:" && url.protocol !== "https:") {
5960
- throw new Error(t("errorRestrictedPage"));
5732
+ const json = await response.json().catch(() => ({}));
5733
+ if (!response.ok) {
5734
+ throw new GatewayHttpError(
5735
+ json.error?.code ?? json.error?.message ?? t("errorGatewayStatus", String(response.status)),
5736
+ response.status,
5737
+ json.error?.message
5738
+ );
5961
5739
  }
5962
- if (url.hostname === "chromewebstore.google.com" || url.hostname === "chrome.google.com" && url.pathname.startsWith("/webstore")) {
5963
- throw new Error(t("errorWebStorePage"));
5740
+ return json;
5741
+ }
5742
+ async function signedPairingRequest(pair, payload, origin, requestId, action, completion) {
5743
+ const nonce = randomNonce();
5744
+ const body = {
5745
+ gatewayId: payload.gatewayId,
5746
+ requestId,
5747
+ pairingToken: payload.pairingToken,
5748
+ timestamp: Date.now(),
5749
+ nonce,
5750
+ ...action === "request" ? {
5751
+ device: {
5752
+ displayName: `Chrome · ${navigator.platform || "Browser"}`,
5753
+ platform: "chrome",
5754
+ extensionId: chrome.runtime.id,
5755
+ publicKeyJwk: await publicKeyJwk(pair)
5756
+ }
5757
+ } : {},
5758
+ ...action === "complete" ? completion : {}
5759
+ };
5760
+ const signature = await sign(pair, buildDevicePairingProof(action, body));
5761
+ const path = action === "request" ? "/api/device-pairing/requests" : `/api/device-pairing/requests/${requestId}/${action}`;
5762
+ const response = await post(origin, path, { ...body, signature });
5763
+ if (typeof response.signedPayload !== "string" || typeof response.signature !== "string") {
5764
+ throw new Error(t("errorUnsignedPairing"));
5964
5765
  }
5965
- url.username = "";
5966
- url.password = "";
5967
- url.hash = "";
5968
- return url.toString();
5766
+ if (!await verifyGateway(payload.gatewayPublicKey, response.signedPayload, response.signature)) {
5767
+ throw new Error(t("errorGatewayVerification"));
5768
+ }
5769
+ const decoded = decodeJson(response.signedPayload);
5770
+ if (decoded.gateway.id !== payload.gatewayId || decoded.nonce !== nonce || decoded.request.requestId !== requestId) {
5771
+ throw new Error(t("errorPairingMismatch"));
5772
+ }
5773
+ return decoded;
5969
5774
  }
5970
- function siteAccessError(cause, url) {
5775
+ function shouldTryNextRoute(cause) {
5776
+ return !(cause instanceof GatewayHttpError) || isRetryableDevicePairingHttpStatus(cause.status);
5777
+ }
5778
+ function isRouteFailure(cause) {
5779
+ if (cause instanceof GatewayHttpError) return isRetryableDevicePairingHttpStatus(cause.status);
5971
5780
  const message = cause instanceof Error ? cause.message : String(cause);
5972
- const hostname = new URL(url).hostname;
5973
- if (/cannot access|cannot be scripted|missing host permission|extensions gallery|chrome web store/i.test(message)) {
5974
- return new Error(t("errorSiteAccessBlocked", hostname));
5975
- }
5976
- return cause instanceof Error ? cause : new Error(message);
5781
+ return /network|fetch|respond|identity|route/i.test(message);
5977
5782
  }
5978
- async function runWithTabSiteAccess(tabId, operation) {
5979
- const tab = await chrome.tabs.get(tabId);
5980
- if (tab.id === void 0 || tab.windowId === void 0 || !tab.url) {
5981
- throw new Error(t("errorTabUnavailable"));
5783
+ async function signedPairingRequestAcrossRoutes(pair, payload, origins, requestId, action, completion) {
5784
+ let lastRouteError;
5785
+ for (const origin of origins) {
5786
+ try {
5787
+ return { origin, result: await signedPairingRequest(pair, payload, origin, requestId, action, completion) };
5788
+ } catch (cause) {
5789
+ if (!shouldTryNextRoute(cause)) throw cause;
5790
+ lastRouteError = cause;
5791
+ }
5982
5792
  }
5983
- const url = sanitizedUrl(tab.url);
5984
- const originPattern = `${new URL(url).origin}/*`;
5985
- const alreadyGranted = await chrome.permissions.contains({ origins: [originPattern] });
5986
- let newlyGranted = false;
5987
- if (!alreadyGranted) {
5988
- let granted = false;
5793
+ throw lastRouteError instanceof Error ? lastRouteError : new Error("No Gateway route could be reached");
5794
+ }
5795
+ async function refreshAccessToken(gatewayUrls, refreshToken, pair, gateway, operation = {
5796
+ requestId: crypto.randomUUID(),
5797
+ nextRefreshToken: createRefreshToken()
5798
+ }) {
5799
+ const { nextRefreshToken, requestId } = operation;
5800
+ const timestamp = Date.now();
5801
+ const nonce = randomNonce();
5802
+ const credentialId = refreshToken.slice("xopc_rt_".length).split("_")[0];
5803
+ const proof = `xopc-device-refresh-v2
5804
+ ${credentialId}
5805
+ ${timestamp}
5806
+ ${nonce}
5807
+ ${requestId}
5808
+ ${nextRefreshToken}`;
5809
+ const signature = await sign(pair, proof);
5810
+ const body = {
5811
+ refreshToken,
5812
+ nextRefreshToken,
5813
+ requestId,
5814
+ timestamp,
5815
+ nonce,
5816
+ signature
5817
+ };
5818
+ let gatewayUrl;
5819
+ let response;
5820
+ let lastRouteError;
5821
+ for (const origin of gatewayUrls) {
5989
5822
  try {
5990
- granted = await chrome.permissions.request({ origins: [originPattern] });
5823
+ response = await post(origin, "/api/device-auth/refresh", body);
5824
+ gatewayUrl = origin;
5825
+ break;
5991
5826
  } catch (cause) {
5992
- throw siteAccessError(cause, url);
5827
+ if (!shouldTryNextRoute(cause)) throw cause;
5828
+ lastRouteError = cause;
5993
5829
  }
5994
- if (!granted) throw new Error(t("errorSiteAccessRequired", new URL(url).hostname));
5995
- newlyGranted = true;
5996
5830
  }
5997
- try {
5998
- return await operation({ ...tab, id: tab.id, windowId: tab.windowId, url });
5999
- } catch (cause) {
6000
- if (newlyGranted) await chrome.permissions.remove({ origins: [originPattern] }).catch(() => false);
6001
- throw siteAccessError(cause, url);
5831
+ if (!response || !gatewayUrl) {
5832
+ throw lastRouteError instanceof Error ? lastRouteError : new Error(t("errorNoReachableGatewayRoute"));
6002
5833
  }
6003
- }
6004
- async function sha256(value) {
6005
- const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
6006
- return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
6007
- }
6008
- function extractPage(mode) {
6009
- const normalize = (value) => value.replace(/\s+/g, " ").trim();
6010
- if (mode === "selection") {
6011
- const focused = document.activeElement;
6012
- const fieldSelection = focused instanceof HTMLTextAreaElement || focused instanceof HTMLInputElement && ["text", "search", "url", "email", "tel"].includes(focused.type) ? focused.value.slice(focused.selectionStart ?? 0, focused.selectionEnd ?? 0) : "";
6013
- const selection = normalize(fieldSelection || window.getSelection()?.toString() || "");
6014
- return { title: document.title, url: location.href, timeOrigin: performance.timeOrigin, selection };
5834
+ if (typeof response.signedPayload !== "string" || typeof response.signature !== "string" || !await verifyGateway(gateway.gatewayPublicKey, response.signedPayload, response.signature)) throw new Error(t("errorGatewayVerification"));
5835
+ const responseProof = decodeJson(response.signedPayload);
5836
+ if (responseProof.purpose !== "device-refresh-v3" || responseProof.gatewayId !== gateway.gatewayId || responseProof.requestId !== requestId || responseProof.nonce !== nonce || !(responseProof.expiresAt > Date.now()) || responseProof.expiresAt > Date.now() + 6e4) throw new Error("Gateway refresh response does not match this request");
5837
+ const payload = responseProof.tokens;
5838
+ if (!payload?.accessToken || !payload.accessTokenExpiresAt || payload.refreshToken !== nextRefreshToken) {
5839
+ throw new Error(t("errorInvalidCredentials"));
6015
5840
  }
6016
- const source = document.querySelector("article, main") ?? document.body;
6017
- if (!source) return { title: document.title, url: location.href, timeOrigin: performance.timeOrigin };
6018
- const clone = source.cloneNode(true);
6019
- clone.querySelectorAll([
6020
- "script",
6021
- "style",
6022
- "noscript",
6023
- "template",
6024
- "iframe",
6025
- "svg",
6026
- "canvas",
6027
- "video",
6028
- "audio",
6029
- "nav",
6030
- "aside",
6031
- "header",
6032
- "footer",
6033
- "form",
6034
- "[hidden]",
6035
- '[aria-hidden="true"]',
6036
- "[inert]"
6037
- ].join(",")).forEach((element) => element.remove());
6038
- const text = normalize(clone.innerText || clone.textContent || "");
6039
- return { title: document.title, url: location.href, timeOrigin: performance.timeOrigin, text };
5841
+ return { ...payload, gatewayUrl };
6040
5842
  }
6041
- async function captureContextMenuSelection(tabId, selectionText, pageUrl) {
6042
- const tab = await chrome.tabs.get(tabId);
6043
- if (!tab.url) throw new Error(t("errorTabUnavailable"));
6044
- const url = sanitizedUrl(tab.url);
6045
- if (pageUrl && sanitizedUrl(pageUrl) !== url) throw new Error(t("errorPageChangedReading"));
6046
- const selection = selectionText.replace(/\s+/g, " ").trim();
6047
- if (!selection) throw new Error(t("errorSelectText"));
6048
- return buildPageContext({ title: tab.title ?? "", timeOrigin: Date.now(), selection }, url);
5843
+ function wait(milliseconds) {
5844
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
6049
5845
  }
6050
- async function captureTabPage(tabId, mode) {
6051
- const tab = await chrome.tabs.get(tabId);
6052
- if (!tab.url) throw new Error(t("errorTabUnavailable"));
6053
- sanitizedUrl(tab.url);
6054
- const result = await chrome.scripting.executeScript({
6055
- target: { tabId, allFrames: true },
6056
- func: extractPage,
6057
- args: [mode]
6058
- });
6059
- const main = result[0]?.result;
6060
- if (!main) throw new Error(t("errorReadPage"));
6061
- const currentTab = await chrome.tabs.get(tabId);
6062
- if (!currentTab.url || sanitizedUrl(main.url) !== sanitizedUrl(tab.url) || sanitizedUrl(main.url) !== sanitizedUrl(currentTab.url)) {
6063
- throw new Error(t("errorPageChangedReading"));
6064
- }
6065
- const content = result.map((item) => item.result).find((item) => mode === "selection" ? item?.selection : item?.text);
6066
- if (!content?.selection && !content?.text) {
6067
- throw new Error(t(mode === "selection" ? "errorSelectText" : "errorNoReadableContent"));
5846
+ async function pairGateway(invitation, onApproval, requestedOrigin) {
5847
+ const payload = parseBrowserPairingInvitation(invitation, true);
5848
+ const origins = [...new Set(payload.routes.map((route) => new URL(route.url).origin))];
5849
+ const selectedOrigin = requestedOrigin && origins.includes(requestedOrigin) ? requestedOrigin : origins[0];
5850
+ const permittedOrigins = (await Promise.all(origins.map(async (origin) => ({
5851
+ origin,
5852
+ permitted: await hasGatewayOriginPermission(origin)
5853
+ })))).filter((candidate) => candidate.permitted).map((candidate) => candidate.origin);
5854
+ const pair = await getOrCreateKeyPair();
5855
+ const stored = await chrome.storage.local.get(PAIRING_JOURNAL_KEY);
5856
+ let journal = stored[PAIRING_JOURNAL_KEY];
5857
+ if (journal && journal.payload.pairingToken !== payload.pairingToken) {
5858
+ throw new Error("PAIRING_ALREADY_PENDING");
6068
5859
  }
6069
- return buildPageContext({ ...main, selection: content.selection, text: content.text }, main.url);
6070
- }
6071
- async function captureTabWithPermission(tabId, mode) {
6072
- return runWithTabSiteAccess(tabId, () => captureTabPage(tabId, mode));
6073
- }
6074
- async function activeTabId() {
6075
- return (await chrome.tabs.query({ active: true, currentWindow: true }))[0]?.id;
6076
- }
6077
- async function currentTabDescriptor() {
6078
- const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
6079
- if (tab.id === void 0 || tab.windowId === void 0 || !tab.url) throw new Error(t("errorNoActivePage"));
6080
- return runWithTabSiteAccess(tab.id, async (accessibleTab) => {
6081
- const result = await chrome.scripting.executeScript({
6082
- target: { tabId: accessibleTab.id },
6083
- func: () => ({ url: location.href, timeOrigin: performance.timeOrigin })
6084
- });
6085
- const marker = result[0]?.result;
6086
- if (!marker) throw new Error(t("errorIdentifyPage"));
6087
- const currentUrl = sanitizedUrl(marker.url);
6088
- if (new URL(currentUrl).origin !== new URL(accessibleTab.url).origin) {
6089
- throw new Error(t("errorPageChangedConnecting"));
6090
- }
6091
- return {
6092
- tabId: String(accessibleTab.id),
6093
- windowId: String(accessibleTab.windowId),
6094
- documentId: await sha256(`${currentUrl}
6095
- ${marker.timeOrigin}`),
6096
- urlOrigin: new URL(currentUrl).origin
6097
- };
6098
- });
6099
- }
6100
-
6101
- const DATABASE_NAME = "xopc-browser-chat-state";
6102
- const DATABASE_VERSION = 2;
6103
- function openDatabase() {
6104
- return new Promise((resolve, reject) => {
6105
- const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
6106
- request.onupgradeneeded = () => {
6107
- if (!request.result.objectStoreNames.contains("drafts")) request.result.createObjectStore("drafts");
6108
- if (!request.result.objectStoreNames.contains("outbox")) {
6109
- request.result.createObjectStore("outbox");
6110
- }
5860
+ if (!journal) {
5861
+ if (payload.expiresAt <= Date.now()) throw new Error(t("errorExpiredPairingLink"));
5862
+ journal = {
5863
+ invitation: invitation.trim(),
5864
+ payload,
5865
+ requestId: crypto.randomUUID(),
5866
+ idempotencyKey: crypto.randomUUID(),
5867
+ initialRefreshToken: createRefreshToken()
6111
5868
  };
6112
- request.onsuccess = () => resolve(request.result);
6113
- request.onerror = () => reject(request.error ?? new Error(t("errorOpenOutbox")));
6114
- });
6115
- }
6116
- async function clearBrowserChatState() {
6117
- const db = await openDatabase();
6118
- try {
6119
- await new Promise((resolve, reject) => {
6120
- const tx = db.transaction(["outbox", "drafts"], "readwrite");
6121
- tx.objectStore("outbox").clear();
6122
- tx.objectStore("drafts").clear();
6123
- tx.oncomplete = () => resolve();
6124
- tx.onerror = () => reject(tx.error);
6125
- tx.onabort = () => reject(tx.error);
6126
- });
6127
- } finally {
6128
- db.close();
5869
+ await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
5870
+ }
5871
+ delete journal.nextOrigin;
5872
+ await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
5873
+ const requestId = journal.requestId;
5874
+ const availableOrigins = journal.origin && permittedOrigins.includes(journal.origin) ? [journal.origin, ...permittedOrigins.filter((candidate) => candidate !== journal.origin)] : [selectedOrigin, ...permittedOrigins.filter((candidate) => candidate !== selectedOrigin)];
5875
+ try {
5876
+ let { origin, result } = await signedPairingRequestAcrossRoutes(
5877
+ pair,
5878
+ journal.payload,
5879
+ availableOrigins,
5880
+ requestId,
5881
+ "request"
5882
+ );
5883
+ journal.origin = origin;
5884
+ await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
5885
+ if (result.request.status === "pending") onApproval(result.request.confirmationCode);
5886
+ while (result.request.status === "pending") {
5887
+ await wait(1500);
5888
+ const response = await signedPairingRequestAcrossRoutes(
5889
+ pair,
5890
+ journal.payload,
5891
+ [origin, ...permittedOrigins.filter((candidate) => candidate !== origin)],
5892
+ requestId,
5893
+ "status"
5894
+ );
5895
+ ({ origin, result } = response);
5896
+ journal.origin = origin;
5897
+ await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
5898
+ }
5899
+ if (result.request.status !== "approved" && result.request.status !== "completed") {
5900
+ await chrome.storage.local.remove(PAIRING_JOURNAL_KEY);
5901
+ throw new Error(t("errorPairingStatus", result.request.status));
5902
+ }
5903
+ if (result.request.status !== "completed") {
5904
+ const response = await signedPairingRequestAcrossRoutes(
5905
+ pair,
5906
+ journal.payload,
5907
+ [origin, ...permittedOrigins.filter((candidate) => candidate !== origin)],
5908
+ requestId,
5909
+ "complete",
5910
+ {
5911
+ idempotencyKey: journal.idempotencyKey,
5912
+ initialRefreshToken: journal.initialRefreshToken
5913
+ }
5914
+ );
5915
+ ({ origin, result } = response);
5916
+ }
5917
+ if (!result.request.deviceId) throw new Error(t("errorBrowserNotRegistered"));
5918
+ journal.origin = origin;
5919
+ journal.completed = { deviceId: result.request.deviceId, gatewayName: result.gateway.name };
5920
+ journal.refresh ??= { requestId: crypto.randomUUID(), nextRefreshToken: createRefreshToken() };
5921
+ await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
5922
+ const orderedOrigins = [origin, ...permittedOrigins.filter((candidate) => candidate !== origin)];
5923
+ const tokens = await refreshAccessToken(
5924
+ orderedOrigins,
5925
+ journal.initialRefreshToken,
5926
+ pair,
5927
+ journal.payload,
5928
+ journal.refresh
5929
+ );
5930
+ const profile = {
5931
+ gatewayId: journal.payload.gatewayId,
5932
+ gatewayName: journal.completed.gatewayName,
5933
+ gatewayUrl: tokens.gatewayUrl,
5934
+ gatewayUrls: [tokens.gatewayUrl, ...orderedOrigins.filter((candidate) => candidate !== tokens.gatewayUrl)],
5935
+ gatewayPublicKey: journal.payload.gatewayPublicKey,
5936
+ deviceId: journal.completed.deviceId,
5937
+ refreshToken: tokens.refreshToken,
5938
+ accessToken: tokens.accessToken,
5939
+ accessTokenExpiresAt: tokens.accessTokenExpiresAt
5940
+ };
5941
+ await chrome.storage.local.set({ [PROFILE_KEY]: profile });
5942
+ await chrome.storage.local.remove(PAIRING_JOURNAL_KEY);
5943
+ return profile;
5944
+ } catch (cause) {
5945
+ const nextOrigin = origins.find((origin) => !permittedOrigins.includes(origin));
5946
+ if (nextOrigin && isRouteFailure(cause)) {
5947
+ journal.nextOrigin = nextOrigin;
5948
+ await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
5949
+ throw new Error(`PAIRING_ROUTE_PERMISSION_REQUIRED:${nextOrigin}`);
5950
+ }
5951
+ throw cause;
6129
5952
  }
6130
5953
  }
6131
-
6132
- const PROFILE_KEY = "xopc.browser.profile";
6133
- const PAIRING_JOURNAL_KEY = "xopc.browser.pairing";
6134
- const REFRESH_JOURNAL_KEY = "xopc.browser.auth-refresh";
6135
- const AUTO_CONNECT_KEY = "xopc.browser.auto-connect";
6136
- const KEY_DATABASE = "xopc-browser-identity";
6137
- const KEY_STORE = "identity";
6138
- const KEY_NAME = "device-key";
6139
- const NATIVE_HOST_NAME = "ai.xopc.browser";
6140
- const GATEWAY_REQUEST_TIMEOUT_MS = 8e3;
6141
- const ACCESS_TOKEN_EXPIRY_MARGIN_MS = 3e4;
6142
- const REFRESH_LOCK_NAME = "xopc.browser.auth-refresh";
6143
- let refreshTask;
6144
- function assertProfileIdentity(profile, expected) {
6145
- if (!profile || profile.gatewayId !== expected.gatewayId || profile.deviceId !== expected.deviceId || profile.gatewayPublicKey !== expected.gatewayPublicKey) throw new Error("Gateway identity changed");
6146
- }
6147
- async function browserPublicKeyThumbprint(jwk) {
6148
- const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
6149
- return encodeBase64Url(new Uint8Array(await crypto.subtle.digest(
6150
- "SHA-256",
6151
- new TextEncoder().encode(canonical)
6152
- )));
6153
- }
6154
- async function isAutoConnectEnabled() {
6155
- const stored = await chrome.storage.local.get(AUTO_CONNECT_KEY);
6156
- return stored[AUTO_CONNECT_KEY] !== false;
6157
- }
6158
- async function setAutoConnectEnabled(enabled) {
6159
- await chrome.storage.local.set({ [AUTO_CONNECT_KEY]: enabled });
5954
+ async function readPendingBrowserPairing() {
5955
+ const stored = await chrome.storage.local.get(PAIRING_JOURNAL_KEY);
5956
+ const journal = stored[PAIRING_JOURNAL_KEY];
5957
+ return journal ? { invitation: journal.invitation, ...journal.nextOrigin ? { nextOrigin: journal.nextOrigin } : {} } : void 0;
6160
5958
  }
6161
- async function discoverLocalGateway(options) {
5959
+ async function cancelPendingBrowserPairing() {
5960
+ const stored = await chrome.storage.local.get(PAIRING_JOURNAL_KEY);
5961
+ const journal = stored[PAIRING_JOURNAL_KEY];
5962
+ if (!journal) return;
5963
+ const origins = [...new Set(journal.payload.routes.map((route) => new URL(route.url).origin))];
5964
+ const permittedOrigins = (await Promise.all(origins.map(async (origin) => ({
5965
+ origin,
5966
+ permitted: await hasGatewayOriginPermission(origin)
5967
+ })))).filter((candidate) => candidate.permitted).map((candidate) => candidate.origin);
6162
5968
  try {
6163
- if (!options?.force && !await isAutoConnectEnabled()) return void 0;
6164
- const pair = await getOrCreateKeyPair();
6165
- const key = await publicKeyJwk(pair);
6166
- const nonce = randomNonce();
6167
- const response = await chrome.runtime.sendNativeMessage(
6168
- NATIVE_HOST_NAME,
6169
- {
6170
- type: "bootstrap",
6171
- extensionId: chrome.runtime.id,
6172
- publicKeyJwk: key,
6173
- publicKeyThumbprint: await browserPublicKeyThumbprint(key),
6174
- nonce
6175
- }
5969
+ await signedPairingRequestAcrossRoutes(
5970
+ await getOrCreateKeyPair(),
5971
+ journal.payload,
5972
+ journal.origin && permittedOrigins.includes(journal.origin) ? [journal.origin, ...permittedOrigins.filter((candidate) => candidate !== journal.origin)] : permittedOrigins,
5973
+ journal.requestId,
5974
+ "cancel"
6176
5975
  );
6177
- if (response.ok !== true || typeof response.gatewayUrl !== "string" || typeof response.invitation !== "string" || typeof response.expiresAt !== "number") {
6178
- if (options?.force) throw new Error(response.error || t("errorInvalidLocalHostResponse"));
6179
- return void 0;
6180
- }
6181
- return {
6182
- gatewayUrl: response.gatewayUrl,
6183
- invitation: response.invitation,
6184
- expiresAt: response.expiresAt
6185
- };
6186
5976
  } catch (cause) {
6187
- if (options?.force) throw cause;
6188
- return void 0;
5977
+ if (!(cause instanceof GatewayHttpError) || !["PAIRING_NOT_FOUND", "PAIRING_EXPIRED", "PAIRING_CANCELLED"].includes(cause.message)) throw cause;
6189
5978
  }
5979
+ await chrome.storage.local.remove(PAIRING_JOURNAL_KEY);
6190
5980
  }
6191
- function encodeBase64Url(bytes) {
6192
- let binary = "";
6193
- bytes.forEach((byte) => {
6194
- binary += String.fromCharCode(byte);
6195
- });
6196
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
6197
- }
6198
- function decodeBase64Url(value) {
6199
- const padded = value.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - value.length % 4) % 4);
6200
- const binary = atob(padded);
6201
- return Uint8Array.from(binary, (character) => character.charCodeAt(0));
5981
+ async function readProfile() {
5982
+ const stored = await chrome.storage.local.get([PROFILE_KEY, PAIRING_JOURNAL_KEY]);
5983
+ const profile = stored[PROFILE_KEY];
5984
+ if (!profile?.gatewayId || !profile.gatewayUrl || !profile.refreshToken) return void 0;
5985
+ const pairing = stored[PAIRING_JOURNAL_KEY];
5986
+ if (pairing?.payload?.gatewayId === profile.gatewayId && pairing.completed?.deviceId === profile.deviceId) {
5987
+ await chrome.storage.local.remove(PAIRING_JOURNAL_KEY);
5988
+ }
5989
+ return profile;
6202
5990
  }
6203
- function decodeJson(value) {
6204
- return JSON.parse(new TextDecoder().decode(decodeBase64Url(value)));
5991
+ function withRefreshLock(operation) {
5992
+ const locks = globalThis.navigator?.locks;
5993
+ if (!locks) return operation();
5994
+ return new Promise((resolve, reject) => {
5995
+ void locks.request(REFRESH_LOCK_NAME, () => operation().then(resolve, reject)).catch(reject);
5996
+ });
6205
5997
  }
6206
- function randomNonce(bytes = 24) {
6207
- return encodeBase64Url(crypto.getRandomValues(new Uint8Array(bytes)));
5998
+ function isRefreshDenied(cause) {
5999
+ return cause instanceof GatewayHttpError && cause.status === 401 && cause.code === "REFRESH_DENIED";
6208
6000
  }
6209
- function createRefreshToken() {
6210
- return `xopc_rt_${crypto.randomUUID()}_${randomNonce(32)}`;
6001
+ async function refreshJournalFor(profile) {
6002
+ const stored = await chrome.storage.local.get(REFRESH_JOURNAL_KEY);
6003
+ const journal = stored[REFRESH_JOURNAL_KEY];
6004
+ if (journal?.gatewayId === profile.gatewayId && journal.refreshToken === profile.refreshToken) return journal;
6005
+ const next = {
6006
+ gatewayId: profile.gatewayId,
6007
+ refreshToken: profile.refreshToken,
6008
+ requestId: crypto.randomUUID(),
6009
+ nextRefreshToken: createRefreshToken()
6010
+ };
6011
+ await chrome.storage.local.set({ [REFRESH_JOURNAL_KEY]: next });
6012
+ return next;
6211
6013
  }
6212
- function parseBrowserPairingInvitation(value, allowExpired = false) {
6213
- const encoded = readBrowserPairingInvitation(value);
6214
- const parsed = browserPairingInvitationPayloadSchema.safeParse(decodeJson(encoded));
6215
- if (!parsed.success) throw new Error("Pairing invitation version is not supported");
6216
- const payload = parsed.data;
6217
- if (!allowExpired && payload.expiresAt <= Date.now()) throw new Error("Pairing invitation has expired");
6218
- return payload;
6014
+ async function recoverLocalProfile(gatewayId) {
6015
+ const local = await discoverLocalGateway();
6016
+ if (!local) return void 0;
6017
+ try {
6018
+ if (parseBrowserPairingInvitation(local.invitation).gatewayId !== gatewayId) return void 0;
6019
+ return await pairGateway(local.invitation, () => void 0, new URL(local.gatewayUrl).origin);
6020
+ } catch {
6021
+ return void 0;
6022
+ }
6219
6023
  }
6220
- function openIdentityDatabase() {
6221
- return new Promise((resolve, reject) => {
6222
- const request = indexedDB.open(KEY_DATABASE, 1);
6223
- request.onupgradeneeded = () => request.result.createObjectStore(KEY_STORE);
6224
- request.onsuccess = () => resolve(request.result);
6225
- request.onerror = () => reject(request.error);
6024
+ async function refreshStoredProfile(options) {
6025
+ return withRefreshLock(async () => {
6026
+ const profile = await readProfile();
6027
+ if (!profile) throw new Error(t("errorBrowserNotPaired"));
6028
+ if (options?.rejectedAccessToken && profile.accessToken !== options.rejectedAccessToken && profile.accessTokenExpiresAt > Date.now() + ACCESS_TOKEN_EXPIRY_MARGIN_MS) return profile;
6029
+ if (!options?.rejectedAccessToken && profile.accessTokenExpiresAt > Date.now() + ACCESS_TOKEN_EXPIRY_MARGIN_MS) return profile;
6030
+ const gatewayUrls = profile.gatewayUrls?.length ? profile.gatewayUrls : [profile.gatewayUrl];
6031
+ const journal = await refreshJournalFor(profile);
6032
+ try {
6033
+ const tokens = await refreshAccessToken(
6034
+ gatewayUrls,
6035
+ profile.refreshToken,
6036
+ await getOrCreateKeyPair(),
6037
+ profile,
6038
+ journal
6039
+ );
6040
+ const current = await readProfile();
6041
+ assertProfileIdentity(current, profile);
6042
+ if (current && current.gatewayId === profile.gatewayId && current.refreshToken !== profile.refreshToken) {
6043
+ return current;
6044
+ }
6045
+ const next = {
6046
+ ...profile,
6047
+ ...tokens,
6048
+ gatewayUrls: [tokens.gatewayUrl, ...gatewayUrls.filter((candidate) => candidate !== tokens.gatewayUrl)]
6049
+ };
6050
+ await chrome.storage.local.set({ [PROFILE_KEY]: next });
6051
+ await chrome.storage.local.remove(REFRESH_JOURNAL_KEY);
6052
+ return next;
6053
+ } catch (cause) {
6054
+ const current = await readProfile();
6055
+ assertProfileIdentity(current, profile);
6056
+ if (current && current.gatewayId === profile.gatewayId && current.refreshToken !== profile.refreshToken && current.accessTokenExpiresAt > Date.now()) return current;
6057
+ if (!isRefreshDenied(cause)) throw cause;
6058
+ const recovered = await recoverLocalProfile(profile.gatewayId);
6059
+ if (recovered) {
6060
+ await chrome.storage.local.remove(REFRESH_JOURNAL_KEY);
6061
+ return recovered;
6062
+ }
6063
+ await chrome.storage.local.remove([PROFILE_KEY, REFRESH_JOURNAL_KEY]);
6064
+ throw new Error(t("errorBrowserNotPaired"), { cause });
6065
+ }
6226
6066
  });
6227
6067
  }
6228
- async function storedKeyPair() {
6229
- const database = await openIdentityDatabase();
6068
+ async function getAccessProfile(options) {
6069
+ const profile = await readProfile();
6070
+ if (!profile) throw new Error(t("errorBrowserNotPaired"));
6071
+ if (!options?.rejectedAccessToken && profile.accessTokenExpiresAt > Date.now() + ACCESS_TOKEN_EXPIRY_MARGIN_MS) return profile;
6072
+ if (refreshTask) return refreshTask;
6073
+ refreshTask = refreshStoredProfile(options);
6230
6074
  try {
6231
- return await new Promise((resolve, reject) => {
6232
- const request = database.transaction(KEY_STORE).objectStore(KEY_STORE).get(KEY_NAME);
6233
- request.onsuccess = () => resolve(request.result);
6234
- request.onerror = () => reject(request.error);
6235
- });
6075
+ return await refreshTask;
6236
6076
  } finally {
6237
- database.close();
6077
+ refreshTask = void 0;
6238
6078
  }
6239
6079
  }
6240
- async function getOrCreateKeyPair() {
6241
- const existing = await storedKeyPair();
6242
- if (existing) return existing;
6243
- const pair = await crypto.subtle.generateKey(
6244
- { name: "ECDSA", namedCurve: "P-256" },
6245
- false,
6246
- ["sign", "verify"]
6247
- );
6248
- const database = await openIdentityDatabase();
6249
- await new Promise((resolve, reject) => {
6250
- const request = database.transaction(KEY_STORE, "readwrite").objectStore(KEY_STORE).put(pair, KEY_NAME);
6251
- request.onsuccess = () => resolve();
6252
- request.onerror = () => reject(request.error);
6253
- }).finally(() => database.close());
6254
- return pair;
6080
+ async function gatewayFetch(path, init = {}, expectedIdentity) {
6081
+ let profile = await getAccessProfile();
6082
+ const identity = expectedIdentity ?? profile;
6083
+ assertProfileIdentity(profile, identity);
6084
+ const request = () => {
6085
+ const headers = new Headers(init.headers);
6086
+ headers.set("Authorization", `Bearer ${profile.accessToken}`);
6087
+ return fetch(`${profile.gatewayUrl}${path}`, { ...init, headers });
6088
+ };
6089
+ let response = await request();
6090
+ assertProfileIdentity(await readProfile(), identity);
6091
+ if (response.status !== 401) return response;
6092
+ profile = await getAccessProfile({ rejectedAccessToken: profile.accessToken });
6093
+ assertProfileIdentity(profile, identity);
6094
+ response = await request();
6095
+ assertProfileIdentity(await readProfile(), identity);
6096
+ return response;
6255
6097
  }
6256
- async function forgetIdentity() {
6257
- await new Promise((resolve, reject) => {
6258
- const request = indexedDB.deleteDatabase(KEY_DATABASE);
6259
- request.onsuccess = () => resolve();
6260
- request.onerror = () => reject(request.error);
6261
- request.onblocked = () => reject(new Error(t("errorIdentityInUse")));
6098
+ async function registerBrowserEndpoint() {
6099
+ const profile = await getAccessProfile();
6100
+ const pair = await getOrCreateKeyPair();
6101
+ const publicKey = encodeBase64Url(new Uint8Array(await crypto.subtle.exportKey("spki", pair.publicKey)));
6102
+ const response = await gatewayFetch("/api/endpoint-tools/principals", {
6103
+ method: "POST",
6104
+ headers: { "Content-Type": "application/json" },
6105
+ body: JSON.stringify({
6106
+ principalId: profile.deviceId,
6107
+ displayName: "xopc Chrome",
6108
+ kind: "browser",
6109
+ platform: "chrome",
6110
+ publicKey
6111
+ })
6262
6112
  });
6113
+ if (!response.ok) throw new Error(t("errorEndpointRegistration", String(response.status)));
6263
6114
  }
6264
- async function publicKeyJwk(pair) {
6265
- const exported = await crypto.subtle.exportKey("jwk", pair.publicKey);
6266
- if (!exported.x || !exported.y) throw new Error(t("errorIdentityExport"));
6267
- return { kty: "EC", crv: "P-256", x: exported.x, y: exported.y };
6115
+ async function createBrowserEndpointHello(tools = []) {
6116
+ const profile = await getAccessProfile();
6117
+ const pair = await getOrCreateKeyPair();
6118
+ const unsigned = {
6119
+ principalId: profile.deviceId,
6120
+ endpointId: `browser:${profile.deviceId}`,
6121
+ connectionInstanceId: crypto.randomUUID(),
6122
+ displayName: "xopc Chrome",
6123
+ kind: "browser",
6124
+ platform: "chrome",
6125
+ appVersion: chrome.runtime.getManifest().version,
6126
+ availability: "foreground",
6127
+ nonce: crypto.randomUUID(),
6128
+ signedAt: Date.now(),
6129
+ signature: "pending",
6130
+ tools
6131
+ };
6132
+ return {
6133
+ ...unsigned,
6134
+ signature: await sign(pair, endpointHelloSigningPayload(unsigned))
6135
+ };
6268
6136
  }
6269
- async function sign(pair, text) {
6270
- const signature = await crypto.subtle.sign(
6271
- { name: "ECDSA", hash: "SHA-256" },
6272
- pair.privateKey,
6273
- new TextEncoder().encode(text)
6274
- );
6275
- return encodeBase64Url(new Uint8Array(signature));
6137
+ async function forgetProfile() {
6138
+ await chrome.storage.local.remove([PROFILE_KEY, PAIRING_JOURNAL_KEY, REFRESH_JOURNAL_KEY]);
6276
6139
  }
6277
- async function verifyGateway(publicKey, payload, signature) {
6140
+ async function revokeAndForgetProfile() {
6278
6141
  try {
6279
- const key = await crypto.subtle.importKey(
6280
- "raw",
6281
- decodeBase64Url(publicKey),
6282
- { name: "Ed25519" },
6283
- false,
6284
- ["verify"]
6285
- );
6286
- return crypto.subtle.verify(
6287
- { name: "Ed25519" },
6288
- key,
6289
- decodeBase64Url(signature),
6290
- new TextEncoder().encode(payload)
6291
- );
6292
- } catch {
6293
- return false;
6142
+ const profile = await getAccessProfile();
6143
+ await fetch(`${profile.gatewayUrl}/api/devices/me`, {
6144
+ method: "DELETE",
6145
+ headers: { Authorization: `Bearer ${profile.accessToken}` }
6146
+ });
6147
+ } finally {
6148
+ await forgetProfile();
6149
+ try {
6150
+ await clearBrowserChatState();
6151
+ } finally {
6152
+ try {
6153
+ await forgetIdentity();
6154
+ } finally {
6155
+ await setAutoConnectEnabled(false);
6156
+ }
6157
+ }
6294
6158
  }
6295
6159
  }
6296
- function requestGatewayOriginPermissions(origins) {
6297
- const patterns = origins.filter((origin) => {
6298
- const url = new URL(origin);
6299
- return !(url.protocol === "http:" && ["127.0.0.1", "localhost"].includes(url.hostname));
6300
- }).map((origin) => `${new URL(origin).origin}/*`);
6301
- if (!patterns.length) return Promise.resolve(true);
6302
- return chrome.permissions.request({ origins: patterns });
6160
+
6161
+ const REALTIME_PROTOCOL_VERSION = 2;
6162
+ const REALTIME_MAX_CLIENT_FRAME_BYTES = 256 * 1024;
6163
+ const REALTIME_HEARTBEAT_INTERVAL_MS = 15e3;
6164
+ const REALTIME_HEARTBEAT_TIMEOUT_MS = 45e3;
6165
+ const REALTIME_CAPABILITIES = ["realtime.capability-negotiation.v1"];
6166
+ const realtimeClientKindSchema = _enum([
6167
+ "web",
6168
+ "desktop",
6169
+ "mobile",
6170
+ "browser_extension",
6171
+ "tui",
6172
+ "mcp"
6173
+ ]);
6174
+ const realtimeTopicSchema = string().min(1).max(512);
6175
+ const realtimeEventNameSchema = string().regex(/^[a-z][a-z0-9_.-]*$/).max(160);
6176
+ const messageIdSchema = uuid();
6177
+ const timestampSchema = number().int().nonnegative();
6178
+ const sequenceSchema = number().int().positive();
6179
+ const capabilitySchema = string().min(1).max(160);
6180
+ const envelope = (kind, payload) => object({
6181
+ protocolVersion: literal(REALTIME_PROTOCOL_VERSION),
6182
+ messageId: messageIdSchema,
6183
+ kind: literal(kind),
6184
+ sentAt: timestampSchema,
6185
+ payload
6186
+ });
6187
+ const realtimeSubscriptionSchema = object({
6188
+ topic: realtimeTopicSchema,
6189
+ afterSeq: number().int().nonnegative().optional(),
6190
+ view: literal("compact").optional()
6191
+ });
6192
+ discriminatedUnion("kind", [
6193
+ envelope("realtime.hello", object({
6194
+ ticket: string().min(32).max(512),
6195
+ clientId: string().min(1).max(160),
6196
+ clientKind: realtimeClientKindSchema,
6197
+ subscriptions: array(realtimeSubscriptionSchema).max(100).default([]),
6198
+ capabilities: array(capabilitySchema).max(64).optional(),
6199
+ endpoint: endpointHelloPayloadSchema.optional()
6200
+ })),
6201
+ envelope("realtime.subscribe", object({
6202
+ subscriptions: array(realtimeSubscriptionSchema).min(1).max(100)
6203
+ })),
6204
+ envelope("realtime.unsubscribe", object({
6205
+ topics: array(realtimeTopicSchema).min(1).max(100)
6206
+ })),
6207
+ envelope("realtime.ping", object({})),
6208
+ envelope("endpoint.message", clientEndpointMessageSchema)
6209
+ ]);
6210
+ const realtimeEventPayloadSchema = object({
6211
+ topic: realtimeTopicSchema,
6212
+ seq: sequenceSchema,
6213
+ event: realtimeEventNameSchema,
6214
+ data: unknown()
6215
+ });
6216
+ const serverRealtimeMessageSchema = discriminatedUnion("kind", [
6217
+ envelope("realtime.ready", object({
6218
+ connectionId: uuid(),
6219
+ heartbeatIntervalMs: literal(REALTIME_HEARTBEAT_INTERVAL_MS),
6220
+ heartbeatTimeoutMs: literal(REALTIME_HEARTBEAT_TIMEOUT_MS),
6221
+ negotiatedCapabilities: array(capabilitySchema).max(64).optional(),
6222
+ endpoint: object({
6223
+ endpointId: string().min(1).max(160),
6224
+ turnToken: string().min(32).max(160)
6225
+ }).optional()
6226
+ })),
6227
+ envelope("realtime.event", realtimeEventPayloadSchema),
6228
+ envelope("realtime.subscribed", object({
6229
+ topic: realtimeTopicSchema,
6230
+ cursor: number().int().nonnegative()
6231
+ })),
6232
+ envelope("realtime.gap", object({
6233
+ topic: realtimeTopicSchema,
6234
+ requestedSeq: number().int().nonnegative(),
6235
+ earliestSeq: sequenceSchema,
6236
+ recoverable: boolean()
6237
+ })),
6238
+ envelope("realtime.pong", object({})),
6239
+ envelope("realtime.error", object({
6240
+ code: string().min(1).max(80),
6241
+ message: string().min(1).max(500)
6242
+ })),
6243
+ envelope("endpoint.message", serverEndpointMessageSchema)
6244
+ ]);
6245
+ function parseServerRealtimeMessage(value) {
6246
+ return serverRealtimeMessageSchema.parse(value);
6247
+ }
6248
+
6249
+ class RealtimeConnectionError extends Error {
6250
+ constructor(message, retryable) {
6251
+ super(message);
6252
+ this.retryable = retryable;
6253
+ this.name = "RealtimeConnectionError";
6254
+ }
6255
+ retryable;
6303
6256
  }
6304
- async function hasGatewayOriginPermission(origin) {
6305
- const url = new URL(origin);
6306
- if (url.protocol === "http:" && ["127.0.0.1", "localhost"].includes(url.hostname)) return true;
6307
- return chrome.permissions.contains({ origins: [`${url.origin}/*`] });
6257
+ class RealtimeFrameTooLargeError extends Error {
6258
+ constructor(bytes, maxBytes) {
6259
+ super(`Realtime client frame is ${bytes} bytes; maximum is ${maxBytes} bytes`);
6260
+ this.bytes = bytes;
6261
+ this.maxBytes = maxBytes;
6262
+ this.name = "RealtimeFrameTooLargeError";
6263
+ }
6264
+ bytes;
6265
+ maxBytes;
6308
6266
  }
6309
- class GatewayHttpError extends Error {
6310
- constructor(code, status, serverMessage) {
6311
- super(code);
6312
- this.code = code;
6313
- this.status = status;
6314
- this.serverMessage = serverMessage;
6315
- this.name = "GatewayHttpError";
6267
+ function frameText(data) {
6268
+ if (typeof data === "string") return data;
6269
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
6270
+ if (ArrayBuffer.isView(data)) {
6271
+ const bytes = Uint8Array.from(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
6272
+ return new TextDecoder().decode(bytes);
6316
6273
  }
6317
- code;
6318
- status;
6319
- serverMessage;
6274
+ throw new Error("Realtime server sent a non-text frame");
6320
6275
  }
6321
- async function post(origin, path, body) {
6322
- const controller = new AbortController();
6323
- const timeout = setTimeout(() => controller.abort(), GATEWAY_REQUEST_TIMEOUT_MS);
6324
- let response;
6325
- try {
6326
- response = await fetch(`${origin}${path}`, {
6327
- method: "POST",
6328
- headers: { "Content-Type": "application/json" },
6329
- body: JSON.stringify(body),
6330
- signal: controller.signal
6331
- });
6332
- } catch (cause) {
6333
- if (controller.signal.aborted) throw new Error(t("errorGatewayTimeout", String(GATEWAY_REQUEST_TIMEOUT_MS / 1e3)));
6334
- throw cause;
6335
- } finally {
6336
- clearTimeout(timeout);
6276
+ function isNonRetryableCloseCode(code) {
6277
+ return code === 1009 || code === 4400 || code === 4401 || code === 4409 || code === 4413;
6278
+ }
6279
+ class RealtimeClient {
6280
+ constructor(options) {
6281
+ this.options = options;
6282
+ }
6283
+ options;
6284
+ desiredSubscriptions = /* @__PURE__ */ new Map();
6285
+ cursors = /* @__PURE__ */ new Map();
6286
+ socket;
6287
+ reconnectTimer;
6288
+ heartbeatTimer;
6289
+ connectionTimer;
6290
+ ticketAbort;
6291
+ generation = 0;
6292
+ reconnectAttempts = 0;
6293
+ shouldReconnect = false;
6294
+ ready = false;
6295
+ lastServerMessageAt = 0;
6296
+ endpointBinding;
6297
+ reconnectWhenSocketOpens = false;
6298
+ offeredCapabilities = /* @__PURE__ */ new Set();
6299
+ connect() {
6300
+ if (this.shouldReconnect) return;
6301
+ this.shouldReconnect = true;
6302
+ void this.open(false);
6303
+ }
6304
+ disconnect() {
6305
+ this.shouldReconnect = false;
6306
+ this.reconnectWhenSocketOpens = false;
6307
+ this.generation += 1;
6308
+ this.clearTimers();
6309
+ this.ready = false;
6310
+ this.offeredCapabilities.clear();
6311
+ this.endpointBinding?.onDisconnected?.();
6312
+ this.socket?.close(1e3, "Client disconnected");
6313
+ this.socket = void 0;
6314
+ this.options.onStateChange?.("disconnected");
6315
+ }
6316
+ reconnect() {
6317
+ this.reconnectWhenSocketOpens = false;
6318
+ this.generation += 1;
6319
+ this.clearTimers();
6320
+ this.ready = false;
6321
+ this.endpointBinding?.onDisconnected?.();
6322
+ this.socket?.close(1e3, "Client reconnecting");
6323
+ this.socket = void 0;
6324
+ this.reconnectAttempts = 0;
6325
+ this.shouldReconnect = true;
6326
+ void this.open(true);
6327
+ }
6328
+ subscribe(topic, afterSeq) {
6329
+ this.desiredSubscriptions.set(topic, afterSeq);
6330
+ if (afterSeq !== void 0) this.cursors.set(topic, afterSeq);
6331
+ if (this.ready) {
6332
+ this.sendSubscriptions([{ topic, afterSeq: this.cursors.get(topic) ?? afterSeq }]);
6333
+ }
6337
6334
  }
6338
- const json = await response.json().catch(() => ({}));
6339
- if (!response.ok) {
6340
- throw new GatewayHttpError(
6341
- json.error?.code ?? json.error?.message ?? t("errorGatewayStatus", String(response.status)),
6342
- response.status,
6343
- json.error?.message
6344
- );
6335
+ unsubscribe(topic) {
6336
+ this.desiredSubscriptions.delete(topic);
6337
+ if (this.ready) this.send(this.clientMessage("realtime.unsubscribe", { topics: [topic] }));
6345
6338
  }
6346
- return json;
6347
- }
6348
- async function signedPairingRequest(pair, payload, origin, requestId, action, completion) {
6349
- const nonce = randomNonce();
6350
- const body = {
6351
- gatewayId: payload.gatewayId,
6352
- requestId,
6353
- pairingToken: payload.pairingToken,
6354
- timestamp: Date.now(),
6355
- nonce,
6356
- ...action === "request" ? {
6357
- device: {
6358
- displayName: `Chrome · ${navigator.platform || "Browser"}`,
6359
- platform: "chrome",
6360
- extensionId: chrome.runtime.id,
6361
- publicKeyJwk: await publicKeyJwk(pair)
6362
- }
6363
- } : {},
6364
- ...action === "complete" ? completion : {}
6365
- };
6366
- const signature = await sign(pair, buildDevicePairingProof(action, body));
6367
- const path = action === "request" ? "/api/device-pairing/requests" : `/api/device-pairing/requests/${requestId}/${action}`;
6368
- const response = await post(origin, path, { ...body, signature });
6369
- if (typeof response.signedPayload !== "string" || typeof response.signature !== "string") {
6370
- throw new Error(t("errorUnsignedPairing"));
6339
+ sendEndpointMessage(message) {
6340
+ if (!this.ready) throw new Error("Realtime connection is not ready");
6341
+ this.send(this.clientMessage("endpoint.message", message));
6371
6342
  }
6372
- if (!await verifyGateway(payload.gatewayPublicKey, response.signedPayload, response.signature)) {
6373
- throw new Error(t("errorGatewayVerification"));
6343
+ setEndpoint(binding) {
6344
+ this.endpointBinding = binding;
6345
+ this.refreshEndpointConnection();
6374
6346
  }
6375
- const decoded = decodeJson(response.signedPayload);
6376
- if (decoded.gateway.id !== payload.gatewayId || decoded.nonce !== nonce || decoded.request.requestId !== requestId) {
6377
- throw new Error(t("errorPairingMismatch"));
6347
+ clearEndpoint(binding) {
6348
+ if (binding && this.endpointBinding !== binding) return;
6349
+ const previous = this.endpointBinding;
6350
+ this.endpointBinding = void 0;
6351
+ previous?.onDisconnected?.();
6352
+ this.refreshEndpointConnection();
6378
6353
  }
6379
- return decoded;
6380
- }
6381
- function shouldTryNextRoute(cause) {
6382
- return !(cause instanceof GatewayHttpError) || isRetryableDevicePairingHttpStatus(cause.status);
6383
- }
6384
- function isRouteFailure(cause) {
6385
- if (cause instanceof GatewayHttpError) return isRetryableDevicePairingHttpStatus(cause.status);
6386
- const message = cause instanceof Error ? cause.message : String(cause);
6387
- return /network|fetch|respond|identity|route/i.test(message);
6388
- }
6389
- async function signedPairingRequestAcrossRoutes(pair, payload, origins, requestId, action, completion) {
6390
- let lastRouteError;
6391
- for (const origin of origins) {
6392
- try {
6393
- return { origin, result: await signedPairingRequest(pair, payload, origin, requestId, action, completion) };
6394
- } catch (cause) {
6395
- if (!shouldTryNextRoute(cause)) throw cause;
6396
- lastRouteError = cause;
6354
+ refreshEndpointConnection() {
6355
+ if (!this.shouldReconnect) return;
6356
+ if (this.socket?.readyState === 0) {
6357
+ this.reconnectWhenSocketOpens = true;
6358
+ return;
6397
6359
  }
6360
+ this.reconnect();
6398
6361
  }
6399
- throw lastRouteError instanceof Error ? lastRouteError : new Error("No Gateway route could be reached");
6400
- }
6401
- async function refreshAccessToken(gatewayUrls, refreshToken, pair, gateway, operation = {
6402
- requestId: crypto.randomUUID(),
6403
- nextRefreshToken: createRefreshToken()
6404
- }) {
6405
- const { nextRefreshToken, requestId } = operation;
6406
- const timestamp = Date.now();
6407
- const nonce = randomNonce();
6408
- const credentialId = refreshToken.slice("xopc_rt_".length).split("_")[0];
6409
- const proof = `xopc-device-refresh-v2
6410
- ${credentialId}
6411
- ${timestamp}
6412
- ${nonce}
6413
- ${requestId}
6414
- ${nextRefreshToken}`;
6415
- const signature = await sign(pair, proof);
6416
- const body = {
6417
- refreshToken,
6418
- nextRefreshToken,
6419
- requestId,
6420
- timestamp,
6421
- nonce,
6422
- signature
6423
- };
6424
- let gatewayUrl;
6425
- let response;
6426
- let lastRouteError;
6427
- for (const origin of gatewayUrls) {
6362
+ async open(reconnecting) {
6363
+ const generation = ++this.generation;
6364
+ const timeoutMs = this.options.connectionTimeoutMs ?? 15e3;
6365
+ const ticketAbort = new AbortController();
6366
+ this.ticketAbort?.abort();
6367
+ this.ticketAbort = ticketAbort;
6368
+ let socket;
6369
+ let closed = false;
6370
+ let messageQueue;
6371
+ const failAttempt = (error, retry = true) => {
6372
+ if (closed || generation !== this.generation) return;
6373
+ closed = true;
6374
+ this.clearConnectionTimer();
6375
+ ticketAbort.abort();
6376
+ if (this.ticketAbort === ticketAbort) this.ticketAbort = void 0;
6377
+ this.ready = false;
6378
+ this.offeredCapabilities.clear();
6379
+ this.endpointBinding?.onDisconnected?.();
6380
+ this.clearHeartbeat();
6381
+ if (this.socket === socket) this.socket = void 0;
6382
+ if (socket) {
6383
+ socket.onopen = null;
6384
+ socket.onmessage = null;
6385
+ socket.onerror = null;
6386
+ socket.onclose = null;
6387
+ socket.close(4e3, error.slice(0, 120));
6388
+ }
6389
+ if (this.shouldReconnect && retry) this.scheduleReconnect(error);
6390
+ else if (this.shouldReconnect) {
6391
+ this.shouldReconnect = false;
6392
+ this.options.onStateChange?.("error", error);
6393
+ } else this.options.onStateChange?.("disconnected");
6394
+ };
6395
+ this.connectionTimer = setTimeout(() => failAttempt("Realtime connection timed out"), timeoutMs);
6396
+ this.options.onStateChange?.(reconnecting ? "reconnecting" : "connecting");
6428
6397
  try {
6429
- response = await post(origin, "/api/device-auth/refresh", body);
6430
- gatewayUrl = origin;
6431
- break;
6432
- } catch (cause) {
6433
- if (!shouldTryNextRoute(cause)) throw cause;
6434
- lastRouteError = cause;
6398
+ const [issuedTicket, endpoint] = await Promise.all([
6399
+ this.options.issueTicket(ticketAbort.signal),
6400
+ this.endpointBinding?.createHello()
6401
+ ]);
6402
+ const ticket = issuedTicket.ticket;
6403
+ if (REALTIME_PROTOCOL_VERSION < issuedTicket.realtime.minVersion) {
6404
+ throw new RealtimeConnectionError("CLIENT_UPDATE_REQUIRED", false);
6405
+ }
6406
+ if (REALTIME_PROTOCOL_VERSION > issuedTicket.realtime.maxVersion) {
6407
+ throw new RealtimeConnectionError("GATEWAY_UPDATE_REQUIRED", false);
6408
+ }
6409
+ const capabilities = REALTIME_CAPABILITIES.filter((capability) => issuedTicket.realtime.capabilities.includes(capability));
6410
+ if (closed || !this.shouldReconnect || generation !== this.generation) return;
6411
+ this.offeredCapabilities = new Set(capabilities ?? []);
6412
+ socket = this.options.createWebSocket(this.options.getWebSocketUrl());
6413
+ this.socket = socket;
6414
+ socket.onopen = () => {
6415
+ if (generation !== this.generation) return;
6416
+ if (this.reconnectWhenSocketOpens) {
6417
+ this.reconnect();
6418
+ return;
6419
+ }
6420
+ const subscriptions = [...this.desiredSubscriptions].map(([topic, afterSeq]) => ({
6421
+ topic,
6422
+ afterSeq: this.cursors.get(topic) ?? afterSeq
6423
+ }));
6424
+ this.send(this.clientMessage("realtime.hello", {
6425
+ ticket,
6426
+ clientId: this.options.clientId,
6427
+ clientKind: this.options.clientKind,
6428
+ subscriptions,
6429
+ ...capabilities?.length ? { capabilities } : {},
6430
+ ...endpoint ? { endpoint } : {}
6431
+ }));
6432
+ };
6433
+ socket.onmessage = (event) => {
6434
+ if (generation !== this.generation) return;
6435
+ const process = () => {
6436
+ if (closed || generation !== this.generation) return;
6437
+ return this.handleServerMessage(frameText(event.data));
6438
+ };
6439
+ if (!messageQueue) {
6440
+ try {
6441
+ const pending = process();
6442
+ if (!pending) return;
6443
+ const current2 = pending.catch((error) => failAttempt(error instanceof Error ? error.message : "Invalid realtime server message")).then(() => void 0);
6444
+ messageQueue = current2;
6445
+ void current2.finally(() => {
6446
+ if (messageQueue === current2) messageQueue = void 0;
6447
+ });
6448
+ } catch (error) {
6449
+ failAttempt(error instanceof Error ? error.message : "Invalid realtime server message");
6450
+ }
6451
+ return;
6452
+ }
6453
+ const current = messageQueue.then(process).catch((error) => failAttempt(error instanceof Error ? error.message : "Invalid realtime server message")).then(() => void 0);
6454
+ messageQueue = current;
6455
+ void current.finally(() => {
6456
+ if (messageQueue === current) messageQueue = void 0;
6457
+ });
6458
+ };
6459
+ socket.onerror = () => {
6460
+ };
6461
+ socket.onclose = (event) => {
6462
+ failAttempt(
6463
+ event.reason || `WebSocket closed (${event.code ?? 0})`,
6464
+ !isNonRetryableCloseCode(event.code)
6465
+ );
6466
+ };
6467
+ } catch (error) {
6468
+ failAttempt(
6469
+ error instanceof Error ? error.message : "Realtime connection failed",
6470
+ !(error instanceof RealtimeConnectionError) || error.retryable
6471
+ );
6472
+ }
6473
+ }
6474
+ handleServerMessage(text) {
6475
+ const message = parseServerRealtimeMessage(JSON.parse(text));
6476
+ this.lastServerMessageAt = Date.now();
6477
+ if (message.kind === "realtime.ready") {
6478
+ const negotiated = message.payload.negotiatedCapabilities ?? [];
6479
+ if (negotiated.some((capability) => !this.offeredCapabilities.has(capability))) {
6480
+ throw new Error("Realtime server negotiated a capability the client did not offer");
6481
+ }
6482
+ this.ready = true;
6483
+ this.reconnectAttempts = 0;
6484
+ this.clearConnectionTimer();
6485
+ this.ticketAbort = void 0;
6486
+ this.options.onStateChange?.("connected");
6487
+ this.options.onCapabilities?.(negotiated);
6488
+ if (message.payload.endpoint) this.endpointBinding?.onReady(message.payload.endpoint);
6489
+ this.clearHeartbeat();
6490
+ this.heartbeatTimer = setInterval(() => {
6491
+ if (!this.ready) return;
6492
+ if (Date.now() - this.lastServerMessageAt >= message.payload.heartbeatTimeoutMs) {
6493
+ this.reconnect();
6494
+ return;
6495
+ }
6496
+ this.send(this.clientMessage("realtime.ping", {}));
6497
+ }, message.payload.heartbeatIntervalMs);
6498
+ } else if (message.kind === "realtime.event") {
6499
+ const cursor = this.cursors.get(message.payload.topic);
6500
+ if (cursor !== void 0 && message.payload.seq <= cursor) return;
6501
+ this.options.onEvent?.(message.payload);
6502
+ this.cursors.set(message.payload.topic, message.payload.seq);
6503
+ } else if (message.kind === "realtime.subscribed") {
6504
+ this.cursors.set(message.payload.topic, message.payload.cursor);
6505
+ this.options.onSubscribed?.(message.payload);
6506
+ } else if (message.kind === "realtime.gap") {
6507
+ this.cursors.set(message.payload.topic, message.payload.earliestSeq - 1);
6508
+ return this.options.onGap?.(message.payload);
6509
+ } else if (message.kind === "realtime.error") {
6510
+ this.options.onStateChange?.("error", message.payload.message);
6511
+ } else if (message.kind === "endpoint.message") {
6512
+ this.endpointBinding?.onMessage(message.payload);
6513
+ this.options.onEndpointMessage?.(message.payload);
6435
6514
  }
6436
6515
  }
6437
- if (!response || !gatewayUrl) {
6438
- throw lastRouteError instanceof Error ? lastRouteError : new Error(t("errorNoReachableGatewayRoute"));
6439
- }
6440
- if (typeof response.signedPayload !== "string" || typeof response.signature !== "string" || !await verifyGateway(gateway.gatewayPublicKey, response.signedPayload, response.signature)) throw new Error(t("errorGatewayVerification"));
6441
- const responseProof = decodeJson(response.signedPayload);
6442
- if (responseProof.purpose !== "device-refresh-v3" || responseProof.gatewayId !== gateway.gatewayId || responseProof.requestId !== requestId || responseProof.nonce !== nonce || !(responseProof.expiresAt > Date.now()) || responseProof.expiresAt > Date.now() + 6e4) throw new Error("Gateway refresh response does not match this request");
6443
- const payload = responseProof.tokens;
6444
- if (!payload?.accessToken || !payload.accessTokenExpiresAt || payload.refreshToken !== nextRefreshToken) {
6445
- throw new Error(t("errorInvalidCredentials"));
6446
- }
6447
- return { ...payload, gatewayUrl };
6448
- }
6449
- function wait(milliseconds) {
6450
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
6451
- }
6452
- async function pairGateway(invitation, onApproval, requestedOrigin) {
6453
- const payload = parseBrowserPairingInvitation(invitation, true);
6454
- const origins = [...new Set(payload.routes.map((route) => new URL(route.url).origin))];
6455
- const selectedOrigin = requestedOrigin && origins.includes(requestedOrigin) ? requestedOrigin : origins[0];
6456
- const permissionTask = requestGatewayOriginPermissions([selectedOrigin]);
6457
- if (!await permissionTask) throw new Error("Gateway site permission was not granted");
6458
- const permittedOrigins = (await Promise.all(origins.map(async (origin) => ({
6459
- origin,
6460
- permitted: await hasGatewayOriginPermission(origin)
6461
- })))).filter((candidate) => candidate.permitted).map((candidate) => candidate.origin);
6462
- const pair = await getOrCreateKeyPair();
6463
- const stored = await chrome.storage.local.get(PAIRING_JOURNAL_KEY);
6464
- let journal = stored[PAIRING_JOURNAL_KEY];
6465
- if (journal && journal.payload.pairingToken !== payload.pairingToken) {
6466
- throw new Error("PAIRING_ALREADY_PENDING");
6516
+ sendSubscriptions(subscriptions) {
6517
+ this.send(this.clientMessage("realtime.subscribe", { subscriptions }));
6467
6518
  }
6468
- if (!journal) {
6469
- if (payload.expiresAt <= Date.now()) throw new Error(t("errorExpiredPairingLink"));
6470
- journal = {
6471
- invitation: invitation.trim(),
6472
- payload,
6473
- requestId: crypto.randomUUID(),
6474
- idempotencyKey: crypto.randomUUID(),
6475
- initialRefreshToken: createRefreshToken()
6519
+ clientMessage(kind, payload) {
6520
+ const messageId = this.options.createMessageId?.() ?? globalThis.crypto?.randomUUID?.();
6521
+ if (!messageId) throw new Error("RealtimeClient requires createMessageId when crypto.randomUUID is unavailable");
6522
+ return {
6523
+ protocolVersion: REALTIME_PROTOCOL_VERSION,
6524
+ messageId,
6525
+ kind,
6526
+ sentAt: Date.now(),
6527
+ payload
6476
6528
  };
6477
- await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
6478
6529
  }
6479
- delete journal.nextOrigin;
6480
- await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
6481
- const requestId = journal.requestId;
6482
- const availableOrigins = journal.origin && permittedOrigins.includes(journal.origin) ? [journal.origin, ...permittedOrigins.filter((candidate) => candidate !== journal.origin)] : [selectedOrigin, ...permittedOrigins.filter((candidate) => candidate !== selectedOrigin)];
6483
- try {
6484
- let { origin, result } = await signedPairingRequestAcrossRoutes(
6485
- pair,
6486
- journal.payload,
6487
- availableOrigins,
6488
- requestId,
6489
- "request"
6490
- );
6491
- journal.origin = origin;
6492
- await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
6493
- if (result.request.status === "pending") onApproval(result.request.confirmationCode);
6494
- while (result.request.status === "pending") {
6495
- await wait(1500);
6496
- const response = await signedPairingRequestAcrossRoutes(
6497
- pair,
6498
- journal.payload,
6499
- [origin, ...permittedOrigins.filter((candidate) => candidate !== origin)],
6500
- requestId,
6501
- "status"
6502
- );
6503
- ({ origin, result } = response);
6504
- journal.origin = origin;
6505
- await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
6506
- }
6507
- if (result.request.status !== "approved" && result.request.status !== "completed") {
6508
- await chrome.storage.local.remove(PAIRING_JOURNAL_KEY);
6509
- throw new Error(t("errorPairingStatus", result.request.status));
6510
- }
6511
- if (result.request.status !== "completed") {
6512
- const response = await signedPairingRequestAcrossRoutes(
6513
- pair,
6514
- journal.payload,
6515
- [origin, ...permittedOrigins.filter((candidate) => candidate !== origin)],
6516
- requestId,
6517
- "complete",
6518
- {
6519
- idempotencyKey: journal.idempotencyKey,
6520
- initialRefreshToken: journal.initialRefreshToken
6521
- }
6522
- );
6523
- ({ origin, result } = response);
6530
+ send(message) {
6531
+ if (!this.socket || this.socket.readyState !== 1) return;
6532
+ const data = JSON.stringify(message);
6533
+ const bytes = new TextEncoder().encode(data).byteLength;
6534
+ if (bytes > REALTIME_MAX_CLIENT_FRAME_BYTES) {
6535
+ throw new RealtimeFrameTooLargeError(bytes, REALTIME_MAX_CLIENT_FRAME_BYTES);
6524
6536
  }
6525
- if (!result.request.deviceId) throw new Error(t("errorBrowserNotRegistered"));
6526
- journal.origin = origin;
6527
- journal.completed = { deviceId: result.request.deviceId, gatewayName: result.gateway.name };
6528
- journal.refresh ??= { requestId: crypto.randomUUID(), nextRefreshToken: createRefreshToken() };
6529
- await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
6530
- const orderedOrigins = [origin, ...permittedOrigins.filter((candidate) => candidate !== origin)];
6531
- const tokens = await refreshAccessToken(
6532
- orderedOrigins,
6533
- journal.initialRefreshToken,
6534
- pair,
6535
- journal.payload,
6536
- journal.refresh
6537
- );
6538
- const profile = {
6539
- gatewayId: journal.payload.gatewayId,
6540
- gatewayName: journal.completed.gatewayName,
6541
- gatewayUrl: tokens.gatewayUrl,
6542
- gatewayUrls: [tokens.gatewayUrl, ...orderedOrigins.filter((candidate) => candidate !== tokens.gatewayUrl)],
6543
- gatewayPublicKey: journal.payload.gatewayPublicKey,
6544
- deviceId: journal.completed.deviceId,
6545
- refreshToken: tokens.refreshToken,
6546
- accessToken: tokens.accessToken,
6547
- accessTokenExpiresAt: tokens.accessTokenExpiresAt
6548
- };
6549
- await chrome.storage.local.set({ [PROFILE_KEY]: profile });
6550
- await chrome.storage.local.remove(PAIRING_JOURNAL_KEY);
6551
- return profile;
6552
- } catch (cause) {
6553
- const nextOrigin = origins.find((origin) => !permittedOrigins.includes(origin));
6554
- if (nextOrigin && isRouteFailure(cause)) {
6555
- journal.nextOrigin = nextOrigin;
6556
- await chrome.storage.local.set({ [PAIRING_JOURNAL_KEY]: journal });
6557
- throw new Error(`PAIRING_ROUTE_PERMISSION_REQUIRED:${nextOrigin}`);
6537
+ this.socket.send(data);
6538
+ }
6539
+ scheduleReconnect(error) {
6540
+ if (!this.shouldReconnect) return;
6541
+ this.reconnectAttempts += 1;
6542
+ const max = this.options.maxReconnectAttempts;
6543
+ if (max !== void 0 && this.reconnectAttempts > max) {
6544
+ this.shouldReconnect = false;
6545
+ this.options.onStateChange?.("error", error);
6546
+ return;
6558
6547
  }
6559
- throw cause;
6548
+ this.options.onStateChange?.("reconnecting", error);
6549
+ const cap = Math.min(3e4, 1e3 * 2 ** Math.min(this.reconnectAttempts - 1, 5));
6550
+ const delay = Math.round(cap * (0.5 + Math.random() * 0.5));
6551
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
6552
+ this.reconnectTimer = setTimeout(() => {
6553
+ this.reconnectTimer = void 0;
6554
+ void this.open(true);
6555
+ }, delay);
6560
6556
  }
6561
- }
6562
- async function readPendingBrowserPairing() {
6563
- const stored = await chrome.storage.local.get(PAIRING_JOURNAL_KEY);
6564
- const journal = stored[PAIRING_JOURNAL_KEY];
6565
- return journal ? { invitation: journal.invitation, ...journal.nextOrigin ? { nextOrigin: journal.nextOrigin } : {} } : void 0;
6566
- }
6567
- async function cancelPendingBrowserPairing() {
6568
- const stored = await chrome.storage.local.get(PAIRING_JOURNAL_KEY);
6569
- const journal = stored[PAIRING_JOURNAL_KEY];
6570
- if (!journal) return;
6571
- const origins = [...new Set(journal.payload.routes.map((route) => new URL(route.url).origin))];
6572
- const permittedOrigins = (await Promise.all(origins.map(async (origin) => ({
6573
- origin,
6574
- permitted: await hasGatewayOriginPermission(origin)
6575
- })))).filter((candidate) => candidate.permitted).map((candidate) => candidate.origin);
6576
- try {
6577
- await signedPairingRequestAcrossRoutes(
6578
- await getOrCreateKeyPair(),
6579
- journal.payload,
6580
- journal.origin && permittedOrigins.includes(journal.origin) ? [journal.origin, ...permittedOrigins.filter((candidate) => candidate !== journal.origin)] : permittedOrigins,
6581
- journal.requestId,
6582
- "cancel"
6583
- );
6584
- } catch (cause) {
6585
- if (!(cause instanceof GatewayHttpError) || !["PAIRING_NOT_FOUND", "PAIRING_EXPIRED", "PAIRING_CANCELLED"].includes(cause.message)) throw cause;
6557
+ clearHeartbeat() {
6558
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
6559
+ this.heartbeatTimer = void 0;
6586
6560
  }
6587
- await chrome.storage.local.remove(PAIRING_JOURNAL_KEY);
6588
- }
6589
- async function readProfile() {
6590
- const stored = await chrome.storage.local.get([PROFILE_KEY, PAIRING_JOURNAL_KEY]);
6591
- const profile = stored[PROFILE_KEY];
6592
- if (!profile?.gatewayId || !profile.gatewayUrl || !profile.refreshToken) return void 0;
6593
- const pairing = stored[PAIRING_JOURNAL_KEY];
6594
- if (pairing?.payload?.gatewayId === profile.gatewayId && pairing.completed?.deviceId === profile.deviceId) {
6595
- await chrome.storage.local.remove(PAIRING_JOURNAL_KEY);
6561
+ clearTimers() {
6562
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
6563
+ this.reconnectTimer = void 0;
6564
+ this.clearConnectionTimer();
6565
+ this.ticketAbort?.abort();
6566
+ this.ticketAbort = void 0;
6567
+ this.clearHeartbeat();
6568
+ }
6569
+ clearConnectionTimer() {
6570
+ if (this.connectionTimer) clearTimeout(this.connectionTimer);
6571
+ this.connectionTimer = void 0;
6596
6572
  }
6597
- return profile;
6598
- }
6599
- function withRefreshLock(operation) {
6600
- const locks = globalThis.navigator?.locks;
6601
- if (!locks) return operation();
6602
- return new Promise((resolve, reject) => {
6603
- void locks.request(REFRESH_LOCK_NAME, () => operation().then(resolve, reject)).catch(reject);
6604
- });
6605
- }
6606
- function isRefreshDenied(cause) {
6607
- return cause instanceof GatewayHttpError && cause.status === 401 && cause.code === "REFRESH_DENIED";
6608
6573
  }
6609
- async function refreshJournalFor(profile) {
6610
- const stored = await chrome.storage.local.get(REFRESH_JOURNAL_KEY);
6611
- const journal = stored[REFRESH_JOURNAL_KEY];
6612
- if (journal?.gatewayId === profile.gatewayId && journal.refreshToken === profile.refreshToken) return journal;
6613
- const next = {
6614
- gatewayId: profile.gatewayId,
6615
- refreshToken: profile.refreshToken,
6616
- requestId: crypto.randomUUID(),
6617
- nextRefreshToken: createRefreshToken()
6574
+
6575
+ const PENDING_CONTEXT_KEY = "xopc.browser.pending-context";
6576
+ const TAB_BINDING_PREFIX = "xopc.browser.tab-binding.";
6577
+ async function buildPageContext(raw, pageUrl) {
6578
+ const url = sanitizedUrl(pageUrl);
6579
+ const selected = raw.selection ? truncateUtf8(raw.selection, MAX_BROWSER_SELECTION_BYTES) : void 0;
6580
+ const page = raw.text ? truncateUtf8(raw.text, MAX_BROWSER_PAGE_TEXT_BYTES) : void 0;
6581
+ const canonical = {
6582
+ title: raw.title.trim() || new URL(url).hostname,
6583
+ url,
6584
+ capturedAt: Date.now(),
6585
+ documentId: await sha256(`${url}
6586
+ ${raw.timeOrigin}`),
6587
+ selection: selected?.value,
6588
+ text: page?.value
6618
6589
  };
6619
- await chrome.storage.local.set({ [REFRESH_JOURNAL_KEY]: next });
6620
- return next;
6590
+ return {
6591
+ kind: "browser_page",
6592
+ sourceId: crypto.randomUUID(),
6593
+ version: await sha256(JSON.stringify(canonical)),
6594
+ ...canonical,
6595
+ truncated: selected?.truncated === true || page?.truncated === true
6596
+ };
6597
+ }
6598
+ function truncateUtf8(value, maxBytes) {
6599
+ const encoder = new TextEncoder();
6600
+ if (encoder.encode(value).byteLength <= maxBytes) return { value, truncated: false };
6601
+ let low = 0;
6602
+ let high = value.length;
6603
+ while (low < high) {
6604
+ const middle = Math.ceil((low + high) / 2);
6605
+ if (encoder.encode(value.slice(0, middle)).byteLength <= maxBytes) low = middle;
6606
+ else high = middle - 1;
6607
+ }
6608
+ return { value: value.slice(0, low), truncated: true };
6621
6609
  }
6622
- async function recoverLocalProfile(gatewayId) {
6623
- const local = await discoverLocalGateway();
6624
- if (!local) return void 0;
6610
+ function sanitizedUrl(raw) {
6611
+ let url;
6625
6612
  try {
6626
- if (parseBrowserPairingInvitation(local.invitation).gatewayId !== gatewayId) return void 0;
6627
- return await pairGateway(local.invitation, () => void 0, new URL(local.gatewayUrl).origin);
6613
+ url = new URL(raw);
6628
6614
  } catch {
6629
- return void 0;
6615
+ throw new Error(t("errorInvalidWebAddress"));
6616
+ }
6617
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
6618
+ throw new Error(t("errorRestrictedPage"));
6619
+ }
6620
+ if (url.hostname === "chromewebstore.google.com" || url.hostname === "chrome.google.com" && url.pathname.startsWith("/webstore")) {
6621
+ throw new Error(t("errorWebStorePage"));
6630
6622
  }
6623
+ url.username = "";
6624
+ url.password = "";
6625
+ url.hash = "";
6626
+ return url.toString();
6631
6627
  }
6632
- async function refreshStoredProfile(options) {
6633
- return withRefreshLock(async () => {
6634
- const profile = await readProfile();
6635
- if (!profile) throw new Error(t("errorBrowserNotPaired"));
6636
- if (options?.rejectedAccessToken && profile.accessToken !== options.rejectedAccessToken && profile.accessTokenExpiresAt > Date.now() + ACCESS_TOKEN_EXPIRY_MARGIN_MS) return profile;
6637
- if (!options?.rejectedAccessToken && profile.accessTokenExpiresAt > Date.now() + ACCESS_TOKEN_EXPIRY_MARGIN_MS) return profile;
6638
- const gatewayUrls = profile.gatewayUrls?.length ? profile.gatewayUrls : [profile.gatewayUrl];
6639
- const journal = await refreshJournalFor(profile);
6640
- try {
6641
- const tokens = await refreshAccessToken(
6642
- gatewayUrls,
6643
- profile.refreshToken,
6644
- await getOrCreateKeyPair(),
6645
- profile,
6646
- journal
6647
- );
6648
- const current = await readProfile();
6649
- assertProfileIdentity(current, profile);
6650
- if (current && current.gatewayId === profile.gatewayId && current.refreshToken !== profile.refreshToken) {
6651
- return current;
6652
- }
6653
- const next = {
6654
- ...profile,
6655
- ...tokens,
6656
- gatewayUrls: [tokens.gatewayUrl, ...gatewayUrls.filter((candidate) => candidate !== tokens.gatewayUrl)]
6657
- };
6658
- await chrome.storage.local.set({ [PROFILE_KEY]: next });
6659
- await chrome.storage.local.remove(REFRESH_JOURNAL_KEY);
6660
- return next;
6661
- } catch (cause) {
6662
- const current = await readProfile();
6663
- assertProfileIdentity(current, profile);
6664
- if (current && current.gatewayId === profile.gatewayId && current.refreshToken !== profile.refreshToken && current.accessTokenExpiresAt > Date.now()) return current;
6665
- if (!isRefreshDenied(cause)) throw cause;
6666
- const recovered = await recoverLocalProfile(profile.gatewayId);
6667
- if (recovered) {
6668
- await chrome.storage.local.remove(REFRESH_JOURNAL_KEY);
6669
- return recovered;
6670
- }
6671
- await chrome.storage.local.remove([PROFILE_KEY, REFRESH_JOURNAL_KEY]);
6672
- throw new Error(t("errorBrowserNotPaired"), { cause });
6673
- }
6674
- });
6628
+ function siteAccessError(cause, url) {
6629
+ const message = cause instanceof Error ? cause.message : String(cause);
6630
+ const hostname = new URL(url).hostname;
6631
+ if (/cannot access|cannot be scripted|missing host permission|extensions gallery|chrome web store/i.test(message)) {
6632
+ return new Error(t("errorSiteAccessBlocked", hostname));
6633
+ }
6634
+ return cause instanceof Error ? cause : new Error(message);
6675
6635
  }
6676
- async function getAccessProfile(options) {
6677
- const profile = await readProfile();
6678
- if (!profile) throw new Error(t("errorBrowserNotPaired"));
6679
- if (!options?.rejectedAccessToken && profile.accessTokenExpiresAt > Date.now() + ACCESS_TOKEN_EXPIRY_MARGIN_MS) return profile;
6680
- if (refreshTask) return refreshTask;
6681
- refreshTask = refreshStoredProfile(options);
6636
+ async function runWithTabSiteAccess(tabId, operation) {
6637
+ const tab = await chrome.tabs.get(tabId);
6638
+ if (tab.id === void 0 || tab.windowId === void 0 || !tab.url) {
6639
+ throw new Error(t("errorTabUnavailable"));
6640
+ }
6641
+ const url = sanitizedUrl(tab.url);
6682
6642
  try {
6683
- return await refreshTask;
6684
- } finally {
6685
- refreshTask = void 0;
6643
+ return await operation({ ...tab, id: tab.id, windowId: tab.windowId, url });
6644
+ } catch (cause) {
6645
+ throw siteAccessError(cause, url);
6686
6646
  }
6687
6647
  }
6688
- async function gatewayFetch(path, init = {}, expectedIdentity) {
6689
- let profile = await getAccessProfile();
6690
- const identity = expectedIdentity ?? profile;
6691
- assertProfileIdentity(profile, identity);
6692
- const request = () => {
6693
- const headers = new Headers(init.headers);
6694
- headers.set("Authorization", `Bearer ${profile.accessToken}`);
6695
- return fetch(`${profile.gatewayUrl}${path}`, { ...init, headers });
6696
- };
6697
- let response = await request();
6698
- assertProfileIdentity(await readProfile(), identity);
6699
- if (response.status !== 401) return response;
6700
- profile = await getAccessProfile({ rejectedAccessToken: profile.accessToken });
6701
- assertProfileIdentity(profile, identity);
6702
- response = await request();
6703
- assertProfileIdentity(await readProfile(), identity);
6704
- return response;
6648
+ async function sha256(value) {
6649
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
6650
+ return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
6705
6651
  }
6706
- async function registerBrowserEndpoint() {
6707
- const profile = await getAccessProfile();
6708
- const pair = await getOrCreateKeyPair();
6709
- const publicKey = encodeBase64Url(new Uint8Array(await crypto.subtle.exportKey("spki", pair.publicKey)));
6710
- const response = await gatewayFetch("/api/endpoint-tools/principals", {
6711
- method: "POST",
6712
- headers: { "Content-Type": "application/json" },
6713
- body: JSON.stringify({
6714
- principalId: profile.deviceId,
6715
- displayName: "xopc Chrome",
6716
- kind: "browser",
6717
- platform: "chrome",
6718
- publicKey
6719
- })
6652
+ function extractPage(mode) {
6653
+ const normalize = (value) => value.replace(/\s+/g, " ").trim();
6654
+ if (mode === "selection") {
6655
+ const focused = document.activeElement;
6656
+ const fieldSelection = focused instanceof HTMLTextAreaElement || focused instanceof HTMLInputElement && ["text", "search", "url", "email", "tel"].includes(focused.type) ? focused.value.slice(focused.selectionStart ?? 0, focused.selectionEnd ?? 0) : "";
6657
+ const selection = normalize(fieldSelection || window.getSelection()?.toString() || "");
6658
+ return { title: document.title, url: location.href, timeOrigin: performance.timeOrigin, selection };
6659
+ }
6660
+ const source = document.querySelector("article, main") ?? document.body;
6661
+ if (!source) return { title: document.title, url: location.href, timeOrigin: performance.timeOrigin };
6662
+ const clone = source.cloneNode(true);
6663
+ clone.querySelectorAll([
6664
+ "script",
6665
+ "style",
6666
+ "noscript",
6667
+ "template",
6668
+ "iframe",
6669
+ "svg",
6670
+ "canvas",
6671
+ "video",
6672
+ "audio",
6673
+ "nav",
6674
+ "aside",
6675
+ "header",
6676
+ "footer",
6677
+ "form",
6678
+ "[hidden]",
6679
+ '[aria-hidden="true"]',
6680
+ "[inert]"
6681
+ ].join(",")).forEach((element) => element.remove());
6682
+ const text = normalize(clone.innerText || clone.textContent || "");
6683
+ return { title: document.title, url: location.href, timeOrigin: performance.timeOrigin, text };
6684
+ }
6685
+ async function captureContextMenuSelection(tabId, selectionText, pageUrl) {
6686
+ const tab = await chrome.tabs.get(tabId);
6687
+ if (!tab.url) throw new Error(t("errorTabUnavailable"));
6688
+ const url = sanitizedUrl(tab.url);
6689
+ if (pageUrl && sanitizedUrl(pageUrl) !== url) throw new Error(t("errorPageChangedReading"));
6690
+ const selection = selectionText.replace(/\s+/g, " ").trim();
6691
+ if (!selection) throw new Error(t("errorSelectText"));
6692
+ return buildPageContext({ title: tab.title ?? "", timeOrigin: Date.now(), selection }, url);
6693
+ }
6694
+ async function captureTabPage(tabId, mode) {
6695
+ const tab = await chrome.tabs.get(tabId);
6696
+ if (!tab.url) throw new Error(t("errorTabUnavailable"));
6697
+ sanitizedUrl(tab.url);
6698
+ const result = await chrome.scripting.executeScript({
6699
+ target: { tabId, allFrames: true },
6700
+ func: extractPage,
6701
+ args: [mode]
6720
6702
  });
6721
- if (!response.ok) throw new Error(t("errorEndpointRegistration", String(response.status)));
6703
+ const main = result[0]?.result;
6704
+ if (!main) throw new Error(t("errorReadPage"));
6705
+ const currentTab = await chrome.tabs.get(tabId);
6706
+ if (!currentTab.url || sanitizedUrl(main.url) !== sanitizedUrl(tab.url) || sanitizedUrl(main.url) !== sanitizedUrl(currentTab.url)) {
6707
+ throw new Error(t("errorPageChangedReading"));
6708
+ }
6709
+ const content = result.map((item) => item.result).find((item) => mode === "selection" ? item?.selection : item?.text);
6710
+ if (!content?.selection && !content?.text) {
6711
+ throw new Error(t(mode === "selection" ? "errorSelectText" : "errorNoReadableContent"));
6712
+ }
6713
+ return buildPageContext({ ...main, selection: content.selection, text: content.text }, main.url);
6722
6714
  }
6723
- async function createBrowserEndpointHello(tools = []) {
6724
- const profile = await getAccessProfile();
6725
- const pair = await getOrCreateKeyPair();
6726
- const unsigned = {
6727
- principalId: profile.deviceId,
6728
- endpointId: `browser:${profile.deviceId}`,
6729
- connectionInstanceId: crypto.randomUUID(),
6730
- displayName: "xopc Chrome",
6731
- kind: "browser",
6732
- platform: "chrome",
6733
- appVersion: chrome.runtime.getManifest().version,
6734
- availability: "foreground",
6735
- nonce: crypto.randomUUID(),
6736
- signedAt: Date.now(),
6737
- signature: "pending",
6738
- tools
6739
- };
6740
- return {
6741
- ...unsigned,
6742
- signature: await sign(pair, endpointHelloSigningPayload(unsigned))
6743
- };
6715
+ async function captureTabWithPermission(tabId, mode) {
6716
+ return runWithTabSiteAccess(tabId, () => captureTabPage(tabId, mode));
6744
6717
  }
6745
- async function forgetProfile() {
6746
- await chrome.storage.local.remove([PROFILE_KEY, PAIRING_JOURNAL_KEY, REFRESH_JOURNAL_KEY]);
6718
+ async function activeTabId() {
6719
+ return (await chrome.tabs.query({ active: true, currentWindow: true }))[0]?.id;
6747
6720
  }
6748
- async function revokeAndForgetProfile() {
6749
- try {
6750
- const profile = await getAccessProfile();
6751
- await fetch(`${profile.gatewayUrl}/api/devices/me`, {
6752
- method: "DELETE",
6753
- headers: { Authorization: `Bearer ${profile.accessToken}` }
6721
+ async function currentTabDescriptor() {
6722
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
6723
+ if (tab.id === void 0 || tab.windowId === void 0 || !tab.url) throw new Error(t("errorNoActivePage"));
6724
+ return runWithTabSiteAccess(tab.id, async (accessibleTab) => {
6725
+ const result = await chrome.scripting.executeScript({
6726
+ target: { tabId: accessibleTab.id },
6727
+ func: () => ({ url: location.href, timeOrigin: performance.timeOrigin })
6754
6728
  });
6755
- } finally {
6756
- await forgetProfile();
6757
- try {
6758
- await clearBrowserChatState();
6759
- } finally {
6760
- try {
6761
- await forgetIdentity();
6762
- } finally {
6763
- await setAutoConnectEnabled(false);
6764
- }
6729
+ const marker = result[0]?.result;
6730
+ if (!marker) throw new Error(t("errorIdentifyPage"));
6731
+ const currentUrl = sanitizedUrl(marker.url);
6732
+ if (new URL(currentUrl).origin !== new URL(accessibleTab.url).origin) {
6733
+ throw new Error(t("errorPageChangedConnecting"));
6765
6734
  }
6766
- }
6735
+ return {
6736
+ tabId: String(accessibleTab.id),
6737
+ windowId: String(accessibleTab.windowId),
6738
+ documentId: await sha256(`${currentUrl}
6739
+ ${marker.timeOrigin}`),
6740
+ urlOrigin: new URL(currentUrl).origin
6741
+ };
6742
+ });
6767
6743
  }
6768
6744
 
6769
6745
  export { revokeAndForgetProfile as A, runWithTabSiteAccess as B, saveExtensionLocalePreference as C, setAutoConnectEnabled as D, ENDPOINT_PROTOCOL_VERSION as E, strictObject as F, string as G, t as H, MAX_BROWSER_CONTEXTS_PER_TURN as M, PENDING_CONTEXT_KEY as P, REALTIME_PROTOCOL_VERSION as R, TAB_BINDING_PREFIX as T, _enum as _, RealtimeClient as a, activeTabId as b, array as c, boolean as d, browserPageContextsInputSchema as e, cancelPendingBrowserPairing as f, canonicalJson as g, captureContextMenuSelection as h, captureTabWithPermission as i, createBrowserEndpointHello as j, currentTabDescriptor as k, discoverLocalGateway as l, discriminatedUnion as m, endpointToolContentSchema as n, extensionLocale as o, gatewayFetch as p, getAccessProfile as q, literal as r, loadExtensionLocalePreference as s, number as t, object as u, openDatabase as v, pairGateway as w, readPendingBrowserPairing as x, readProfile as y, registerBrowserEndpoint as z };