@xopcai/xopc 0.0.273 → 0.0.275

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/browser-ext/_locales/en/messages.json +215 -0
  2. package/dist/browser-ext/_locales/zh_CN/messages.json +215 -0
  3. package/dist/browser-ext/dist/assets/{auth-DkwOUI6_.js → auth-ClQTK565.js} +291 -115
  4. package/dist/browser-ext/dist/assets/{sidepanel-C86O7q5Y.css → sidepanel-CeGqObOa.css} +41 -20
  5. package/dist/browser-ext/dist/background.js +36 -4
  6. package/dist/browser-ext/dist/sidepanel.html +2 -2
  7. package/dist/browser-ext/dist/sidepanel.js +457 -310
  8. package/dist/browser-ext/manifest.json +6 -4
  9. package/dist/extensions/telegram/xopc.extension.json +1 -1
  10. package/dist/gateway/static/root/assets/{activity-store-cF7IX2RP.js → activity-store-SJZ5fS17.js} +1 -1
  11. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-BrgxX8nG.js → agent-avatar-dicebear-Dx6x7MXq.js} +1 -1
  12. package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-DvhuRCLA.js → agent-defaults-settings-panel-BuxTpjkf.js} +1 -1
  13. package/dist/gateway/static/root/assets/agents-_f9HEOol.js +1 -0
  14. package/dist/gateway/static/root/assets/{api-CYkdUpkx.js → api-CnJAVHBu.js} +1 -1
  15. package/dist/gateway/static/root/assets/{app-management-settings-panel-DfoNzWIa.js → app-management-settings-panel-BYKWXcVl.js} +1 -1
  16. package/dist/gateway/static/root/assets/{appearance-settings-czbLdCqj.js → appearance-settings-BmhSJc4X.js} +1 -1
  17. package/dist/gateway/static/root/assets/apps-page-DYTnK9lL.js +1 -0
  18. package/dist/gateway/static/root/assets/{archive-plugin-D14P13mX.js → archive-plugin-BY6ncIeE.js} +1 -1
  19. package/dist/gateway/static/root/assets/attachment-load-B6cw8KtO.js +1 -0
  20. package/dist/gateway/static/root/assets/{attachment-preview-renderer-BDTD8YUe.js → attachment-preview-renderer-CfEDFYPE.js} +1 -1
  21. package/dist/gateway/static/root/assets/attachment-process-heavy-C5CqLCsX.js +5 -0
  22. package/dist/gateway/static/root/assets/{automations-page-DM7ZEewI.js → automations-page-D1WwsSf3.js} +1 -1
  23. package/dist/gateway/static/root/assets/automations-workspace-CjF56MI_.js +6 -0
  24. package/dist/gateway/static/root/assets/{binary-plugins-CxNg6InF.js → binary-plugins-CC-wYm0K.js} +2 -2
  25. package/dist/gateway/static/root/assets/{block-editor-D22VcYnC.js → block-editor-D8rh9KNe.js} +3 -3
  26. package/dist/gateway/static/root/assets/{browser-automation-inputs-6NWTLEpd.js → browser-automation-inputs-DGCqIPXT.js} +1 -1
  27. package/dist/gateway/static/root/assets/{browser-automations-page-BrqySdbW.js → browser-automations-page-CMrffyct.js} +2 -2
  28. package/dist/gateway/static/root/assets/browser-settings-page-1onvP1Se.js +8 -0
  29. package/dist/gateway/static/root/assets/capabilities-settings-panel-C9ZSJxjr.js +8 -0
  30. package/dist/gateway/static/root/assets/{channels-settings-yHsY174W.js → channels-settings-CvLTnI99.js} +1 -1
  31. package/dist/gateway/static/root/assets/{channels-status-swr-BUZimNmp.js → channels-status-swr-BTT2sIGg.js} +1 -1
  32. package/dist/gateway/static/root/assets/{chat-terminal-dock-CZPBNpMI.js → chat-terminal-dock-F_dUdZ8l.js} +2 -2
  33. package/dist/gateway/static/root/assets/connectors-page-D7NHV8B2.js +2 -0
  34. package/dist/gateway/static/root/assets/{date-picker-Da_0QyLe.js → date-picker-MHuajwQ-.js} +1 -1
  35. package/dist/gateway/static/root/assets/{dependency-picker-D_djqzy7.js → dependency-picker-BcMJJI9X.js} +1 -1
  36. package/dist/gateway/static/root/assets/{desktop-pet-Celmbw-T.js → desktop-pet-DlNm_7H0.js} +1 -1
  37. package/dist/gateway/static/root/assets/{desktop-pet-settings-CqNNgF45.js → desktop-pet-settings-BI1JQMnE.js} +1 -1
  38. package/dist/gateway/static/root/assets/device-pairing-wizard-Cp5jO5B_.js +1 -0
  39. package/dist/gateway/static/root/assets/excel-worksheet-utils-DpdB-DjU.js +1 -0
  40. package/dist/gateway/static/root/assets/{extension-debug-page-ku3kCjvH.js → extension-debug-page-DZr0GkkA.js} +1 -1
  41. package/dist/gateway/static/root/assets/{extension-page-C7FpHwL4.js → extension-page-QsHoQ6VX.js} +1 -1
  42. package/dist/gateway/static/root/assets/{extension-settings-page-OMj2tU2_.js → extension-settings-page-I2YX4Tzb.js} +1 -1
  43. package/dist/gateway/static/root/assets/{gateway-config-swr-6eb1vKE1.js → gateway-config-swr-cd8owslz.js} +1 -1
  44. package/dist/gateway/static/root/assets/gateway-settings-GwfQu8rN.js +1 -0
  45. package/dist/gateway/static/root/assets/{gateway-startup-retry-DUFFCU8H.js → gateway-startup-retry-CVTa5HSS.js} +1 -1
  46. package/dist/gateway/static/root/assets/{heartbeat-settings-CSFzOACM.js → heartbeat-settings-D-hVYXzL.js} +1 -1
  47. package/dist/gateway/static/root/assets/home-page-SEW_0k8Q.js +1 -0
  48. package/dist/gateway/static/root/assets/{image-generation-api-C1cHRBRp.js → image-generation-api-DtMRG1oF.js} +1 -1
  49. package/dist/gateway/static/root/assets/{index-C6OWYFtQ.js → index-DfZZLrWp.js} +35 -35
  50. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BuvD3iv8.js → keyboard-shortcuts-settings-D_I-hiG2.js} +1 -1
  51. package/dist/gateway/static/root/assets/{local-app-workbench-page-CaC4-THD.js → local-app-workbench-page-BcY7wIHs.js} +2 -2
  52. package/dist/gateway/static/root/assets/{local-apps-page-DdJ8zwBp.js → local-apps-page-DniZuXJJ.js} +1 -1
  53. package/dist/gateway/static/root/assets/{locale-store-BBSCIl4I.js → locale-store-BkJ7g35d.js} +1 -1
  54. package/dist/gateway/static/root/assets/{logs-page-BaR2he0K.js → logs-page-DIiOMrJ_.js} +2 -2
  55. package/dist/gateway/static/root/assets/management-settings-BPw41YX6.js +1 -0
  56. package/dist/gateway/static/root/assets/{markdown-view-c5vtnYe8.js → markdown-view-B2IR4E2r.js} +1 -1
  57. package/dist/gateway/static/root/assets/{media-plugins-D_a6GDY6.js → media-plugins-BoP3LSQz.js} +1 -1
  58. package/dist/gateway/static/root/assets/{messages-DjuBdDCn.js → messages-BGc-OJWg.js} +2 -2
  59. package/dist/gateway/static/root/assets/note-detail-panel-BI2xr54R.js +5 -0
  60. package/dist/gateway/static/root/assets/notes-page-CM7Ns8i1.js +8 -0
  61. package/dist/gateway/static/root/assets/{page-tabs-C7Le9bSI.js → page-tabs-B21WfV92.js} +1 -1
  62. package/dist/gateway/static/root/assets/{preview-open-alternatives-B2pOxJBK.js → preview-open-alternatives-BZoDpPqV.js} +1 -1
  63. package/dist/gateway/static/root/assets/{proactive-card-BlQ1vCtV.js → proactive-card-Bw9CbzUX.js} +1 -1
  64. package/dist/gateway/static/root/assets/{proactive-page-CghKTfYa.js → proactive-page-Czw_CEDB.js} +1 -1
  65. package/dist/gateway/static/root/assets/{product-open-page-DpjiHgwu.js → product-open-page-DsQcgZkI.js} +1 -1
  66. package/dist/gateway/static/root/assets/project-detail-page-jOECabbf.js +1 -0
  67. package/dist/gateway/static/root/assets/projects-page-Ck9j5q0x.js +1 -0
  68. package/dist/gateway/static/root/assets/{registry-api-CyQ4k1Ux.js → registry-api-CTq8EECj.js} +1 -1
  69. package/dist/gateway/static/root/assets/remote-access-hub-2xcpaLTC.js +1 -0
  70. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-Bl4ArLHf.js → runtime-tools-settings-panel-Bz5gIRjy.js} +2 -2
  71. package/dist/gateway/static/root/assets/{schema-form-KQFmfWd4.js → schema-form-CqM0ntNm.js} +1 -1
  72. package/dist/gateway/static/root/assets/sessions-page-CE9MK60k.js +1 -0
  73. package/dist/gateway/static/root/assets/settings-advanced-gate-D0grhQan.js +1 -0
  74. package/dist/gateway/static/root/assets/{settings-form-section-Dbnls9ty.js → settings-form-section-LqcDYJ2m.js} +1 -1
  75. package/dist/gateway/static/root/assets/{settings-page-CDKirYRG.js → settings-page-BpvBmheO.js} +1 -1
  76. package/dist/gateway/static/root/assets/setup-status-panel-CYuqNzWH.js +1 -0
  77. package/dist/gateway/static/root/assets/share-preview-page-F3qhAryJ.js +2 -0
  78. package/dist/gateway/static/root/assets/shares-settings-B4wSoQM3.js +1 -0
  79. package/dist/gateway/static/root/assets/skill-reload-api-C3LD7fkR.js +1 -0
  80. package/dist/gateway/static/root/assets/{skills-page-Bd0_ED_p.js → skills-page-D9fI548d.js} +1 -1
  81. package/dist/gateway/static/root/assets/{skills-page.utils-rA-XT-YG.js → skills-page.utils-DZNmyYqU.js} +1 -1
  82. package/dist/gateway/static/root/assets/{system-settings-panel-CSkMrHLz.js → system-settings-panel-CMsmFOhV.js} +1 -1
  83. package/dist/gateway/static/root/assets/task-detail-page-DRwTJ8E0.js +1 -0
  84. package/dist/gateway/static/root/assets/{theme-store-DWpIEWen.js → theme-store-mV6Cm2vj.js} +1 -1
  85. package/dist/gateway/static/root/assets/tunnel-api-DGaqXwql.js +1 -0
  86. package/dist/gateway/static/root/assets/{use-autosave-DkQAIAuB.js → use-autosave-bXCl0Y0j.js} +1 -1
  87. package/dist/gateway/static/root/assets/user-model-page-B3RjDSri.js +2 -0
  88. package/dist/gateway/static/root/assets/{voice-api-key-field-BrqcH0P-.js → voice-api-key-field-Cx9rl7mP.js} +1 -1
  89. package/dist/gateway/static/root/assets/{voice-config-api-BJ95Z3pT.js → voice-config-api-D3DdcH4H.js} +1 -1
  90. package/dist/gateway/static/root/assets/{work-discovery-overlay-vfTWKm13.js → work-discovery-overlay-ywJpaqKp.js} +1 -1
  91. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-CHdr_ng9.js → workflow-run-setup-panel-DyGukxOL.js} +2 -2
  92. package/dist/gateway/static/root/assets/workflows-page-CifIqO2B.js +6 -0
  93. package/dist/gateway/static/root/index.html +9 -9
  94. package/dist/package.js +1 -1
  95. package/dist/packages/gateway-contract/src/device-pairing.js +41 -2
  96. package/dist/src/agent/child-agent-factory.d.ts +3 -0
  97. package/dist/src/agent/child-agent-factory.js +2 -1
  98. package/dist/src/agent/external-tools/endpoint-provider.js +1 -1
  99. package/dist/src/agent/service.js +20 -16
  100. package/dist/src/agent/tools/factory.d.ts +1 -0
  101. package/dist/src/agent/tools/factory.js +21 -4
  102. package/dist/src/agent/workflow/workflow-child-tools.js +2 -0
  103. package/dist/src/browser/native-messaging-host.d.ts +1 -1
  104. package/dist/src/browser/native-messaging-host.js +3 -2
  105. package/dist/src/browser/providers/browser-ext-install.d.ts +1 -1
  106. package/dist/src/browser/providers/browser-ext-install.js +8 -3
  107. package/dist/src/browser/providers/realtime-extension.d.ts +4 -1
  108. package/dist/src/browser/providers/realtime-extension.js +26 -10
  109. package/dist/src/browser/readiness.d.ts +4 -1
  110. package/dist/src/browser/readiness.js +11 -5
  111. package/dist/src/cli/commands/onboard.js +1 -1
  112. package/dist/src/gateway/gateway-workflow-host.types.d.ts +2 -0
  113. package/dist/src/gateway/heartbeat/service.js +1 -1
  114. package/dist/src/gateway/hono/app.js +5 -2
  115. package/dist/src/gateway/hono/routes/agent-stream.js +1 -1
  116. package/dist/src/gateway/hono/routes/browser.js +24 -11
  117. package/dist/src/gateway/hono/routes/device-pairing-approval.d.ts +9 -2
  118. package/dist/src/gateway/hono/routes/device-pairing-approval.js +50 -10
  119. package/dist/src/gateway/hono/routes/devices.d.ts +2 -1
  120. package/dist/src/gateway/hono/routes/devices.js +33 -16
  121. package/dist/src/gateway/hono/routes/endpoint-tools.js +9 -0
  122. package/dist/src/gateway/hono/routes/models.js +6 -0
  123. package/dist/src/gateway/hono/routes/session-input-handler.d.ts +1 -1
  124. package/dist/src/gateway/hono/routes/session-input-handler.js +1 -1
  125. package/dist/src/gateway/hono/routes/side-chats.js +1 -1
  126. package/dist/src/gateway/hono/routes/tunnel.js +34 -2
  127. package/dist/src/gateway/hono/routes/voice.js +1 -1
  128. package/dist/src/gateway/rate-limit/buckets.js +2 -1
  129. package/dist/src/gateway/service/run-gateway-agent.js +1 -1
  130. package/dist/src/heartbeat/index.js +1 -1
  131. package/dist/src/providers/model-catalog-persistence.js +4 -0
  132. package/dist/src/providers/model-catalog-types.d.ts +1 -0
  133. package/dist/src/providers/xopc-cloud-model-source.js +12 -1
  134. package/dist/src/storage/sqlite/device-pairing-approval.js +3 -1
  135. package/dist/src/storage/sqlite/endpoint-session-binding-repository.d.ts +1 -0
  136. package/dist/src/storage/sqlite/endpoint-session-binding-repository.js +9 -1
  137. package/dist/src/storage/sqlite/index.d.ts +1 -1
  138. package/dist/src/storage/sqlite/index.js +2 -2
  139. package/dist/src/tui/tui.js +1 -1
  140. package/dist/src/tunnel/xopc-cloud-registration.d.ts +5 -0
  141. package/dist/src/tunnel/xopc-cloud-registration.js +17 -11
  142. package/dist/src/workflows/service/workflow-run-service.js +4 -1
  143. package/package.json +2 -2
  144. package/dist/gateway/static/root/assets/agents-C0By2FIo.js +0 -1
  145. package/dist/gateway/static/root/assets/apps-page-BSJXKWcE.js +0 -1
  146. package/dist/gateway/static/root/assets/attachment-load-Dok_B1Gv.js +0 -2
  147. package/dist/gateway/static/root/assets/attachment-process-heavy-B2_kr5EA.js +0 -11
  148. package/dist/gateway/static/root/assets/automations-workspace--1TsZf3O.js +0 -6
  149. package/dist/gateway/static/root/assets/browser-settings-page-DOHgQer_.js +0 -8
  150. package/dist/gateway/static/root/assets/capabilities-settings-panel-Bpse_5K-.js +0 -8
  151. package/dist/gateway/static/root/assets/connectors-page-B_wLKGnH.js +0 -2
  152. package/dist/gateway/static/root/assets/device-pairing-wizard-B5_w9qlm.js +0 -1
  153. package/dist/gateway/static/root/assets/excel-worksheet-utils-EyvwNT4X.js +0 -1
  154. package/dist/gateway/static/root/assets/gateway-settings-CH-6vkgN.js +0 -1
  155. package/dist/gateway/static/root/assets/home-page-BJebAKB-.js +0 -1
  156. package/dist/gateway/static/root/assets/management-settings-r-iYWZFY.js +0 -1
  157. package/dist/gateway/static/root/assets/note-detail-panel-BV6nbgac.js +0 -5
  158. package/dist/gateway/static/root/assets/notes-page-BmLKoWwi.js +0 -8
  159. package/dist/gateway/static/root/assets/project-detail-page-CV8Lh296.js +0 -1
  160. package/dist/gateway/static/root/assets/projects-page-COweOaJP.js +0 -1
  161. package/dist/gateway/static/root/assets/remote-access-hub-ChPPGVU5.js +0 -1
  162. package/dist/gateway/static/root/assets/sessions-page-DgmHibOK.js +0 -1
  163. package/dist/gateway/static/root/assets/settings-advanced-gate-DV7kMco2.js +0 -1
  164. package/dist/gateway/static/root/assets/setup-status-panel-SQmtWoOs.js +0 -1
  165. package/dist/gateway/static/root/assets/share-preview-page-DwgLjS1m.js +0 -2
  166. package/dist/gateway/static/root/assets/shares-settings-Cs5bUrYw.js +0 -1
  167. package/dist/gateway/static/root/assets/skill-reload-api-DyKrjq7F.js +0 -1
  168. package/dist/gateway/static/root/assets/task-detail-page-CEQU-L_G.js +0 -1
  169. package/dist/gateway/static/root/assets/tunnel-api-COJcGnEi.js +0 -1
  170. package/dist/gateway/static/root/assets/user-model-page-BQAU55lf.js +0 -2
  171. package/dist/gateway/static/root/assets/workflows-page-hwRJD7vR.js +0 -6
