@xopcai/xopc 0.0.368 → 0.0.370
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.
- package/README.md +59 -174
- package/README.zh-CN.md +59 -174
- package/dist/.pnpm/@noble_hashes@1.8.0/node_modules/@noble/hashes/esm/_md.js +131 -0
- package/dist/.pnpm/@noble_hashes@1.8.0/node_modules/@noble/hashes/esm/_u64.js +30 -0
- package/dist/.pnpm/@noble_hashes@1.8.0/node_modules/@noble/hashes/esm/sha2.js +252 -0
- package/dist/.pnpm/@noble_hashes@1.8.0/node_modules/@noble/hashes/esm/sha256.js +16 -0
- package/dist/.pnpm/@noble_hashes@1.8.0/node_modules/@noble/hashes/esm/utils.js +66 -0
- package/dist/browser-ext/_locales/en/messages.json +30 -1
- package/dist/browser-ext/_locales/zh_CN/messages.json +30 -1
- package/dist/browser-ext/dist/assets/{auth-beGtbLPS.js → page-context-BeworyNB.js} +1331 -1355
- package/dist/browser-ext/dist/assets/{sidepanel-BT0ojEzG.css → sidepanel-Ccl0eSa1.css} +43 -0
- package/dist/browser-ext/dist/background.js +680 -182
- package/dist/browser-ext/dist/sidepanel.html +2 -2
- package/dist/browser-ext/dist/sidepanel.js +393 -194
- package/dist/browser-ext/manifest.json +2 -8
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{activity-store-D9ZMh5ZR.js → activity-store-BQ3PMp2q.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-Bj3rvcTm.js → agent-avatar-dicebear-C61i1QbX.js} +1 -1
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-0fCR1DHx.js +1 -0
- package/dist/gateway/static/root/assets/agent-plugin-dialog-Di99mUTT.js +1 -0
- package/dist/gateway/static/root/assets/agents-BRZZmYH_.js +66 -0
- package/dist/gateway/static/root/assets/{agents-admin-api-B5QDepbW.js → agents-admin-api-HBZvVyzF.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-2rTEtB_X.js → app-management-settings-panel-noqqxkds.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-BPVBS80k.js → appearance-settings-DG8QN0gR.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-1FuKlZYP.js → apps-page-CgiyzFYo.js} +1 -1
- package/dist/gateway/static/root/assets/{archive-plugin-BbT1O7oK.js → archive-plugin-BO3I9zRP.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-DjwP_h8h.js → automations-page-CgAcRFDS.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-BIkxIpab.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-gS7Wg68v.js → binary-plugins-gFD7vH9R.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-DbwMaivP.js → block-editor-B1VE-jAe.js} +2 -2
- package/dist/gateway/static/root/assets/browser-automation-inputs-DxRoKFun.js +1 -0
- package/dist/gateway/static/root/assets/browser-automations-page-CIjyMS6f.js +3 -0
- package/dist/gateway/static/root/assets/browser-settings-page-BMAivBfp.js +3 -0
- package/dist/gateway/static/root/assets/{capabilities-Lswa0hv8.js → capabilities-CY4SiHcP.js} +1 -1
- package/dist/gateway/static/root/assets/{capabilities-Dd1PZRQw.js → capabilities-gjQtMhKu.js} +1 -1
- package/dist/gateway/static/root/assets/capabilities-page-DRdcfo68.js +2 -0
- package/dist/gateway/static/root/assets/{capabilities-settings-panel-DTHR1DrJ.js → capabilities-settings-panel-ZuL6pWjM.js} +4 -4
- package/dist/gateway/static/root/assets/{capability-header-actions-494dnJsj.js → capability-header-actions-D0GC1FEv.js} +1 -1
- package/dist/gateway/static/root/assets/channels-CgE22Pzd.js +1 -0
- package/dist/gateway/static/root/assets/{chat-terminal-dock-Bl7-Zk-x.js → chat-terminal-dock-CEjh1utz.js} +2 -2
- package/dist/gateway/static/root/assets/{computer-settings-page-ByExKr_6.js → computer-settings-page-vFHJAmQr.js} +1 -1
- package/dist/gateway/static/root/assets/{connector-service-page-BK_5aFxO.js → connector-service-page-DTrwZ4kV.js} +1 -1
- package/dist/gateway/static/root/assets/connectors-page-DRcMiTWu.js +1 -0
- package/dist/gateway/static/root/assets/copy-text-row-C1gaJwlZ.js +1 -0
- package/dist/gateway/static/root/assets/{date-picker-BXJv6472.js → date-picker-NfGIPSYY.js} +1 -1
- package/dist/gateway/static/root/assets/dependency-picker-CqguveUi.js +1 -0
- package/dist/gateway/static/root/assets/{desktop-pet-DWNEC3mk.js → desktop-pet-CTx1zkJm.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-BYwlxGiN.js → desktop-pet-settings-BRDS3EGc.js} +1 -1
- package/dist/gateway/static/root/assets/{device-pairing-wizard-BAc4F35b.js → device-pairing-wizard-Du4Ciuyw.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-D26tSY8Z.js → directory-picker-path-field-CjstlxFl.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-xYIGNegB.js → extension-debug-page-BfAtjhoK.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-BoQ1-3AW.js → extension-page-BJmudejr.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-provider-Cw_JU6RR.js → extension-provider-Br6LXyIj.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BH5TFGPe.js → extension-settings-page-UvmHniyz.js} +1 -1
- package/dist/gateway/static/root/assets/{fallback-models-04Wqmw14.js → fallback-models-euw9MGdt.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-swr-CNf0ZeRw.js → gateway-config-swr-7RKqgU_U.js} +1 -1
- package/dist/gateway/static/root/assets/gateway-realtime-F6D4A7N5.js +1 -0
- package/dist/gateway/static/root/assets/gateway-realtime-store-BheKaO6d.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-settings-BqvCJVUC.js → gateway-settings-C7roPHh1.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-startup-retry-DIxHacUw.js → gateway-startup-retry-jiV5sbEX.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-BGNCTtM3.js +2 -0
- package/dist/gateway/static/root/assets/{image-generation-api-CS0q0Kto.js → image-generation-api-DO4mXb6I.js} +1 -1
- package/dist/gateway/static/root/assets/{imports-page-n_rSLo6b.js → imports-page-DxCoT0VV.js} +1 -1
- package/dist/gateway/static/root/assets/index-BGBkIIzM.css +1 -0
- package/dist/gateway/static/root/assets/index-FCEE325-.js +106 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-tVkrHbYF.js → keyboard-shortcuts-settings-CnLln9Ds.js} +1 -1
- package/dist/gateway/static/root/assets/{local-app-workbench-page-L8tYuas0.js → local-app-workbench-page-yXRkSrEj.js} +1 -1
- package/dist/gateway/static/root/assets/{local-apps-page-SSJ9B1bH.js → local-apps-page-D46buzTk.js} +1 -1
- package/dist/gateway/static/root/assets/local-session-drafts-pox8evDV.js +1 -0
- package/dist/gateway/static/root/assets/{locale-store-D6_rF-qT.js → locale-store-COVscAqC.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-DmcCHnH0.js +2 -0
- package/dist/gateway/static/root/assets/management-settings-khN1ThNL.js +1 -0
- package/dist/gateway/static/root/assets/{markdown-view-_xwUuKce.js → markdown-view-B_qnAcW2.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-C631CDu4.js → media-plugins-C0Pc-wSY.js} +1 -1
- package/dist/gateway/static/root/assets/messages-Bb2GK-Nj.js +3 -0
- package/dist/gateway/static/root/assets/models-hub-cache-DyPPksMP.js +1 -0
- package/dist/gateway/static/root/assets/note-detail-panel-C_t58yha.js +5 -0
- package/dist/gateway/static/root/assets/{notes-api-vIZWQ9Yt.js → notes-api-sBgBcyLW.js} +1 -1
- package/dist/gateway/static/root/assets/notes-page-BJ0QS6xx.js +8 -0
- package/dist/gateway/static/root/assets/{page-context-capture-button-BlPDM7GM.js → page-context-capture-button-DfRODHv1.js} +1 -1
- package/dist/gateway/static/root/assets/personal-page-CiiXwwQE.js +1 -0
- package/dist/gateway/static/root/assets/{product-open-page-BJjWvoER.js → product-open-page-_Bp6HRmm.js} +1 -1
- package/dist/gateway/static/root/assets/{product-preview-content-CEd4E7Gf.js → product-preview-content-BUdiO0Ct.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-Clb-fXYk.js +1 -0
- package/dist/gateway/static/root/assets/{projects-page-lszV8HuS.js → projects-page-D9JEEqhh.js} +1 -1
- package/dist/gateway/static/root/assets/{registry-api-uJnjqTcn.js → registry-api-Bh9UBb-f.js} +1 -1
- package/dist/gateway/static/root/assets/remote-access-hub-CmyTq3_l.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-CjnNQYW8.js → runtime-tools-settings-panel-B1_inj8L.js} +1 -1
- package/dist/gateway/static/root/assets/{scenes-page-B3T3dDOR.js → scenes-page-CUNAZN2_.js} +2 -2
- package/dist/gateway/static/root/assets/{schema-form-DT3c4Vd_.js → schema-form-CfT-9UVw.js} +1 -1
- package/dist/gateway/static/root/assets/{send-session-input-CICkGwn-.js → send-session-input-BW40VHiL.js} +1 -1
- package/dist/gateway/static/root/assets/{session-api-BcQodoAu.js → session-api-yjpEz4pP.js} +1 -1
- package/dist/gateway/static/root/assets/session-manager-BqlvrjPE.js +16 -0
- package/dist/gateway/static/root/assets/sessions-page-CXZKScvu.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-B2vKXmzg.js +1 -0
- package/dist/gateway/static/root/assets/{settings-page-D-PZjcss.js → settings-page-1h3vyueg.js} +1 -1
- package/dist/gateway/static/root/assets/setup-status-panel-Cqc7UlZy.js +1 -0
- package/dist/gateway/static/root/assets/{share-link-dialog-CWS8EkJS.js → share-link-dialog-HCHM8qnf.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-23NJAKiq.js → share-preview-page-Ce6FNWt1.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-api-iKhHbvBD.js → shares-api-c0dfWz6h.js} +1 -1
- package/dist/gateway/static/root/assets/shares-settings-Dh5jisCK.js +1 -0
- package/dist/gateway/static/root/assets/{skill-api-C-MWdQwl.js → skill-api-deDZeQS8.js} +1 -1
- package/dist/gateway/static/root/assets/{skill-reload-api-iy02aB13.js → skill-reload-api-Ge5XymHa.js} +1 -1
- package/dist/gateway/static/root/assets/skills-page-DZozl9U_.js +2 -0
- package/dist/gateway/static/root/assets/system-settings-panel-CtzyMxqq.js +1 -0
- package/dist/gateway/static/root/assets/{task-detail-page-B2SIEyhZ.js → task-detail-page-5f_V0h-p.js} +2 -2
- package/dist/gateway/static/root/assets/{task-expandable-content-D2LRR4BC.js → task-expandable-content-hYcgCEE3.js} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-BJfhz-Zq.js → theme-store-BOfCKgl6.js} +1 -1
- package/dist/gateway/static/root/assets/{tracing-settings-page-aHD56iaS.js → tracing-settings-page-DDWFXcST.js} +1 -1
- package/dist/gateway/static/root/assets/{tunnel-api-BCf8CfTC.js → tunnel-api-vMtlbJ-m.js} +1 -1
- package/dist/gateway/static/root/assets/{usage-page-B4XUkuJd.js → usage-page-B1-BCW8U.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-BnnTw9RE.js → use-autosave-CSckcjFx.js} +1 -1
- package/dist/gateway/static/root/assets/{use-composer-attachments-Itczg0y1.js → use-composer-attachments-DJya1hIL.js} +1 -1
- package/dist/gateway/static/root/assets/use-file-preview-expanded-C5ONO7sF.js +2 -0
- package/dist/gateway/static/root/assets/user-model-page-D4BSq9HJ.js +2 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-BphJku8x.js → voice-api-key-field-GEw5x2fU.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-config-api-B4u4coFj.js → voice-config-api-3yyVwXln.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-jgOEP1xw.js → work-discovery-overlay-CjiNYJV5.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-N69crzRu.js → workflow-run-setup-panel-DJOetXvm.js} +2 -2
- package/dist/gateway/static/root/assets/workflows-page-CcAPFh2R.js +6 -0
- package/dist/gateway/static/root/assets/{workspace-api-Lzv6GDO8.js → workspace-api-WhbMvdBE.js} +2 -2
- package/dist/gateway/static/root/index.html +32 -34
- package/dist/package.js +1 -1
- package/dist/packages/browser-control-contract/src/automation.js +213 -0
- package/dist/packages/browser-control-contract/src/index.js +43 -12
- package/dist/packages/endpoint-tools-client/src/index.js +189 -0
- package/dist/packages/endpoint-tools-protocol/src/bridge.js +105 -0
- package/dist/packages/endpoint-tools-protocol/src/index.js +93 -2
- package/dist/packages/endpoint-tools-protocol/src/location.js +109 -0
- package/dist/packages/gateway-contract/src/gateway-scopes.js +3 -1
- package/dist/packages/gateway-contract/src/notifications.js +1 -0
- package/dist/packages/gateway-contract/src/session-input-command.js +30 -5
- package/dist/packages/realtime-protocol/src/voice.js +9 -3
- package/dist/src/agent/agent-manager.js +22 -26
- package/dist/src/agent/coding/repository-instructions.js +8 -1
- package/dist/src/agent/embedded/codemode-extension.d.ts +7 -0
- package/dist/src/agent/embedded/codemode-extension.js +232 -0
- package/dist/src/agent/embedded/codemode-output.d.ts +2 -0
- package/dist/src/agent/embedded/codemode-output.js +34 -0
- package/dist/src/agent/embedded/external-tool-discovery.d.ts +6 -0
- package/dist/src/agent/embedded/external-tool-discovery.js +60 -0
- package/dist/src/agent/embedded/model-runtime.js +5 -2
- package/dist/src/agent/embedded/run-for-session.js +10 -1
- package/dist/src/agent/embedded/run-turn.js +10 -3
- package/dist/src/agent/embedded/session-runner.d.ts +9 -1
- package/dist/src/agent/embedded/session-runner.js +111 -23
- package/dist/src/agent/embedded/session-tool-result-guard.d.ts +2 -0
- package/dist/src/agent/embedded/session-tool-result-guard.js +17 -0
- package/dist/src/agent/embedded/subscribe-session.js +14 -4
- package/dist/src/agent/embedded/tool-metadata.d.ts +15 -0
- package/dist/src/agent/embedded/tool-metadata.js +38 -0
- package/dist/src/agent/embedded/tool-search-extension.d.ts +5 -0
- package/dist/src/agent/embedded/tool-search-extension.js +31 -0
- package/dist/src/agent/embedded/types.d.ts +7 -0
- package/dist/src/agent/embedded/xopc-tools-bridge.js +6 -0
- package/dist/src/agent/external-tools/endpoint-provider.d.ts +1 -1
- package/dist/src/agent/external-tools/endpoint-provider.js +148 -56
- package/dist/src/agent/external-tools/extension-provider.js +8 -1
- package/dist/src/agent/external-tools/gateway-tools.d.ts +1 -1
- package/dist/src/agent/external-tools/gateway-tools.js +45 -22
- package/dist/src/agent/external-tools/index.js +46 -14
- package/dist/src/agent/external-tools/memory-provider.js +8 -1
- package/dist/src/agent/external-tools/read-policy.js +2 -1
- package/dist/src/agent/external-tools/service.d.ts +2 -0
- package/dist/src/agent/external-tools/service.js +25 -3
- package/dist/src/agent/external-tools/tool-registry.d.ts +14 -0
- package/dist/src/agent/external-tools/tool-registry.js +14 -0
- package/dist/src/agent/external-tools/types.d.ts +8 -1
- package/dist/src/agent/external-tools/types.js +0 -1
- package/dist/src/agent/mcp/config.d.ts +18 -0
- package/dist/src/agent/mcp/{bundle-mcp-config.js → config.js} +8 -25
- package/dist/src/agent/mcp/native-mcp.d.ts +6 -0
- package/dist/src/agent/mcp/native-mcp.js +73 -0
- package/dist/src/agent/service.js +2 -5
- package/dist/src/agent/skills/installer.js +1 -1
- package/dist/src/agent/skills/skill-manager.js +1 -1
- package/dist/src/agent/skills/skill-sources.js +1 -1
- package/dist/src/agent/source-context/index.d.ts +0 -1
- package/dist/src/agent/source-context/index.js +1 -2
- package/dist/src/agent/source-context/injector.js +1 -0
- package/dist/src/agent/source-context/types.d.ts +2 -2
- package/dist/src/agent/source-context/types.js +1 -1
- package/dist/src/agent/tool-manuals/browser.d.ts +1 -1
- package/dist/src/agent/tool-manuals/browser.js +3 -3
- package/dist/src/agent/tools/browser/tool/browser-use-tool.js +1 -1
- package/dist/src/agent/tools/browser/tool/schemas.d.ts +0 -13
- package/dist/src/agent/tools/browser/tool/schemas.js +0 -4
- package/dist/src/agent/tools/browser-automation-tool.d.ts +74 -1
- package/dist/src/agent/tools/browser-automation-tool.js +52 -6
- package/dist/src/agent/tools/cli-clarify.js +1 -1
- package/dist/src/agent/tools/codemode-permissions.d.ts +4 -0
- package/dist/src/agent/tools/codemode-permissions.js +23 -0
- package/dist/src/agent/tools/dataBatch.js +3 -1
- package/dist/src/agent/tools/delegation-parent-policy.js +1 -4
- package/dist/src/agent/tools/factory.js +4 -3
- package/dist/src/agent/transcript/provider-capabilities.js +0 -1
- package/dist/src/agent/transcript/transcript-policy.js +1 -2
- package/dist/src/agent-config/schema.d.ts +28 -0
- package/dist/src/agent-config/schema.js +7 -0
- package/dist/src/auth/credentials.js +7 -20
- package/dist/src/auth/model-credential-store.js +11 -4
- package/dist/src/auth/oauth/index.d.ts +1 -1
- package/dist/src/auth/oauth/index.js +3 -3
- package/dist/src/auth/oauth/openai.d.ts +2 -0
- package/dist/src/auth/oauth/openai.js +90 -0
- package/dist/src/auth/oauth/registry.js +3 -3
- package/dist/src/auth/oauth/types.d.ts +1 -0
- package/dist/src/auth/oauth/xopc-cloud.js +1 -1
- package/dist/src/auth/sync-provider-auth.js +5 -0
- package/dist/src/automations/capabilities/write.d.ts +2 -0
- package/dist/src/automations/domain/types.d.ts +11 -0
- package/dist/src/automations/domain/validation.d.ts +4 -0
- package/dist/src/automations/domain/validation.js +1 -0
- package/dist/src/automations/draft/automation-draft-validator.d.ts +1 -0
- package/dist/src/automations/service/action-executor.js +11 -3
- package/dist/src/automations/service/automation-service.d.ts +1 -0
- package/dist/src/automations/service/automation-service.js +13 -1
- package/dist/src/bootstrap/application-state.js +1 -1
- package/dist/src/browser/automations/repository.d.ts +4 -0
- package/dist/src/browser/automations/repository.js +27 -6
- package/dist/src/browser/automations/runner.js +101 -9
- package/dist/src/browser/automations/runtime-service.js +4 -3
- package/dist/src/browser/automations/schema.d.ts +84 -1
- package/dist/src/browser/automations/schema.js +68 -2
- package/dist/src/browser/automations/service.d.ts +78 -1
- package/dist/src/browser/automations/service.js +36 -4
- package/dist/src/browser/automations/types.d.ts +19 -4
- package/dist/src/browser/drivers/extension-driver.d.ts +5 -5
- package/dist/src/browser/drivers/extension-driver.js +10 -10
- package/dist/src/browser/drivers/playwright-driver.js +46 -0
- package/dist/src/browser/index.d.ts +1 -2
- package/dist/src/browser/index.js +2 -3
- package/dist/src/browser/policy/browser-policy.d.ts +0 -1
- package/dist/src/browser/policy/browser-policy.js +1 -4
- package/dist/src/browser/providers/index.d.ts +1 -1
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/providers/realtime-extension.d.ts +1 -1
- package/dist/src/browser/providers/realtime-extension.js +5 -1
- package/dist/src/browser/recordings/service.d.ts +22 -0
- package/dist/src/browser/recordings/service.js +160 -0
- package/dist/src/browser/runtime/browser-runtime.d.ts +1 -1
- package/dist/src/browser/runtime/browser-runtime.js +19 -56
- package/dist/src/chat-commands/builtins/model.js +2 -2
- package/dist/src/cli/command-manifest.js +1 -1
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/cli/commands/extension-agent-plugin.js +0 -19
- package/dist/src/cli/commands/mcp.js +36 -2
- package/dist/src/cli/commands/models.js +1 -1
- package/dist/src/cli/utils/oauth-login.js +2 -2
- package/dist/src/computer/runtime.js +2 -2
- package/dist/src/config/index.js +3 -3
- package/dist/src/config/mcp-config-normalize.d.ts +0 -3
- package/dist/src/config/mcp-config-normalize.js +2 -22
- package/dist/src/config/models-json.d.ts +0 -5
- package/dist/src/config/models-json.js +13 -4
- package/dist/src/config/paths.d.ts +0 -1
- package/dist/src/config/paths.js +1 -4
- package/dist/src/config/schema.d.ts +142 -48
- package/dist/src/config/schema.js +64 -47
- package/dist/src/connectors/auth-provider-registry.d.ts +1 -1
- package/dist/src/connectors/auth-provider-registry.js +1 -21
- package/dist/src/connectors/china-catalog.js +5 -5
- package/dist/src/connectors/connection-recovery-service.d.ts +1 -6
- package/dist/src/connectors/connection-recovery-service.js +3 -105
- package/dist/src/connectors/health.d.ts +1 -2
- package/dist/src/connectors/health.js +2 -101
- package/dist/src/connectors/types.d.ts +1 -1
- package/dist/src/endpoint-tools/binding-service.d.ts +2 -0
- package/dist/src/endpoint-tools/binding-service.js +14 -2
- package/dist/src/endpoint-tools/bridge-host.d.ts +15 -0
- package/dist/src/endpoint-tools/bridge-host.js +166 -0
- package/dist/src/endpoint-tools/bridge.d.ts +27 -0
- package/dist/src/endpoint-tools/bridge.js +88 -0
- package/dist/src/endpoint-tools/capability-catalog.d.ts +2 -0
- package/dist/src/endpoint-tools/capability-catalog.js +8 -0
- package/dist/src/endpoint-tools/grant-service.d.ts +69 -0
- package/dist/src/endpoint-tools/grant-service.js +113 -0
- package/dist/src/endpoint-tools/index.js +1 -1
- package/dist/src/endpoint-tools/invocation-service.js +6 -2
- package/dist/src/endpoint-tools/location-task.d.ts +22 -0
- package/dist/src/endpoint-tools/location-task.js +114 -0
- package/dist/src/endpoint-tools/policy.js +20 -4
- package/dist/src/endpoint-tools/registry.d.ts +2 -0
- package/dist/src/endpoint-tools/registry.js +9 -0
- package/dist/src/endpoint-tools/result-projector.d.ts +15 -0
- package/dist/src/endpoint-tools/result-projector.js +31 -0
- package/dist/src/endpoint-tools/runtime.d.ts +2 -0
- package/dist/src/endpoint-tools/runtime.js +15 -2
- package/dist/src/endpoint-tools/target-resolver.d.ts +12 -0
- package/dist/src/endpoint-tools/target-resolver.js +27 -0
- package/dist/src/endpoint-tools/turn-context.d.ts +6 -0
- package/dist/src/endpoint-tools/turn-context.js +29 -0
- package/dist/src/extensions/agent-plugins/auth.d.ts +0 -2
- package/dist/src/extensions/agent-plugins/auth.js +2 -19
- package/dist/src/extensions/agent-plugins/credentials.js +3 -19
- package/dist/src/extensions/agent-plugins/inventory.d.ts +2 -2
- package/dist/src/extensions/agent-plugins/mcp-adapter.js +1 -5
- package/dist/src/extensions/agent-plugins/source-http-fetch.d.ts +2 -0
- package/dist/src/{agent/mcp/plugin-http-fetch.js → extensions/agent-plugins/source-http-fetch.js} +7 -7
- package/dist/src/extensions/agent-plugins/sources.js +2 -2
- package/dist/src/extensions/agent-plugins/store.js +1 -3
- package/dist/src/extensions/agent-plugins/validation.d.ts +0 -4
- package/dist/src/extensions/agent-plugins/validation.js +13 -20
- package/dist/src/gateway/agents-admin.d.ts +5 -0
- package/dist/src/gateway/agents-admin.js +56 -1
- package/dist/src/gateway/chat-stream/mapper.js +9 -0
- package/dist/src/gateway/chat-stream/protocol.d.ts +4 -4
- package/dist/src/gateway/hono/lib/config-payload.d.ts +9 -4
- package/dist/src/gateway/hono/lib/config-payload.js +2 -5
- package/dist/src/gateway/hono/oauth-async.js +9 -2
- package/dist/src/gateway/hono/routes/agent-plugins.js +4 -9
- package/dist/src/gateway/hono/routes/agents.js +4 -3
- package/dist/src/gateway/hono/routes/browser-automations.js +23 -7
- package/dist/src/gateway/hono/routes/browser-recordings.d.ts +3 -0
- package/dist/src/gateway/hono/routes/browser-recordings.js +78 -0
- package/dist/src/gateway/hono/routes/browser.js +1 -34
- package/dist/src/gateway/hono/routes/connection-waits.js +2 -5
- package/dist/src/gateway/hono/routes/connectors.js +5 -60
- package/dist/src/gateway/hono/routes/device-authorizations.d.ts +3 -0
- package/dist/src/gateway/hono/routes/device-authorizations.js +74 -0
- package/dist/src/gateway/hono/routes/device-push.js +1 -0
- package/dist/src/gateway/hono/routes/endpoint-compatibility.js +7 -0
- package/dist/src/gateway/hono/routes/endpoint-tools.d.ts +1 -0
- package/dist/src/gateway/hono/routes/endpoint-tools.js +53 -3
- package/dist/src/gateway/hono/routes/home.js +1 -1
- package/dist/src/gateway/hono/routes/lazy-bundles.js +25 -1
- package/dist/src/gateway/hono/routes/mcp.d.ts +1 -1
- package/dist/src/gateway/hono/routes/mcp.js +11 -237
- package/dist/src/gateway/hono/routes/personal-agent.js +35 -0
- package/dist/src/gateway/hono/routes/session-command-handler.js +11 -1
- package/dist/src/gateway/hono/routes/session-input-handler.d.ts +8 -2
- package/dist/src/gateway/hono/routes/session-input-handler.js +25 -1
- package/dist/src/gateway/hono/routes/voice.js +5 -2
- package/dist/src/gateway/security/gateway-scopes.js +3 -1
- package/dist/src/gateway/service/agent-runner.js +1 -0
- package/dist/src/gateway/service/config-coordinator.js +2 -5
- package/dist/src/gateway/service/marketplace-service.js +2 -2
- package/dist/src/gateway/service/run-gateway-agent.js +13 -5
- package/dist/src/gateway/service/session-input-coordinator.d.ts +1 -1
- package/dist/src/gateway/service/session-input-coordinator.js +29 -9
- package/dist/src/gateway/service/sessions-api.js +0 -6
- package/dist/src/gateway/service.js +3 -12
- package/dist/src/gateway/session-reset-service.js +0 -6
- package/dist/src/home-intelligence/generator.d.ts +2 -0
- package/dist/src/home-intelligence/generator.js +53 -4
- package/dist/src/home-intelligence/host.d.ts +2 -1
- package/dist/src/home-intelligence/host.js +23 -3
- package/dist/src/imports/compatibility.js +12 -10
- package/dist/src/mcp/channel-bridge.js +1 -1
- package/dist/src/migrations/cloud-public-models.js +1 -0
- package/dist/src/notifications/clarification.d.ts +5 -0
- package/dist/src/notifications/clarification.js +66 -0
- package/dist/src/notifications/device-store.js +1 -0
- package/dist/src/notifications/harmony-push.d.ts +1 -0
- package/dist/src/notifications/harmony-push.js +3 -1
- package/dist/src/notifications/planner.d.ts +1 -0
- package/dist/src/notifications/service.d.ts +2 -2
- package/dist/src/notifications/service.js +20 -4
- package/dist/src/notifications/store.d.ts +2 -0
- package/dist/src/notifications/store.js +7 -1
- package/dist/src/notifications/types.d.ts +1 -0
- package/dist/src/notifications/types.js +1 -0
- package/dist/src/providers/env-keys.js +1 -1
- package/dist/src/providers/index.js +26 -24
- package/dist/src/providers/model-call.d.ts +0 -2
- package/dist/src/providers/model-call.js +2 -6
- package/dist/src/providers/model-registry.js +4 -2
- package/dist/src/providers/model-supplements.d.ts +3 -3
- package/dist/src/providers/model-supplements.js +6 -27
- package/dist/src/providers/presentation.js +3 -8
- package/dist/src/providers/prompt-cache-plan.js +1 -1
- package/dist/src/providers/provider-auth-service.d.ts +1 -1
- package/dist/src/providers/provider-auth-service.js +7 -2
- package/dist/src/providers/provider-policy.d.ts +3 -0
- package/dist/src/providers/provider-policy.js +14 -0
- package/dist/src/providers/register-runtime-providers.js +6 -1
- package/dist/src/realtime/runtime.js +1 -1
- package/dist/src/session/store.js +7 -1
- package/dist/src/session/transcript-events.d.ts +5 -0
- package/dist/src/session/transcript-events.js +1 -0
- package/dist/src/session/types.d.ts +2 -0
- package/dist/src/storage/sqlite/clarification-wait-repository.js +1 -0
- package/dist/src/storage/sqlite/device-grant-audit-repository.d.ts +11 -0
- package/dist/src/storage/sqlite/device-grant-audit-repository.js +10 -0
- package/dist/src/storage/sqlite/endpoint-device-settings-repository.d.ts +7 -0
- package/dist/src/storage/sqlite/endpoint-device-settings-repository.js +17 -0
- package/dist/src/storage/sqlite/migrations/238_browser_record_replay.sql +45 -0
- package/dist/src/storage/sqlite/migrations/239_clarification_notifications.sql +26 -0
- package/dist/src/storage/sqlite/migrations/240_device_settings.sql +5 -0
- package/dist/src/storage/sqlite/migrations/241_device_grant_audit.sql +13 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/storage/sqlite/session-input-repository.d.ts +1 -0
- package/dist/src/storage/sqlite/session-input-repository.js +5 -1
- package/dist/src/tasks/home-query-service.d.ts +1 -1
- package/dist/src/tasks/home-query-service.js +6 -2
- package/dist/src/tui/backends/gateway-realtime-backend.js +1 -1
- package/dist/src/tui/xopc-tui-keybindings.d.ts +4 -4
- package/dist/src/voice/dashscope/streaming-tts-stream.d.ts +2 -1
- package/dist/src/voice/dashscope/streaming-tts-stream.js +135 -79
- package/dist/src/voice/realtime/agentEngine.js +78 -1
- package/dist/src/voice/realtime/speakable-segmenter.d.ts +2 -0
- package/dist/src/voice/realtime/speakable-segmenter.js +30 -0
- package/dist/src/voice/tts/providers/alibaba-speech.js +17 -1
- package/dist/src/voice/tts/speak-core.d.ts +3 -0
- package/dist/src/voice/tts/speak-core.js +21 -1
- package/dist/src/voice/tts/speech-provider-types.d.ts +8 -0
- package/package.json +18 -7
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-D0tD3kCe.js +0 -1
- package/dist/gateway/static/root/assets/agent-plugin-dialog-BjdgKCnk.js +0 -1
- package/dist/gateway/static/root/assets/agents-CWXKzX5L.js +0 -66
- package/dist/gateway/static/root/assets/automations-workspace-C9ZT6TAU.js +0 -6
- package/dist/gateway/static/root/assets/browser-automation-inputs-CXG_JveC.js +0 -1
- package/dist/gateway/static/root/assets/browser-automations-page-CJc7OKR3.js +0 -3
- package/dist/gateway/static/root/assets/browser-settings-page-BjaTEUR8.js +0 -3
- package/dist/gateway/static/root/assets/capabilities-page-djBsC_tG.js +0 -2
- package/dist/gateway/static/root/assets/channels-BaYRJtz3.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-DyqRwSH2.js +0 -2
- package/dist/gateway/static/root/assets/copy-text-row-BHDs3cBa.js +0 -1
- package/dist/gateway/static/root/assets/dependency-picker-CWbbhKZa.js +0 -1
- package/dist/gateway/static/root/assets/dist-Che_jiQF.js +0 -1
- package/dist/gateway/static/root/assets/gateway-realtime-C4KtwMY3.js +0 -1
- package/dist/gateway/static/root/assets/gateway-realtime-store-ChZUzVbe.js +0 -1
- package/dist/gateway/static/root/assets/home-page-DhPPLbdD.js +0 -2
- package/dist/gateway/static/root/assets/index-CK4O2xMa.css +0 -1
- package/dist/gateway/static/root/assets/index-DpLD-7Hc.js +0 -106
- package/dist/gateway/static/root/assets/link-2-D_ZKYUW7.js +0 -1
- package/dist/gateway/static/root/assets/local-session-drafts-B_McurIj.js +0 -1
- package/dist/gateway/static/root/assets/logs-page-BWD63eg1.js +0 -2
- package/dist/gateway/static/root/assets/management-settings-aKSibklM.js +0 -1
- package/dist/gateway/static/root/assets/messages-Bnx3GMDB.js +0 -3
- package/dist/gateway/static/root/assets/models-hub-cache-Di83uIPw.js +0 -1
- package/dist/gateway/static/root/assets/note-detail-panel-B_fve3_2.js +0 -5
- package/dist/gateway/static/root/assets/notes-page-DnChyp8S.js +0 -8
- package/dist/gateway/static/root/assets/personal-page-BM2qAgg_.js +0 -1
- package/dist/gateway/static/root/assets/project-detail-page-DM4_VLNX.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-O6j9CWX0.js +0 -1
- package/dist/gateway/static/root/assets/session-manager-B0tOE4qz.js +0 -16
- package/dist/gateway/static/root/assets/sessions-page-BIixHXlQ.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-49SUEWPh.js +0 -1
- package/dist/gateway/static/root/assets/setup-status-panel-Ce3gTbT3.js +0 -1
- package/dist/gateway/static/root/assets/shares-settings-CNEZjF66.js +0 -1
- package/dist/gateway/static/root/assets/skills-page-BndNdYsJ.js +0 -2
- package/dist/gateway/static/root/assets/system-settings-panel-EpZeMlvp.js +0 -1
- package/dist/gateway/static/root/assets/use-file-preview-expanded-DJ5vyrvM.js +0 -2
- package/dist/gateway/static/root/assets/user-model-page-Bz5lkFG9.js +0 -2
- package/dist/gateway/static/root/assets/workflows-page-CPCgOfHh.js +0 -6
- package/dist/src/agent/embedded/system-prompt-override.d.ts +0 -2
- package/dist/src/agent/embedded/system-prompt-override.js +0 -21
- package/dist/src/agent/external-tools/mcp-provider.d.ts +0 -32
- package/dist/src/agent/external-tools/mcp-provider.js +0 -208
- package/dist/src/agent/mcp/bundle-mcp-config.d.ts +0 -20
- package/dist/src/agent/mcp/bundle-mcp-gateway.d.ts +0 -79
- package/dist/src/agent/mcp/bundle-mcp-gateway.js +0 -93
- package/dist/src/agent/mcp/bundle-mcp-names.d.ts +0 -1
- package/dist/src/agent/mcp/bundle-mcp-names.js +0 -25
- package/dist/src/agent/mcp/bundle-mcp-policy.d.ts +0 -1
- package/dist/src/agent/mcp/bundle-mcp-policy.js +0 -6
- package/dist/src/agent/mcp/bundle-mcp-runtime.d.ts +0 -46
- package/dist/src/agent/mcp/bundle-mcp-runtime.js +0 -571
- package/dist/src/agent/mcp/bundle-mcp-tools.d.ts +0 -2
- package/dist/src/agent/mcp/bundle-mcp-tools.js +0 -2
- package/dist/src/agent/mcp/bundle-mcp-types.d.ts +0 -76
- package/dist/src/agent/mcp/bundle-mcp-types.js +0 -1
- package/dist/src/agent/mcp/embedded-mcp.d.ts +0 -10
- package/dist/src/agent/mcp/embedded-mcp.js +0 -14
- package/dist/src/agent/mcp/index.d.ts +0 -5
- package/dist/src/agent/mcp/index.js +0 -12
- package/dist/src/agent/mcp/mcp-config-shared.d.ts +0 -8
- package/dist/src/agent/mcp/mcp-config-shared.js +0 -39
- package/dist/src/agent/mcp/mcp-http.d.ts +0 -23
- package/dist/src/agent/mcp/mcp-http.js +0 -67
- package/dist/src/agent/mcp/mcp-resource-id.d.ts +0 -6
- package/dist/src/agent/mcp/mcp-resource-id.js +0 -19
- package/dist/src/agent/mcp/mcp-stdio-transport.d.ts +0 -26
- package/dist/src/agent/mcp/mcp-stdio-transport.js +0 -120
- package/dist/src/agent/mcp/mcp-stdio.d.ts +0 -18
- package/dist/src/agent/mcp/mcp-stdio.js +0 -35
- package/dist/src/agent/mcp/mcp-transport-config.d.ts +0 -28
- package/dist/src/agent/mcp/mcp-transport-config.js +0 -91
- package/dist/src/agent/mcp/mcp-transport.d.ts +0 -17
- package/dist/src/agent/mcp/mcp-transport.js +0 -141
- package/dist/src/agent/mcp/oauth/index.d.ts +0 -6
- package/dist/src/agent/mcp/oauth/index.js +0 -6
- package/dist/src/agent/mcp/oauth/mcp-oauth-errors.d.ts +0 -5
- package/dist/src/agent/mcp/oauth/mcp-oauth-errors.js +0 -14
- package/dist/src/agent/mcp/oauth/mcp-oauth-manager.d.ts +0 -30
- package/dist/src/agent/mcp/oauth/mcp-oauth-manager.js +0 -276
- package/dist/src/agent/mcp/oauth/mcp-oauth-provider.d.ts +0 -33
- package/dist/src/agent/mcp/oauth/mcp-oauth-provider.js +0 -103
- package/dist/src/agent/mcp/oauth/mcp-oauth-session.d.ts +0 -31
- package/dist/src/agent/mcp/oauth/mcp-oauth-session.js +0 -181
- package/dist/src/agent/mcp/oauth/mcp-oauth-store.d.ts +0 -12
- package/dist/src/agent/mcp/oauth/mcp-oauth-store.js +0 -91
- package/dist/src/agent/mcp/oauth/mcp-oauth-types.d.ts +0 -22
- package/dist/src/agent/mcp/oauth/mcp-oauth-types.js +0 -1
- package/dist/src/agent/mcp/plugin-http-fetch.d.ts +0 -2
- package/dist/src/agent/source-context/mcp-resource-context.d.ts +0 -8
- package/dist/src/agent/source-context/mcp-resource-context.js +0 -30
- package/dist/src/auth/oauth/openai-codex.d.ts +0 -9
- package/dist/src/auth/oauth/openai-codex.js +0 -414
- package/dist/src/browser/policy/approval-store.d.ts +0 -17
- package/dist/src/browser/policy/approval-store.js +0 -62
- package/dist/src/browser/providers/extension-ws-acquire.d.ts +0 -19
- package/dist/src/browser/providers/extension-ws-acquire.js +0 -78
- package/dist/src/browser/providers/extension.d.ts +0 -61
- package/dist/src/browser/providers/extension.js +0 -378
- package/dist/src/connectors/plugin-mcp-recovery.d.ts +0 -28
- package/dist/src/connectors/plugin-mcp-recovery.js +0 -90
- package/dist/src/extensions/agent-plugins/connection.d.ts +0 -8
- package/dist/src/extensions/agent-plugins/connection.js +0 -41
- package/dist/src/extensions/agent-plugins/health.d.ts +0 -4
- package/dist/src/extensions/agent-plugins/health.js +0 -48
- package/dist/src/extensions/agent-plugins/plugin-oauth-scope.d.ts +0 -1
- package/dist/src/extensions/agent-plugins/plugin-oauth-scope.js +0 -8
|
@@ -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
|
|
5304
|
-
const
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
"
|
|
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
|
-
|
|
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
|
|
5891
|
-
|
|
5892
|
-
|
|
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
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
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
|
|
5901
|
-
await
|
|
5902
|
-
|
|
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
|
|
5905
|
-
const
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
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
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
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
|
|
5918
|
-
|
|
5919
|
-
|
|
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
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
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
|
-
|
|
5711
|
+
code;
|
|
5712
|
+
status;
|
|
5713
|
+
serverMessage;
|
|
5951
5714
|
}
|
|
5952
|
-
function
|
|
5953
|
-
|
|
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
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
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
|
-
|
|
5960
|
-
|
|
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
|
-
|
|
5963
|
-
|
|
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
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
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
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
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
|
-
|
|
5823
|
+
response = await post(origin, "/api/device-auth/refresh", body);
|
|
5824
|
+
gatewayUrl = origin;
|
|
5825
|
+
break;
|
|
5991
5826
|
} catch (cause) {
|
|
5992
|
-
|
|
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
|
-
|
|
5998
|
-
|
|
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
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6042
|
-
|
|
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
|
|
6051
|
-
const
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
const
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
const
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
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
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
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
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
}
|
|
6116
|
-
|
|
6117
|
-
const
|
|
6118
|
-
try {
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
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
|
|
6133
|
-
const
|
|
6134
|
-
|
|
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
|
|
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
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
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 (
|
|
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
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
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
|
|
6204
|
-
|
|
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
|
|
6207
|
-
return
|
|
5998
|
+
function isRefreshDenied(cause) {
|
|
5999
|
+
return cause instanceof GatewayHttpError && cause.status === 401 && cause.code === "REFRESH_DENIED";
|
|
6208
6000
|
}
|
|
6209
|
-
function
|
|
6210
|
-
|
|
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
|
|
6213
|
-
const
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
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
|
|
6221
|
-
return
|
|
6222
|
-
const
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
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
|
|
6229
|
-
const
|
|
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
|
|
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
|
-
|
|
6077
|
+
refreshTask = void 0;
|
|
6238
6078
|
}
|
|
6239
6079
|
}
|
|
6240
|
-
async function
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
await
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
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
|
|
6257
|
-
await
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
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
|
|
6265
|
-
const
|
|
6266
|
-
|
|
6267
|
-
|
|
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
|
|
6270
|
-
|
|
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
|
|
6140
|
+
async function revokeAndForgetProfile() {
|
|
6278
6141
|
try {
|
|
6279
|
-
const
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
{
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
);
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
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
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
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
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
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
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
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
|
-
|
|
6318
|
-
status;
|
|
6319
|
-
serverMessage;
|
|
6274
|
+
throw new Error("Realtime server sent a non-text frame");
|
|
6320
6275
|
}
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
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
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
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
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
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
|
-
|
|
6373
|
-
|
|
6343
|
+
setEndpoint(binding) {
|
|
6344
|
+
this.endpointBinding = binding;
|
|
6345
|
+
this.refreshEndpointConnection();
|
|
6374
6346
|
}
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
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
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
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
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
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
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
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
|
-
|
|
6438
|
-
|
|
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
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
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
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
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
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
const
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6563
|
-
|
|
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
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
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
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
const
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
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
|
-
|
|
6620
|
-
|
|
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
|
-
|
|
6623
|
-
|
|
6624
|
-
if (!local) return void 0;
|
|
6610
|
+
function sanitizedUrl(raw) {
|
|
6611
|
+
let url;
|
|
6625
6612
|
try {
|
|
6626
|
-
|
|
6627
|
-
return await pairGateway(local.invitation, () => void 0, new URL(local.gatewayUrl).origin);
|
|
6613
|
+
url = new URL(raw);
|
|
6628
6614
|
} catch {
|
|
6629
|
-
|
|
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
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
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
|
|
6677
|
-
const
|
|
6678
|
-
if (
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
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
|
|
6684
|
-
}
|
|
6685
|
-
|
|
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
|
|
6689
|
-
|
|
6690
|
-
|
|
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
|
-
|
|
6707
|
-
const
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
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
|
-
|
|
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
|
|
6724
|
-
|
|
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
|
|
6746
|
-
await chrome.
|
|
6718
|
+
async function activeTabId() {
|
|
6719
|
+
return (await chrome.tabs.query({ active: true, currentWindow: true }))[0]?.id;
|
|
6747
6720
|
}
|
|
6748
|
-
async function
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
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
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
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 };
|