@@ -24,6 +24,7 @@ import { runXopcEmbeddedTurn } from "./embedded/run-turn.js";
24
24
  import { resolveResponseLanguageForSession } from "./prompt/response-language.js";
25
25
  import { normalizeSkillInstallTarget } from "./skills/install-target.js";
26
26
  import { AgentManager } from "./agent-manager.js";
27
+ import { MAX_WEBCHAT_ATTACHMENT_FILE_BYTES } from "../gateway/chat-limits.js";
27
28
  import { maybeRefineSessionTitleWithLlm, maybeSetProvisionalSessionTitle } from "../session/session-title.js";
28
29
  import { resolveCompactionPolicy } from "./memory/compaction-policy.js";
29
30
  import { SessionStore } from "../session/store.js";
@@ -748,22 +749,25 @@ var AgentService = class {
748
749
  * Persist inbound file attachments to the global media store.
749
750
  */
750
751
  async prepareInboundAttachments(_sessionKey, attachments) {
751
- return persistInboundAttachments(attachments, { resolveUri: async (uri) => {
752
- const target = parseNoteAttachmentTarget(uri);
753
- if (!target) return null;
754
- const notesService = this.notesServiceInstance;
755
- if (!notesService) return null;
756
- const located = await notesService.getAttachmentPath(target.noteId, target.attachmentId);
757
- if (!located) return null;
758
- const buffer = await readFile(located.filePath);
759
- return {
760
- buffer,
761
- path: located.filePath,
762
- mimeType: located.mimeType,
763
- name: located.fileName,
764
- size: buffer.byteLength
765
- };
766
- } });
752
+ return persistInboundAttachments(attachments, {
753
+ maxBytes: MAX_WEBCHAT_ATTACHMENT_FILE_BYTES,
754
+ resolveUri: async (uri) => {
755
+ const target = parseNoteAttachmentTarget(uri);
756
+ if (!target) return null;
757
+ const notesService = this.notesServiceInstance;
758
+ if (!notesService) return null;
759
+ const located = await notesService.getAttachmentPath(target.noteId, target.attachmentId);
760
+ if (!located) return null;
761
+ const buffer = await readFile(located.filePath);
762
+ return {
763
+ buffer,
764
+ path: located.filePath,
765
+ mimeType: located.mimeType,
766
+ name: located.fileName,
767
+ size: buffer.byteLength
768
+ };
769
+ }
770
+ });
767
771
  }
768
772
  endDirectRequestContext() {
769
773
  this.contextMiddleware.onResponse();
@@ -105,6 +105,7 @@ export declare class AgentToolsFactory {
105
105
  constructor(deps: ToolFactoryDeps);
106
106
  private prepareRuntimeEnv;
107
107
  private browserReadinessKey;
108
+ private hasBrowserEndpoint;
108
109
  private checkBrowserReadinessCached;
109
110
  /** Invalidate the readiness cache (config hot-reload, settings-page save, etc.). */
110
111
  invalidateBrowserReadinessCache(): void;
@@ -8,6 +8,7 @@ import { getSessionTaskPlan, setSessionTaskPlan } from "../../storage/sqlite/ses
8
8
  import "../../storage/sqlite/index.js";
9
9
  import { init_agent_scope, resolveDefaultAgentId } from "../agent-scope.js";
10
10
  import { resolveEffectiveAgentConfigForSession } from "../../config/agent-profile.js";
11
+ import "../../../packages/browser-control-contract/src/index.js";
11
12
  import { getEmbeddedExecutionRunId, getEmbeddedExecutionSession } from "../embedded/execution-context.js";
12
13
  import { wrapToolsWithProtection } from "./executor.js";
13
14
  import { EXTERNAL_TOOL_NAMES } from "../external-tools/gateway-tools.js";
@@ -98,7 +99,13 @@ var AgentToolsFactory = class AgentToolsFactory {
98
99
  })).env;
99
100
  };
100
101
  browserReadinessKey() {
101
- return JSON.stringify(this.deps.getConfig?.()?.browser.driver ?? null);
102
+ return JSON.stringify({
103
+ driver: this.deps.getConfig?.()?.browser.driver ?? null,
104
+ extensionConnected: this.hasBrowserEndpoint()
105
+ });
106
+ }
107
+ hasBrowserEndpoint() {
108
+ return this.deps.endpointTools?.registry.list().some((endpoint) => endpoint.kind === "browser" && endpoint.tools.some((tool) => tool.descriptor.name === "browser.control")) ?? false;
102
109
  }
103
110
  async checkBrowserReadinessCached() {
104
111
  const key = this.browserReadinessKey();
@@ -106,7 +113,10 @@ var AgentToolsFactory = class AgentToolsFactory {
106
113
  const cached = this.browserReadinessCache;
107
114
  if (cached && cached.key === key && cached.expiresAt > now && cached.inflight === void 0) return cached.result ?? null;
108
115
  if (cached && cached.key === key && cached.inflight) return cached.inflight;
109
- const inflight = checkBrowserReadiness(this.deps.getConfig?.());
116
+ const inflight = checkBrowserReadiness(this.deps.getConfig?.(), {
117
+ extensionConnected: this.hasBrowserEndpoint(),
118
+ checkExtensionInstall: false
119
+ });
110
120
  this.browserReadinessCache = {
111
121
  key,
112
122
  expiresAt: now + 3e4,
@@ -146,9 +156,14 @@ var AgentToolsFactory = class AgentToolsFactory {
146
156
  emit: this.deps.emitBrowserEvent,
147
157
  resolveTarget: (sessionKey) => {
148
158
  const binding = getBrowserTabBinding(sessionKey);
149
- return binding ? {
159
+ if (binding) return {
150
160
  kind: "attached_tab",
151
161
  bindingId: binding.id
162
+ };
163
+ const endpointBinding = this.deps.endpointTools?.bindings.get(sessionKey);
164
+ return endpointBinding ? {
165
+ kind: "endpoint",
166
+ endpointId: endpointBinding.endpointId
152
167
  } : void 0;
153
168
  }
154
169
  });
@@ -401,7 +416,7 @@ var AgentToolsFactory = class AgentToolsFactory {
401
416
  })] : [],
402
417
  ...browserEnabled ? [createBrowserUseTool({
403
418
  getRuntime: () => this.ensureBrowserRuntime(),
404
- getTaskId: () => this.deps.getCurrentContext()?.sessionKey ?? "default",
419
+ getTaskId: () => options?.sessionKey ?? this.deps.getCurrentContext()?.sessionKey ?? "default",
405
420
  getReadiness: () => this.checkBrowserReadinessCached()
406
421
  })] : [],
407
422
  ...primary ? [createWorkflowTool({
@@ -428,11 +443,13 @@ var AgentToolsFactory = class AgentToolsFactory {
428
443
  getCurrentContext: () => null,
429
444
  getConfig: childOpts.getConfig,
430
445
  getPrimaryModel: () => childOpts.model,
446
+ endpointTools: childOpts.endpointTools ?? this.deps.endpointTools,
431
447
  toolExecutorConfig: childOpts.toolExecutorConfig
432
448
  }).createAllTools({
433
449
  workspace: childOpts.workspace,
434
450
  getPrimaryModel: () => childOpts.model,
435
451
  agentId: options?.agentId ?? childOpts.agentId,
452
+ sessionKey: childOpts.browserSessionKey,
436
453
  disabledTools: new Set([
437
454
  EXTERNAL_TOOL_NAMES.search,
438
455
  EXTERNAL_TOOL_NAMES.describe,
@@ -10,11 +10,13 @@ function buildWorkflowChildTools(childOptions) {
10
10
  getCurrentContext: () => null,
11
11
  getConfig: childOptions.getConfig,
12
12
  getPrimaryModel: () => childOptions.model,
13
+ endpointTools: childOptions.endpointTools,
13
14
  toolExecutorConfig: childOptions.toolExecutorConfig
14
15
  }).createAllTools({
15
16
  workspace: childOptions.workspace,
16
17
  getPrimaryModel: () => childOptions.model,
17
18
  agentId: childOptions.agentId,
19
+ sessionKey: childOptions.browserSessionKey,
18
20
  disabledTools: new Set([
19
21
  EXTERNAL_TOOL_NAMES.search,
20
22
  EXTERNAL_TOOL_NAMES.describe,
@@ -9,7 +9,7 @@ export type BrowserNativeBootstrap = {
9
9
  export type BrowserNativeBootstrapResult = {
10
10
  ok: true;
11
11
  gatewayUrl: string;
12
- pairingLink: string;
12
+ invitation: string;
13
13
  expiresAt: number;
14
14
  };
15
15
  export declare function encodeNativeMessage(value: unknown): Buffer;
@@ -1,4 +1,5 @@
1
1
  import { closeXopcDatabase, openXopcDatabase } from "../storage/sqlite/connection.js";
2
+ import { devicePairingInvitationPayloadSchema, formatBrowserPairingInvitation } from "../../packages/gateway-contract/src/device-pairing.js";
2
3
  import { getGatewayIdentityPublicKeyRaw, getOrCreateGatewayIdentity } from "../storage/sqlite/gateway-identity-repository.js";
3
4
  import { createDevicePairingSetup } from "../storage/sqlite/device-pairing-repository.js";
4
5
  import { init_loader, loadConfig } from "../config/loader.js";
@@ -49,7 +50,7 @@ function createBrowserNativeBootstrap(configPath, request) {
49
50
  return {
50
51
  ok: true,
51
52
  gatewayUrl,
52
- pairingLink: `https://link.xopc.ai/connect#p=${Buffer.from(JSON.stringify({
53
+ invitation: formatBrowserPairingInvitation(Buffer.from(JSON.stringify(devicePairingInvitationPayloadSchema.parse({
53
54
  version: 3,
54
55
  pairingToken: setup.token,
55
56
  gatewayId: identity.id,
@@ -58,7 +59,7 @@ function createBrowserNativeBootstrap(configPath, request) {
58
59
  routes: [route],
59
60
  targetKind: setup.targetKind,
60
61
  expiresAt: setup.expiresAt
61
- })).toString("base64url")}`,
62
+ }))).toString("base64url")),
62
63
  expiresAt: setup.expiresAt
63
64
  };
64
65
  } finally {
@@ -3,7 +3,7 @@
3
3
  * Fixed path — gateway upgrades overwrite in place so Chrome sideload paths stay stable.
4
4
  */
5
5
  import type { Config } from '../../config/schema.js';
6
- export declare const BROWSER_EXT_REQUIRED_FILES: readonly ["manifest.json", "dist/background.js", "dist/sidepanel.html"];
6
+ export declare const BROWSER_EXT_REQUIRED_FILES: readonly ["manifest.json", "dist/background.js", "dist/sidepanel.html", "_locales/en/messages.json", "_locales/zh_CN/messages.json"];
7
7
  export type BrowserExtBundledFrom = 'npm-dist' | 'git-dev' | 'electron-asar' | 'env-override';
8
8
  export interface BrowserExtInstallMeta {
9
9
  xopcVersion: string;
@@ -30,12 +30,15 @@ const STAGING_MAX_AGE_MS = 3600 * 1e3;
30
30
  const INSTALLED_ARTIFACT_NAMES = [
31
31
  "manifest.json",
32
32
  "dist",
33
- "icons"
33
+ "icons",
34
+ "_locales"
34
35
  ];
35
36
  const BROWSER_EXT_REQUIRED_FILES = [
36
37
  "manifest.json",
37
38
  "dist/background.js",
38
- "dist/sidepanel.html"
39
+ "dist/sidepanel.html",
40
+ "_locales/en/messages.json",
41
+ "_locales/zh_CN/messages.json"
39
42
  ];
40
43
  function moduleDir() {
41
44
  return dirname(fileURLToPath(import.meta.url));
@@ -53,7 +56,8 @@ function browserExtContentHash(dir) {
53
56
  const relativePaths = [
54
57
  "manifest.json",
55
58
  ...collectFiles("dist"),
56
- ...collectFiles("icons")
59
+ ...collectFiles("icons"),
60
+ ...collectFiles("_locales")
57
61
  ].sort();
58
62
  for (const relativePath of relativePaths) {
59
63
  hash.update(relativePath);
@@ -220,6 +224,7 @@ function copyBundledTree(src, dest) {
220
224
  mkdirSync(dest, { recursive: true });
221
225
  copyBundledFile(join(src, "manifest.json"), join(dest, "manifest.json"));
222
226
  copyBundledDirectory(join(src, "dist"), join(dest, "dist"));
227
+ copyBundledDirectory(join(src, "_locales"), join(dest, "_locales"));
223
228
  for (const icon of BROWSER_EXT_ICON_FILES) {
224
229
  const iconSrc = join(src, "icons", icon);
225
230
  if (existsSync(iconSrc)) copyBundledFile(iconSrc, join(dest, "icons", icon));
@@ -21,9 +21,12 @@ export declare class RealtimeExtensionBrowserProvider {
21
21
  expectedProtocolVersion: number;
22
22
  extensionVersion: string;
23
23
  principalId: string;
24
+ endpointCount: number;
25
+ selectionRequired: boolean;
24
26
  transport: "gateway-realtime";
25
27
  };
26
28
  shutdown(): Promise<void>;
27
29
  private endpointForInput;
28
- private currentEndpoint;
30
+ private defaultEndpoint;
31
+ private browserEndpoints;
29
32
  }
@@ -14,14 +14,18 @@ var RealtimeExtensionBrowserProvider = class {
14
14
  async start() {}
15
15
  async waitForConnection(timeoutMs = this.connectionTimeout) {
16
16
  const deadline = Date.now() + timeoutMs;
17
- while (!this.currentEndpoint()) {
17
+ while (this.browserEndpoints().length === 0) {
18
18
  if (Date.now() >= deadline) throw new Error(`Extension connection timeout after ${timeoutMs}ms. Open the xopc Chrome extension and connect it to this Gateway.`);
19
19
  await new Promise((resolve) => setTimeout(resolve, 200));
20
20
  }
21
21
  }
22
22
  async send(input, timeoutMs = this.commandTimeout, visualFallback = true) {
23
23
  const endpoint = this.endpointForInput(input);
24
- if (!endpoint) throw new Error("Extension not connected to this Gateway. Open the xopc Chrome extension and connect it first.");
24
+ if (!endpoint) {
25
+ if (input.target?.kind === "endpoint") throw new Error(`The Chrome endpoint bound to this Session is offline (${input.target.endpointId}). Reconnect that browser or bind another one.`);
26
+ if (this.browserEndpoints().length > 1) throw new Error("Multiple Chrome extensions are connected. Attach this Session to a tab or choose a browser device first.");
27
+ throw new Error("Extension not connected to this Gateway. Open the xopc Chrome extension and connect it first.");
28
+ }
25
29
  const tool = this.runtime.registry.getTool(endpoint.endpointId, BROWSER_CONTROL_ENDPOINT_TOOL_NAME);
26
30
  if (!tool) throw new Error("Connected browser extension does not support authenticated Realtime browser control. Reload or update the extension.");
27
31
  const id = crypto.randomUUID();
@@ -51,17 +55,20 @@ var RealtimeExtensionBrowserProvider = class {
51
55
  }
52
56
  }
53
57
  isConnected() {
54
- return Boolean(this.currentEndpoint());
58
+ return this.browserEndpoints().length > 0;
55
59
  }
56
60
  getConnectionStatus() {
57
- const endpoint = this.currentEndpoint();
61
+ const endpoints = this.browserEndpoints();
62
+ const endpoint = endpoints.length === 1 ? endpoints[0] : void 0;
58
63
  return {
59
- socketConnected: Boolean(endpoint),
60
- connected: Boolean(endpoint),
61
- protocolVersion: endpoint ? 4 : null,
64
+ socketConnected: endpoints.length > 0,
65
+ connected: endpoints.length > 0,
66
+ protocolVersion: endpoints.length > 0 ? 4 : null,
62
67
  expectedProtocolVersion: 4,
63
68
  extensionVersion: endpoint?.appVersion ?? null,
64
69
  principalId: endpoint?.principalId ?? null,
70
+ endpointCount: endpoints.length,
71
+ selectionRequired: endpoints.length > 1,
65
72
  transport: "gateway-realtime"
66
73
  };
67
74
  }
@@ -73,10 +80,19 @@ var RealtimeExtensionBrowserProvider = class {
73
80
  if (!binding || !endpoint || endpoint.principalId !== binding.principalId) return void 0;
74
81
  return endpoint;
75
82
  }
76
- return this.currentEndpoint();
83
+ if (input.target?.kind === "endpoint") {
84
+ const endpoint = this.runtime.registry.get(input.target.endpointId);
85
+ if (!endpoint || endpoint.kind !== "browser" || !endpoint.tools.some((tool) => tool.descriptor.name === "browser.control")) return;
86
+ return endpoint;
87
+ }
88
+ return this.defaultEndpoint();
89
+ }
90
+ defaultEndpoint() {
91
+ const endpoints = this.browserEndpoints();
92
+ return endpoints.length === 1 ? endpoints[0] : void 0;
77
93
  }
78
- currentEndpoint() {
79
- return this.runtime.registry.list().filter((endpoint) => endpoint.kind === "browser" && endpoint.tools.some((tool) => tool.descriptor.name === "browser.control")).sort((left, right) => right.lastHeartbeatAt - left.lastHeartbeatAt)[0];
94
+ browserEndpoints() {
95
+ return this.runtime.registry.list().filter((endpoint) => endpoint.kind === "browser" && endpoint.tools.some((tool) => tool.descriptor.name === "browser.control")).sort((left, right) => right.lastHeartbeatAt - left.lastHeartbeatAt);
80
96
  }
81
97
  };
82
98
  //#endregion
@@ -12,4 +12,7 @@ export declare class BrowserNotReadyError extends Error {
12
12
  constructor(hint: BrowserSetupHint);
13
13
  }
14
14
  export declare function buildBrowserSetupDeepLink(driver: BrowserDriverKind): string;
15
- export declare function checkBrowserReadiness(cfg: Config | undefined): Promise<BrowserNotReadyError | null>;
15
+ export declare function checkBrowserReadiness(cfg: Config | undefined, options?: {
16
+ extensionConnected?: boolean;
17
+ checkExtensionInstall?: boolean;
18
+ }): Promise<BrowserNotReadyError | null>;
@@ -14,17 +14,23 @@ var BrowserNotReadyError = class extends Error {
14
14
  function buildBrowserSetupDeepLink(driver) {
15
15
  return `/settings/agent-browser?driver=${driver}`;
16
16
  }
17
- async function checkBrowserReadiness(cfg) {
17
+ async function checkBrowserReadiness(cfg, options = {}) {
18
18
  if (!cfg?.browser.enabled) return null;
19
19
  const driver = cfg.browser.driver;
20
20
  let detail = null;
21
21
  let reason = null;
22
22
  try {
23
23
  if (driver.kind === "extension") {
24
- const { browserExtDoctor } = await import("./providers/browser-ext-install.js");
25
- if (!(await browserExtDoctor()).installed) {
26
- reason = "extension_not_installed";
27
- detail = "Chrome extension artifacts are not installed.";
24
+ if (options.extensionConnected) return null;
25
+ if (options.checkExtensionInstall === false) {
26
+ reason = "extension_not_connected";
27
+ detail = "No authenticated Chrome extension endpoint is connected.";
28
+ } else {
29
+ const { browserExtDoctor } = await import("./providers/browser-ext-install.js");
30
+ if (!(await browserExtDoctor()).installed) {
31
+ reason = "extension_not_installed";
32
+ detail = "Chrome extension artifacts are not installed.";
33
+ }
28
34
  }
29
35
  } else if (driver.kind === "playwright") if (driver.executablePath) await access(driver.executablePath);
30
36
  else {
@@ -4,8 +4,8 @@ import { saveConfig } from "../../config/loader.js";
4
4
  import { resolveGatewayLocalClientHost } from "../../config/gateway-bind.js";
5
5
  import { formatExamples, register } from "../registry.js";
6
6
  import "../../config/index.js";
7
- import { provisionEagerRuntimes } from "../../runtime-tools/bootstrap.js";
8
7
  import { colors } from "../utils/colors.js";
8
+ import { provisionEagerRuntimes } from "../../runtime-tools/bootstrap.js";
9
9
  import { initWorkspace } from "../utils/init-workspace.js";
10
10
  import { setupModel } from "./onboard/model.js";
11
11
  import { getChannelConfigurators } from "./onboard/channels/registry.js";
@@ -1,4 +1,5 @@
1
1
  import type { Config } from '../config/schema.js';
2
+ import type { EndpointToolRuntime } from '../endpoint-tools/index.js';
2
3
  import type { MessageBus } from '../infra/bus/index.js';
3
4
  import type { SessionStore } from '../session/store.js';
4
5
  export interface GatewayWorkflowAgentSurface {
@@ -10,6 +11,7 @@ export interface GatewayWorkflowHost {
10
11
  readonly currentWorkspacePath: string;
11
12
  readonly messageBusInstance: MessageBus;
12
13
  readonly agentService: GatewayWorkflowAgentSurface;
14
+ readonly endpointTools?: EndpointToolRuntime;
13
15
  emit(event: string, payload: unknown): void;
14
16
  readonly sessionIndexInstance: {
15
17
  getStore(): SessionStore;
@@ -1,9 +1,9 @@
1
1
  import { createLogger } from "../../utils/logger/index.js";
2
2
  import { init_logger } from "../../utils/logger.js";
3
3
  import { shouldSilence, stripHeartbeatToken } from "../../heartbeat/tokens.js";
4
- import { appendCronEventLines } from "../../heartbeat/event-prompt.js";
5
4
  import { isWithinActiveHours } from "../../heartbeat/active-hours.js";
6
5
  import { isHeartbeatContentEmpty } from "../../heartbeat/content-check.js";
6
+ import { appendCronEventLines } from "../../heartbeat/event-prompt.js";
7
7
  import { createHeartbeatWake } from "../../heartbeat/wake.js";
8
8
  import { resolveHeartbeatMdPath } from "../workspace-heartbeat-path.js";
9
9
  import { readFile } from "fs/promises";
@@ -5,8 +5,8 @@ import { resolveAllowedBrowserOrigins, resolveGatewayServiceListenPort } from ".
5
5
  import { resolveGatewayEffectiveHost } from "../../config/gateway-bind.js";
6
6
  import { loadTunnelState } from "../../tunnel/tunnel-state.js";
7
7
  import { resolveReverseProxyPublicUrl } from "../public-url.js";
8
- import { BROWSER_EXTENSION_ID } from "../../browser/extension-identity.js";
9
8
  import { maxSessionInputRequestBodyBytes } from "../chat-limits.js";
9
+ import { BROWSER_EXTENSION_ID } from "../../browser/extension-identity.js";
10
10
  import { isLoopbackIpAddress, isTrustedProxyAddress } from "../client-ip.js";
11
11
  import { buildGatewayConsoleCspHeader } from "../security/csp.js";
12
12
  import { checkBrowserOrigin, isChromeExtensionOrigin } from "../security/origin-check.js";
@@ -217,7 +217,10 @@ function createHonoApp(config) {
217
217
  });
218
218
  registerPublicGatewayRoutes(app, service);
219
219
  registerPublicHtmlPreviewRoute(app);
220
- registerDeviceAuthPublicRoutes(app);
220
+ registerDeviceAuthPublicRoutes(app, { getTrustedProxyContext: () => ({
221
+ trustedProxies: service.currentConfig.gateway?.trustedProxies,
222
+ allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true
223
+ }) });
221
224
  registerPublicExtensionAssetRoutes(app, service);
222
225
  registerPublicLocalAppPreviewRoutes(app, service);
223
226
  const authenticated = new Hono();
@@ -1,5 +1,5 @@
1
- import { parseTurnContextRefs } from "../../../agent/source-context/types.js";
2
1
  import { validateWebchatAttachments, validateWebchatContent } from "../../chat-limits.js";
2
+ import { parseTurnContextRefs } from "../../../agent/source-context/types.js";
3
3
  import { replaceLatestSessionTurn, submitSessionInput } from "./session-input-handler.js";
4
4
  //#region src/gateway/hono/routes/agent-stream.ts
5
5
  function registerAgentStreamRoutes(authenticated, deps) {
@@ -10,12 +10,12 @@ import { getGatewayPrincipal } from "../../security/gateway-principal.js";
10
10
  import { getConnInfo } from "@hono/node-server/conninfo";
11
11
  //#region src/gateway/hono/routes/browser.ts
12
12
  /** Browser Control status, approvals, and driver diagnostics. */
13
- async function extensionStatus(service) {
14
- const endpoint = service.endpointTools.registry.list().filter((item) => item.kind === "browser" && item.tools.some((tool) => tool.descriptor.name === "browser.control")).sort((left, right) => right.lastHeartbeatAt - left.lastHeartbeatAt)[0];
15
- const connected = Boolean(endpoint);
13
+ async function extensionStatus(service, includeLocalArtifacts) {
14
+ const endpoints = service.endpointTools.registry.list().filter((item) => item.kind === "browser" && item.tools.some((tool) => tool.descriptor.name === "browser.control")).sort((left, right) => right.lastHeartbeatAt - left.lastHeartbeatAt);
15
+ const endpoint = endpoints[0];
16
+ const connected = endpoints.length > 0;
16
17
  const extensionVersion = endpoint?.appVersion ?? null;
17
- const { browserExtDoctor } = await import("../../../browser/providers/browser-ext-install.js");
18
- const doctor = await browserExtDoctor({ runtimeExtensionVersion: extensionVersion ?? void 0 });
18
+ const doctor = includeLocalArtifacts ? await import("../../../browser/providers/browser-ext-install.js").then(({ browserExtDoctor }) => browserExtDoctor({ runtimeExtensionVersion: extensionVersion ?? void 0 })) : void 0;
19
19
  return {
20
20
  running: true,
21
21
  socketConnected: connected,
@@ -24,7 +24,15 @@ async function extensionStatus(service) {
24
24
  protocolVersion: connected ? 4 : null,
25
25
  expectedProtocolVersion: 4,
26
26
  extensionVersion,
27
- artifacts: {
27
+ endpoints: endpoints.map((item) => ({
28
+ endpointId: item.endpointId,
29
+ principalId: item.principalId,
30
+ displayName: item.displayName,
31
+ platform: item.platform,
32
+ appVersion: item.appVersion,
33
+ lastHeartbeatAt: item.lastHeartbeatAt
34
+ })),
35
+ artifacts: { ...doctor ? {
28
36
  installed: doctor.installed,
29
37
  bundledAvailable: doctor.bundledAvailable,
30
38
  extensionDir: doctor.extensionDir,
@@ -33,9 +41,9 @@ async function extensionStatus(service) {
33
41
  needsRefresh: doctor.needsRefresh,
34
42
  needsChromeReload: doctor.needsChromeReload,
35
43
  nativeHost: doctor.nativeHost
36
- },
44
+ } : {} },
37
45
  bridgeHeld: false,
38
- refCount: connected ? 1 : 0,
46
+ refCount: endpoints.length,
39
47
  transport: "gateway-realtime"
40
48
  };
41
49
  }
@@ -148,7 +156,7 @@ function registerBrowserRoutes(authenticated, deps) {
148
156
  });
149
157
  authenticated.get("/api/browser/extension-status", async (c) => {
150
158
  try {
151
- return c.json(await extensionStatus(service));
159
+ return c.json(await extensionStatus(service, isLocalOwnerRequest(c, service)));
152
160
  } catch (error) {
153
161
  return c.json({
154
162
  running: false,
@@ -160,6 +168,7 @@ function registerBrowserRoutes(authenticated, deps) {
160
168
  });
161
169
  authenticated.get("/api/browser/status", async (c) => {
162
170
  const config = service.currentConfig.browser;
171
+ const localManagementAvailable = isLocalOwnerRequest(c, service);
163
172
  if (!config.enabled) return c.json({
164
173
  ok: true,
165
174
  payload: {
@@ -168,15 +177,19 @@ function registerBrowserRoutes(authenticated, deps) {
168
177
  state: "disabled"
169
178
  }
170
179
  });
171
- const readiness = await checkBrowserReadiness(service.currentConfig);
172
- const driverStatus = config.driver.kind === "extension" ? await extensionStatus(service).catch((error) => ({ error: error instanceof Error ? error.message : String(error) })) : config.driver.kind === "playwright" ? await import("../../../browser/providers/playwright-doctor.js").then(({ playwrightChromiumDoctor }) => playwrightChromiumDoctor()) : void 0;
180
+ const driverStatus = config.driver.kind === "extension" ? await extensionStatus(service, localManagementAvailable).catch((error) => ({ error: error instanceof Error ? error.message : String(error) })) : config.driver.kind === "playwright" ? await import("../../../browser/providers/playwright-doctor.js").then(({ playwrightChromiumDoctor }) => playwrightChromiumDoctor()) : void 0;
173
181
  const extensionConnected = config.driver.kind === "extension" ? driverStatus.connected === true : true;
182
+ const readiness = await checkBrowserReadiness(service.currentConfig, {
183
+ extensionConnected,
184
+ checkExtensionInstall: localManagementAvailable
185
+ });
174
186
  const extensionProtocolDetail = config.driver.kind === "extension" && "socketConnected" in driverStatus && driverStatus.socketConnected && !driverStatus.connected ? `Chrome extension protocol ${driverStatus.protocolVersion ?? "unknown"} does not match ${driverStatus.expectedProtocolVersion ?? "the gateway"}. Reload the extension in Chrome.` : void 0;
175
187
  return c.json({
176
188
  ok: true,
177
189
  payload: {
178
190
  enabled: true,
179
191
  driverKind: config.driver.kind,
192
+ localManagementAvailable,
180
193
  state: !readiness && extensionConnected ? "ready" : "needs_attention",
181
194
  reason: readiness?.hint.reason ?? (extensionConnected ? void 0 : "extension_not_connected"),
182
195
  detail: readiness?.hint.detail ?? extensionProtocolDetail,
@@ -1,3 +1,10 @@
1
- import type { Hono } from 'hono';
2
- export declare function registerPairingApprovalPublicRoutes(app: Hono): void;
1
+ import type { Context, Hono } from 'hono';
2
+ export type PairingPublicRouteOptions = {
3
+ getTrustedProxyContext?: () => {
4
+ trustedProxies?: string[];
5
+ allowRealIpFallback?: boolean;
6
+ };
7
+ };
8
+ export declare function pairingClientKey(c: Context, options?: PairingPublicRouteOptions): string;
9
+ export declare function registerPairingApprovalPublicRoutes(app: Hono, options?: PairingPublicRouteOptions): void;
3
10
  export declare function registerPairingApprovalAdminRoutes(app: Hono): void;
@@ -1,9 +1,41 @@
1
1
  import { devicePairingCompleteSchema, devicePairingProofSchema, devicePairingRequestSchema } from "../../../../packages/gateway-contract/src/device-pairing.js";
2
2
  import { getOrCreateGatewayIdentity, signGatewayPayload } from "../../../storage/sqlite/gateway-identity-repository.js";
3
+ import { resolveClientIpFromRequest } from "../../client-ip.js";
4
+ import { buckets } from "../../rate-limit/buckets.js";
5
+ import "../../rate-limit/index.js";
3
6
  import { DevicePairingError, cancelDevicePairingSetup, decideDevicePairingRequest, operateDevicePairingRequest, readDevicePairingSetup, submitDevicePairingRequest } from "../../../storage/sqlite/device-pairing-approval.js";
4
7
  import os from "node:os";
5
8
  import { z } from "zod";
9
+ import { bodyLimit } from "hono/body-limit";
10
+ import { getConnInfo } from "@hono/node-server/conninfo";
6
11
  //#region src/gateway/hono/routes/device-pairing-approval.ts
12
+ function pairingClientKey(c, options) {
13
+ let remoteAddress;
14
+ try {
15
+ remoteAddress = getConnInfo(c).remote.address;
16
+ } catch {
17
+ remoteAddress = void 0;
18
+ }
19
+ const proxy = options?.getTrustedProxyContext?.();
20
+ return resolveClientIpFromRequest({
21
+ remoteAddress,
22
+ getHeader: (name) => c.req.header(name),
23
+ trustedProxies: proxy?.trustedProxies,
24
+ allowRealIpFallback: proxy?.allowRealIpFallback
25
+ });
26
+ }
27
+ function pairingBlocked(c, key) {
28
+ const blocked = buckets.pairingExchange().check(key);
29
+ if (!blocked.blocked) return void 0;
30
+ c.header("Retry-After", String(blocked.retryAfterSec));
31
+ return c.json({
32
+ ok: false,
33
+ error: {
34
+ code: "PAIRING_RATE_LIMITED",
35
+ message: "Too many failed pairing attempts"
36
+ }
37
+ }, 429);
38
+ }
7
39
  function pairingError(c, error) {
8
40
  if (error instanceof DevicePairingError) return c.json({
9
41
  ok: false,
@@ -20,7 +52,7 @@ function pairingError(c, error) {
20
52
  }
21
53
  }, 500);
22
54
  }
23
- function registerPairingApprovalPublicRoutes(app) {
55
+ function registerPairingApprovalPublicRoutes(app, options) {
24
56
  for (const action of [
25
57
  "request",
26
58
  "status",
@@ -28,15 +60,21 @@ function registerPairingApprovalPublicRoutes(app) {
28
60
  "cancel"
29
61
  ]) {
30
62
  const path = action === "request" ? "/api/device-pairing/requests" : `/api/device-pairing/requests/:id/${action}`;
31
- app.post(path, async (c) => {
63
+ app.post(path, bodyLimit({ maxSize: 4 * 1024 }), async (c) => {
64
+ const clientKey = pairingClientKey(c, options);
65
+ const blocked = pairingBlocked(c, clientKey);
66
+ if (blocked) return blocked;
32
67
  const parsed = (action === "request" ? devicePairingRequestSchema : action === "complete" ? devicePairingCompleteSchema : devicePairingProofSchema).safeParse(await c.req.json().catch(() => null));
33
- if (!parsed.success || action !== "request" && parsed.data.requestId !== c.req.param("id")) return c.json({
34
- ok: false,
35
- error: {
36
- code: "BAD_REQUEST",
37
- message: "Invalid pairing request"
38
- }
39
- }, 400);
68
+ if (!parsed.success || action !== "request" && parsed.data.requestId !== c.req.param("id")) {
69
+ buckets.pairingExchange().fail(clientKey);
70
+ return c.json({
71
+ ok: false,
72
+ error: {
73
+ code: "BAD_REQUEST",
74
+ message: "Invalid pairing request"
75
+ }
76
+ }, 400);
77
+ }
40
78
  try {
41
79
  const result = action === "request" ? { request: submitDevicePairingRequest(parsed.data) } : operateDevicePairingRequest(action, parsed.data);
42
80
  const signedPayload = Buffer.from(JSON.stringify({
@@ -48,12 +86,14 @@ function registerPairingApprovalPublicRoutes(app) {
48
86
  nonce: parsed.data.nonce,
49
87
  issuedAt: Date.now()
50
88
  })).toString("base64url");
89
+ buckets.pairingExchange().succeed(clientKey);
51
90
  return c.json({
52
91
  ok: true,
53
92
  signedPayload,
54
93
  signature: signGatewayPayload(signedPayload)
55
94
  });
56
95
  } catch (error) {
96
+ buckets.pairingExchange().fail(clientKey);
57
97
  return pairingError(c, error);
58
98
  }
59
99
  });
@@ -97,4 +137,4 @@ function registerPairingApprovalAdminRoutes(app) {
97
137
  });
98
138
  }
99
139
  //#endregion
100
- export { registerPairingApprovalAdminRoutes, registerPairingApprovalPublicRoutes };
140
+ export { pairingClientKey, registerPairingApprovalAdminRoutes, registerPairingApprovalPublicRoutes };
@@ -1,4 +1,5 @@
1
1
  import type { Hono } from 'hono';
2
2
  import type { AuthenticatedRouteDeps } from './deps.js';
3
- export declare function registerDeviceAuthPublicRoutes(app: Hono): void;
3
+ import { type PairingPublicRouteOptions } from './device-pairing-approval.js';
4
+ export declare function registerDeviceAuthPublicRoutes(app: Hono, options?: PairingPublicRouteOptions): void;
4
5
  export declare function registerDeviceRoutes(authenticated: Hono, deps: AuthenticatedRouteDeps): void;