@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +20 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -1,13 +1,13 @@
1
1
  // src/agent.ts
2
- import { readFile as readFile8, writeFile as writeFile5, mkdir as mkdir6, rename, unlink as unlink4, stat as stat7, realpath as realpath7 } from "node:fs/promises";
3
- import { join as join8, dirname, resolve as resolve3 } from "node:path";
4
- import { randomUUID as randomUUID6 } from "node:crypto";
2
+ import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir6, rename, unlink as unlink5, stat as stat7, realpath as realpath7 } from "node:fs/promises";
3
+ import { join as join8, dirname as dirname2, resolve as resolve3 } from "node:path";
4
+ import { randomUUID as randomUUID7 } from "node:crypto";
5
5
  import { spawn as spawn4, execFile as execFile2 } from "node:child_process";
6
6
  import { promisify as promisify2 } from "node:util";
7
7
 
8
8
  // src/protocol.ts
9
9
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
10
- var AGENT_VERSION = "0.1.1";
10
+ var AGENT_VERSION = "0.1.2";
11
11
  var AGENT_API_MAJOR = 1;
12
12
  var AgentError = class extends Error {
13
13
  constructor(code, status = 400) {
@@ -80,7 +80,1042 @@ var isMutating = (name) => CATALOG.find((x) => x.name === name)?.mutating ?? fal
80
80
  import { mkdirSync, openSync, readFileSync, writeFileSync, closeSync, unlinkSync, chmodSync, fstatSync, lstatSync } from "node:fs";
81
81
  import { join } from "node:path";
82
82
  import { createRequire } from "node:module";
83
- import { randomUUID } from "node:crypto";
83
+ import { randomUUID as randomUUID2 } from "node:crypto";
84
+
85
+ // src/secure-pairing.ts
86
+ import { randomInt, randomUUID, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
87
+
88
+ // ../modules/openmaus-protocol/dist/protocol.mjs
89
+ import { createCipheriv, createDecipheriv, createECDH, hkdfSync, randomBytes as randomBytes2 } from "node:crypto";
90
+ import { createHash as createHash2, createHmac, createPrivateKey, createPublicKey, sign, timingSafeEqual as timingSafeEqual2, verify } from "node:crypto";
91
+ import { Duplex } from "node:stream";
92
+ var B64U = /^[A-Za-z0-9_-]*$/;
93
+ var b64u = (bytes) => Buffer.from(bytes).toString("base64url");
94
+ function fromB64u(text) {
95
+ if (!B64U.test(text) || text.length % 4 === 1) return null;
96
+ const bytes = Buffer.from(text, "base64url");
97
+ return bytes.toString("base64url") === text ? bytes : null;
98
+ }
99
+ var sha256 = (bytes) => createHash2("sha256").update(bytes).digest();
100
+ var hmacSha256 = (key2, message) => createHmac("sha256", key2).update(message).digest();
101
+ var KEY_ID_BYTES = 16;
102
+ var PSK_LABEL = Buffer.from("omb-e2e psk v1", "utf8");
103
+ var ADMISSION_LABEL = Buffer.from("omb-relay admission v1", "utf8");
104
+ function credentialHashFromHex(hex) {
105
+ return /^[0-9a-f]{64}$/.test(hex) ? Buffer.from(hex, "hex") : null;
106
+ }
107
+ var keyId = (transportPublicKey) => sha256(transportPublicKey).subarray(0, KEY_ID_BYTES);
108
+ var psk = (credHash, transportKeyId) => hmacSha256(credHash, Buffer.concat([PSK_LABEL, transportKeyId]));
109
+ var RELAY_TOKEN_TTL_S = 12 * 60 * 60;
110
+ var PROTOCOL_NAME = "Noise_NKpsk0_P256_AESGCM_SHA256";
111
+ var PUBLIC_KEY_BYTES = 65;
112
+ var TAG_BYTES = 16;
113
+ var MAX_RECORD_BYTES = 65535;
114
+ var MAX_RECORD_PLAINTEXT = MAX_RECORD_BYTES - TAG_BYTES;
115
+ var MAX_HANDSHAKE_PAYLOAD = 512;
116
+ var MAX_HANDSHAKE_MESSAGE = PUBLIC_KEY_BYTES + MAX_HANDSHAKE_PAYLOAD + TAG_BYTES;
117
+ var CONN_BYTES = 16;
118
+ var MAX_APP_CHARS = 40;
119
+ var NONCE_LIMIT = 2 ** 32;
120
+ var HANDSHAKE_DEADLINE_MS = 1e4;
121
+ var TRANSPORT_DIRECT = 2;
122
+ var CLOSE = {
123
+ normal: 1e3,
124
+ goingAway: 1001,
125
+ protocol: 4e3,
126
+ rejected: 4001,
127
+ deadline: 4002
128
+ };
129
+ var NoiseError = class extends Error {
130
+ reason;
131
+ constructor(reason) {
132
+ super(`e2e: ${reason}`);
133
+ this.reason = reason;
134
+ }
135
+ };
136
+ function prologue(transport) {
137
+ return Buffer.concat([Buffer.from("omb-e2e/1", "utf8"), Buffer.from([0, transport])]);
138
+ }
139
+ var ecdh = (privateKey) => {
140
+ const curve = createECDH("prime256v1");
141
+ if (privateKey) curve.setPrivateKey(privateKey);
142
+ else curve.generateKeys();
143
+ return curve;
144
+ };
145
+ function keyPairFromPrivate(privateKey) {
146
+ if (privateKey.length !== 32) throw new NoiseError("bad_private_key");
147
+ return { privateKey: Buffer.from(privateKey), publicKey: ecdh(privateKey).getPublicKey(null, "uncompressed") };
148
+ }
149
+ function generateKeyPair() {
150
+ const curve = ecdh();
151
+ const raw = curve.getPrivateKey();
152
+ return { privateKey: Buffer.concat([Buffer.alloc(32 - raw.length), raw]), publicKey: curve.getPublicKey(null, "uncompressed") };
153
+ }
154
+ function dh(privateKey, publicKey) {
155
+ if (publicKey.length !== PUBLIC_KEY_BYTES || publicKey[0] !== 4) throw new NoiseError("bad_point");
156
+ try {
157
+ return ecdh(privateKey).computeSecret(publicKey);
158
+ } catch {
159
+ throw new NoiseError("bad_point");
160
+ }
161
+ }
162
+ var CHECK_SCALAR = Buffer.alloc(32, 1);
163
+ function isPublicKey(bytes) {
164
+ try {
165
+ dh(CHECK_SCALAR, bytes);
166
+ return true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ function hkdf(chainingKey, ikm, outputs) {
172
+ const out = Buffer.from(hkdfSync("sha256", ikm, chainingKey, Buffer.alloc(0), 32 * outputs));
173
+ return Array.from({ length: outputs }, (_, index) => out.subarray(index * 32, index * 32 + 32));
174
+ }
175
+ function nonceBytes(n) {
176
+ const nonce = Buffer.alloc(12);
177
+ nonce.writeBigUInt64BE(BigInt(n), 4);
178
+ return nonce;
179
+ }
180
+ function aeadSeal(key2, n, ad, plaintext) {
181
+ const cipher = createCipheriv("aes-256-gcm", key2, nonceBytes(n), { authTagLength: TAG_BYTES });
182
+ cipher.setAAD(ad);
183
+ return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
184
+ }
185
+ function aeadOpen(key2, n, ad, ciphertext) {
186
+ if (ciphertext.length < TAG_BYTES) throw new NoiseError("short_ciphertext");
187
+ const decipher = createDecipheriv("aes-256-gcm", key2, nonceBytes(n), { authTagLength: TAG_BYTES });
188
+ decipher.setAAD(ad);
189
+ decipher.setAuthTag(ciphertext.subarray(ciphertext.length - TAG_BYTES));
190
+ try {
191
+ return Buffer.concat([decipher.update(ciphertext.subarray(0, ciphertext.length - TAG_BYTES)), decipher.final()]);
192
+ } catch {
193
+ throw new NoiseError("aead");
194
+ }
195
+ }
196
+ var CipherState = class {
197
+ #key;
198
+ #n;
199
+ /** `n` other than 0 is for tests of the counter limit. */
200
+ constructor(key2 = null, n = 0) {
201
+ this.#key = key2;
202
+ this.#n = n;
203
+ }
204
+ get hasKey() {
205
+ return this.#key !== null;
206
+ }
207
+ get nonce() {
208
+ return this.#n;
209
+ }
210
+ /** Test-only view of the key, for the vectors. */
211
+ get keyBytes() {
212
+ return this.#key;
213
+ }
214
+ encryptWithAd(ad, plaintext) {
215
+ if (!this.#key) return Buffer.from(plaintext);
216
+ if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
217
+ const out = aeadSeal(this.#key, this.#n, ad, plaintext);
218
+ this.#n += 1;
219
+ return out;
220
+ }
221
+ /** The counter moves only on success; any failure ends the session anyway. */
222
+ decryptWithAd(ad, ciphertext) {
223
+ if (!this.#key) return Buffer.from(ciphertext);
224
+ if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
225
+ const out = aeadOpen(this.#key, this.#n, ad, ciphertext);
226
+ this.#n += 1;
227
+ return out;
228
+ }
229
+ };
230
+ var SymmetricState = class _SymmetricState {
231
+ ck;
232
+ h;
233
+ cipher = new CipherState();
234
+ trace;
235
+ constructor(h, ck, trace) {
236
+ this.h = h;
237
+ this.ck = ck;
238
+ this.trace = trace;
239
+ }
240
+ static initialize(trace = null) {
241
+ const name = Buffer.from(PROTOCOL_NAME, "utf8");
242
+ const h = name.length <= 32 ? Buffer.concat([name, Buffer.alloc(32 - name.length)]) : sha256(name);
243
+ return new _SymmetricState(h, Buffer.from(h), trace);
244
+ }
245
+ clone() {
246
+ const copy = new _SymmetricState(Buffer.from(this.h), Buffer.from(this.ck), this.trace ? [...this.trace] : null);
247
+ copy.cipher = this.cipher;
248
+ return copy;
249
+ }
250
+ mark(step) {
251
+ this.trace?.push({ step, h: this.h.toString("hex"), ck: this.ck.toString("hex"), k: this.cipher.keyBytes?.toString("hex") ?? null });
252
+ }
253
+ mixHash(data) {
254
+ this.h = sha256(Buffer.concat([this.h, data]));
255
+ }
256
+ mixKey(ikm) {
257
+ const [ck, key2] = hkdf(this.ck, ikm, 2);
258
+ this.ck = ck;
259
+ this.cipher = new CipherState(key2);
260
+ }
261
+ mixKeyAndHash(ikm) {
262
+ const [ck, tempH, key2] = hkdf(this.ck, ikm, 3);
263
+ this.ck = ck;
264
+ this.mixHash(tempH);
265
+ this.cipher = new CipherState(key2);
266
+ }
267
+ encryptAndHash(plaintext) {
268
+ const out = this.cipher.encryptWithAd(this.h, plaintext);
269
+ this.mixHash(out);
270
+ return out;
271
+ }
272
+ decryptAndHash(ciphertext) {
273
+ const out = this.cipher.decryptWithAd(this.h, ciphertext);
274
+ this.mixHash(ciphertext);
275
+ return out;
276
+ }
277
+ /** k_pc then k_cp. */
278
+ split() {
279
+ const [first, second] = hkdf(this.ck, Buffer.alloc(0), 2);
280
+ return [new CipherState(first), new CipherState(second)];
281
+ }
282
+ };
283
+ function initialized(transport, responderStatic, trace) {
284
+ const state = SymmetricState.initialize(trace);
285
+ state.mark("init");
286
+ state.mixHash(prologue(transport));
287
+ state.mark("prologue");
288
+ state.mixHash(responderStatic);
289
+ state.mark("s");
290
+ return state;
291
+ }
292
+ var RecordCipher = class {
293
+ #state;
294
+ constructor(state) {
295
+ this.#state = state;
296
+ }
297
+ get nonce() {
298
+ return this.#state.nonce;
299
+ }
300
+ seal(plaintext) {
301
+ if (plaintext.length > MAX_RECORD_PLAINTEXT) throw new NoiseError("record_too_large");
302
+ return this.#state.encryptWithAd(Buffer.alloc(0), plaintext);
303
+ }
304
+ open(record) {
305
+ if (record.length > MAX_RECORD_BYTES) throw new NoiseError("record_too_large");
306
+ return this.#state.decryptWithAd(Buffer.alloc(0), record);
307
+ }
308
+ };
309
+ function sessionKeys(state, side) {
310
+ const [pc, cp] = state.split();
311
+ const kPc = pc.keyBytes;
312
+ const kCp = cp.keyBytes;
313
+ return side === "phone" ? { send: new RecordCipher(pc), receive: new RecordCipher(cp), handshakeHash: state.h, kPc, kCp } : { send: new RecordCipher(cp), receive: new RecordCipher(pc), handshakeHash: state.h, kPc, kCp };
314
+ }
315
+ function encodePayload1(conn, app) {
316
+ return Buffer.from(JSON.stringify({ v: 1, conn: b64u(conn), app }), "utf8");
317
+ }
318
+ function encodePayload2(conn) {
319
+ return Buffer.from(JSON.stringify({ v: 1, conn }), "utf8");
320
+ }
321
+ function jsonObject(bytes) {
322
+ try {
323
+ const value = JSON.parse(Buffer.from(bytes).toString("utf8"));
324
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+ var isConn = (value) => typeof value === "string" && fromB64u(value)?.length === CONN_BYTES;
330
+ function parsePayload1(bytes) {
331
+ if (bytes.length > MAX_HANDSHAKE_PAYLOAD) return null;
332
+ const value = jsonObject(bytes);
333
+ if (!value || value.v !== 1 || !isConn(value.conn)) return null;
334
+ if (value.app !== void 0 && (typeof value.app !== "string" || value.app.length > MAX_APP_CHARS)) return null;
335
+ return { v: 1, conn: value.conn, ...typeof value.app === "string" ? { app: value.app } : {} };
336
+ }
337
+ function checkPayload2(bytes, conn) {
338
+ const value = bytes.length <= MAX_HANDSHAKE_PAYLOAD ? jsonObject(bytes) : null;
339
+ if (!value || value.v !== 1) return "version";
340
+ return value.conn === conn ? "ok" : "conn";
341
+ }
342
+ function phoneHello(options, payload1, conn) {
343
+ if (!isPublicKey(options.remoteStatic)) throw new NoiseError("bad_point");
344
+ if (options.psk.length !== 32) throw new NoiseError("bad_psk");
345
+ if (payload1.length > MAX_HANDSHAKE_PAYLOAD) throw new NoiseError("payload_too_large");
346
+ const state = initialized(options.transport, options.remoteStatic, options.trace ?? null);
347
+ state.mixKeyAndHash(options.psk);
348
+ state.mark("msg1.psk");
349
+ const e = options.ephemeral ?? generateKeyPair();
350
+ state.mixHash(e.publicKey);
351
+ state.mixKey(e.publicKey);
352
+ state.mark("msg1.e");
353
+ state.mixKey(dh(e.privateKey, options.remoteStatic));
354
+ state.mark("msg1.es");
355
+ const message1 = Buffer.concat([e.publicKey, state.encryptAndHash(payload1)]);
356
+ state.mark("msg1.payload");
357
+ let done = false;
358
+ return {
359
+ message1,
360
+ finish(message2) {
361
+ if (done) throw new NoiseError("finished");
362
+ done = true;
363
+ if (message2.length < PUBLIC_KEY_BYTES + TAG_BYTES || message2.length > MAX_HANDSHAKE_MESSAGE) throw new NoiseError("message2_size");
364
+ const re = message2.subarray(0, PUBLIC_KEY_BYTES);
365
+ const ee = dh(e.privateKey, re);
366
+ state.mixHash(re);
367
+ state.mixKey(re);
368
+ state.mark("msg2.e");
369
+ state.mixKey(ee);
370
+ state.mark("msg2.ee");
371
+ const payload2 = state.decryptAndHash(message2.subarray(PUBLIC_KEY_BYTES));
372
+ state.mark("msg2.payload");
373
+ const check = checkPayload2(payload2, conn);
374
+ if (check !== "ok") throw new NoiseError(`payload2_${check}`);
375
+ return sessionKeys(state, "phone");
376
+ }
377
+ };
378
+ }
379
+ function computerAccept(options, message1) {
380
+ if (message1.length < PUBLIC_KEY_BYTES + TAG_BYTES || message1.length > MAX_HANDSHAKE_MESSAGE) return null;
381
+ const re = message1.subarray(0, PUBLIC_KEY_BYTES);
382
+ let es;
383
+ try {
384
+ es = dh(options.staticKey.privateKey, re);
385
+ } catch {
386
+ return null;
387
+ }
388
+ const base = initialized(options.transport, options.staticKey.publicKey, options.trace ?? null);
389
+ let match = null;
390
+ for (const candidate of options.candidates) {
391
+ if (candidate.psk.length !== 32) continue;
392
+ const state2 = base.clone();
393
+ state2.mixKeyAndHash(candidate.psk);
394
+ state2.mark("msg1.psk");
395
+ state2.mixHash(re);
396
+ state2.mixKey(re);
397
+ state2.mark("msg1.e");
398
+ state2.mixKey(es);
399
+ state2.mark("msg1.es");
400
+ try {
401
+ const payload = state2.decryptAndHash(message1.subarray(PUBLIC_KEY_BYTES));
402
+ state2.mark("msg1.payload");
403
+ match ??= { id: candidate.id, state: state2, payload };
404
+ } catch {
405
+ }
406
+ }
407
+ if (!match) return null;
408
+ const payload1 = parsePayload1(match.payload);
409
+ if (!payload1) return null;
410
+ const { state } = match;
411
+ const e = options.ephemeral ?? generateKeyPair();
412
+ state.mixHash(e.publicKey);
413
+ state.mixKey(e.publicKey);
414
+ state.mark("msg2.e");
415
+ state.mixKey(dh(e.privateKey, re));
416
+ state.mark("msg2.ee");
417
+ const payload2 = options.payload2 ? options.payload2(payload1) : encodePayload2(payload1.conn);
418
+ const message2 = Buffer.concat([e.publicKey, state.encryptAndHash(payload2)]);
419
+ state.mark("msg2.payload");
420
+ if (options.trace && state.trace) options.trace.splice(0, options.trace.length, ...state.trace);
421
+ return { id: match.id, payload1, message2, keys: sessionKeys(state, "computer") };
422
+ }
423
+ var newConn = () => randomBytes2(CONN_BYTES);
424
+ var VERSION = 0;
425
+ var TYPE = { data: 0, windowUpdate: 1, ping: 2, goAway: 3 };
426
+ var FLAG = { syn: 1, ack: 2, fin: 4, rst: 8 };
427
+ var KNOWN_FLAGS = 15;
428
+ var GO_AWAY = { normal: 0, protocol: 1, internal: 2, removed: 3 };
429
+ var HEADER_BYTES = 12;
430
+ var MAX_DATA_PAYLOAD = 65507;
431
+ var INITIAL_WINDOW = 256 * 1024;
432
+ var STREAM_WINDOW = 2 * 1024 * 1024;
433
+ var OPEN_WINDOW_DELTA = STREAM_WINDOW - INITIAL_WINDOW;
434
+ var WINDOW_UPDATE_AFTER = 1024 * 1024;
435
+ var MAX_STREAMS = 64;
436
+ var MAX_STREAM_ID = 2 ** 31 - 1;
437
+ var PING_INTERVAL_MS = 2e4;
438
+ var PING_TIMEOUT_MS = 1e4;
439
+ var YamuxError = class extends Error {
440
+ reason;
441
+ constructor(reason) {
442
+ super(`yamux: ${reason}`);
443
+ this.reason = reason;
444
+ }
445
+ };
446
+ function encodeFrame(frame) {
447
+ const payload = frame.type === TYPE.data ? frame.data ?? new Uint8Array(0) : new Uint8Array(0);
448
+ if (frame.type === TYPE.data && payload.length !== frame.length) throw new YamuxError("data_length");
449
+ if (payload.length > MAX_DATA_PAYLOAD) throw new YamuxError("data_too_large");
450
+ const out = Buffer.alloc(HEADER_BYTES + payload.length);
451
+ out.writeUInt8(VERSION, 0);
452
+ out.writeUInt8(frame.type, 1);
453
+ out.writeUInt16BE(frame.flags, 2);
454
+ out.writeUInt32BE(frame.streamId, 4);
455
+ out.writeUInt32BE(frame.length, 8);
456
+ out.set(payload, HEADER_BYTES);
457
+ return out;
458
+ }
459
+ function decodeFrame(bytes) {
460
+ if (bytes.length < HEADER_BYTES) throw new YamuxError("short_frame");
461
+ const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
462
+ if (view.readUInt8(0) !== VERSION) throw new YamuxError("version");
463
+ const type = view.readUInt8(1);
464
+ if (type > TYPE.goAway) throw new YamuxError("type");
465
+ const flags = view.readUInt16BE(2);
466
+ if ((flags & ~KNOWN_FLAGS) !== 0) throw new YamuxError("flags");
467
+ const streamId = view.readUInt32BE(4);
468
+ const length = view.readUInt32BE(8);
469
+ if (type === TYPE.data) {
470
+ if (length > MAX_DATA_PAYLOAD) throw new YamuxError("data_too_large");
471
+ if (bytes.length !== HEADER_BYTES + length) throw new YamuxError("data_length");
472
+ if (streamId === 0) throw new YamuxError("stream_zero");
473
+ return { type, flags, streamId, length, data: view.subarray(HEADER_BYTES) };
474
+ }
475
+ if (bytes.length !== HEADER_BYTES) throw new YamuxError("trailing_bytes");
476
+ if (type === TYPE.windowUpdate) {
477
+ if (streamId === 0) throw new YamuxError("stream_zero");
478
+ } else {
479
+ if (streamId !== 0) throw new YamuxError("session_frame_stream");
480
+ if (type === TYPE.ping && flags !== FLAG.syn && flags !== FLAG.ack) throw new YamuxError("ping_flags");
481
+ if (type === TYPE.goAway && flags !== 0) throw new YamuxError("goaway_flags");
482
+ }
483
+ return { type, flags, streamId, length };
484
+ }
485
+ var openFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.syn, streamId, length: OPEN_WINDOW_DELTA });
486
+ var acceptFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.ack, streamId, length: OPEN_WINDOW_DELTA });
487
+ var resetFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.rst, streamId, length: 0 });
488
+ var finFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.fin, streamId, length: 0 });
489
+ var windowFrame = (streamId, delta) => ({ type: TYPE.windowUpdate, flags: 0, streamId, length: delta });
490
+ var dataFrame = (streamId, data, flags = 0) => ({ type: TYPE.data, flags, streamId, length: data.length, data });
491
+ var pingFrame = (value, flags = FLAG.syn) => ({ type: TYPE.ping, flags, streamId: 0, length: value >>> 0 });
492
+ var goAwayFrame = (code) => ({ type: TYPE.goAway, flags: 0, streamId: 0, length: code });
493
+ var Mux = class {
494
+ role;
495
+ #events;
496
+ #streams = /* @__PURE__ */ new Map();
497
+ /** The highest ID the phone has opened. */
498
+ #highestId = 0;
499
+ #peerGoneAway = false;
500
+ #localGoneAway = false;
501
+ constructor(role, events) {
502
+ this.role = role;
503
+ this.#events = events;
504
+ }
505
+ get activeStreams() {
506
+ return this.#streams.size;
507
+ }
508
+ get peerGoneAway() {
509
+ return this.#peerGoneAway;
510
+ }
511
+ has(streamId) {
512
+ return this.#streams.has(streamId);
513
+ }
514
+ sendWindow(streamId) {
515
+ return this.#streams.get(streamId)?.sendWindow ?? 0;
516
+ }
517
+ /** The phone side. Data may follow at once, within the first 256 KiB. */
518
+ open() {
519
+ if (this.role !== "phone") throw new YamuxError("computer_open");
520
+ if (this.#localGoneAway || this.#peerGoneAway) throw new YamuxError("gone_away");
521
+ if (this.#streams.size >= MAX_STREAMS) throw new YamuxError("too_many_streams");
522
+ const streamId = this.#highestId === 0 ? 1 : this.#highestId + 2;
523
+ if (streamId > MAX_STREAM_ID) throw new YamuxError("ids_exhausted");
524
+ this.#highestId = streamId;
525
+ this.#streams.set(streamId, {
526
+ established: false,
527
+ receiveWindow: INITIAL_WINDOW + OPEN_WINDOW_DELTA,
528
+ sendWindow: INITIAL_WINDOW,
529
+ uncredited: 0,
530
+ localFin: false,
531
+ remoteFin: false
532
+ });
533
+ this.#events.send(openFrame(streamId));
534
+ return streamId;
535
+ }
536
+ /** The computer side, once the origin socket is connected. */
537
+ accept(streamId) {
538
+ const stream = this.#streams.get(streamId);
539
+ if (!stream || stream.established || this.role !== "computer") return;
540
+ stream.established = true;
541
+ stream.receiveWindow += OPEN_WINDOW_DELTA;
542
+ this.#events.send(acceptFrame(streamId));
543
+ }
544
+ /** Sends up to the window and the frame cap; returns the bytes taken. */
545
+ write(streamId, data) {
546
+ const stream = this.#streams.get(streamId);
547
+ if (!stream || stream.localFin || this.role === "computer" && !stream.established) return 0;
548
+ const size = Math.min(data.length, stream.sendWindow, MAX_DATA_PAYLOAD);
549
+ if (size === 0) return 0;
550
+ stream.sendWindow -= size;
551
+ this.#events.send(dataFrame(streamId, data.subarray(0, size)));
552
+ return size;
553
+ }
554
+ finish(streamId) {
555
+ const stream = this.#streams.get(streamId);
556
+ if (!stream || stream.localFin) return;
557
+ stream.localFin = true;
558
+ this.#events.send(finFrame(streamId));
559
+ this.#closeIfDone(streamId, stream);
560
+ }
561
+ reset(streamId) {
562
+ if (!this.#streams.delete(streamId)) return;
563
+ this.#events.send(resetFrame(streamId));
564
+ this.#events.reset(streamId);
565
+ }
566
+ /** The receiver handed `bytes` on (to the origin socket, or the HTTP stack). Grants them back in 1 MiB steps. */
567
+ consumed(streamId, bytes) {
568
+ const stream = this.#streams.get(streamId);
569
+ if (!stream || stream.remoteFin) return;
570
+ stream.uncredited += bytes;
571
+ if (stream.uncredited < WINDOW_UPDATE_AFTER) return;
572
+ const delta = stream.uncredited;
573
+ stream.uncredited = 0;
574
+ stream.receiveWindow += delta;
575
+ this.#events.send(windowFrame(streamId, delta));
576
+ }
577
+ ping(value) {
578
+ this.#events.send(pingFrame(value));
579
+ }
580
+ goAway(code = GO_AWAY.normal) {
581
+ this.#localGoneAway = true;
582
+ this.#events.send(goAwayFrame(code));
583
+ }
584
+ /** Sends GoAway(1); the caller closes with 4000. */
585
+ protocolError() {
586
+ this.goAway(GO_AWAY.protocol);
587
+ this.closeAll();
588
+ }
589
+ closeAll() {
590
+ const ids = [...this.#streams.keys()];
591
+ this.#streams.clear();
592
+ for (const id2 of ids) this.#events.reset(id2);
593
+ }
594
+ receive(frame) {
595
+ if (frame.type === TYPE.ping) {
596
+ if (frame.flags === FLAG.syn) this.#events.send(pingFrame(frame.length, FLAG.ack));
597
+ else this.#events.pong?.(frame.length);
598
+ return;
599
+ }
600
+ if (frame.type === TYPE.goAway) {
601
+ this.#peerGoneAway = true;
602
+ this.#events.goAway?.(frame.length);
603
+ return;
604
+ }
605
+ if (frame.flags & FLAG.syn) {
606
+ this.#receiveOpen(frame);
607
+ return;
608
+ }
609
+ if (frame.flags & FLAG.ack) {
610
+ this.#receiveAck(frame);
611
+ return;
612
+ }
613
+ const stream = this.#known(frame.streamId);
614
+ if (!stream) return;
615
+ this.#receiveOnStream(frame.streamId, stream, frame);
616
+ }
617
+ /** A stream that ended is ignored: an RST and the peer's last frames cross. An ID never opened is an error. */
618
+ #known(streamId) {
619
+ if (streamId % 2 === 0 || streamId > this.#highestId) throw new YamuxError("unknown_stream");
620
+ return this.#streams.get(streamId) ?? null;
621
+ }
622
+ #receiveOpen(frame) {
623
+ if (this.role === "phone") throw new YamuxError("computer_opened_stream");
624
+ if (frame.type !== TYPE.windowUpdate || frame.flags !== FLAG.syn) throw new YamuxError("open_flags");
625
+ if (frame.streamId % 2 === 0) throw new YamuxError("even_stream");
626
+ if (frame.streamId <= this.#highestId) throw new YamuxError("stream_reused");
627
+ this.#highestId = frame.streamId;
628
+ if (this.#localGoneAway || this.#streams.size >= MAX_STREAMS) {
629
+ this.#events.send(resetFrame(frame.streamId));
630
+ return;
631
+ }
632
+ this.#streams.set(frame.streamId, {
633
+ established: false,
634
+ receiveWindow: INITIAL_WINDOW,
635
+ sendWindow: INITIAL_WINDOW + frame.length,
636
+ uncredited: 0,
637
+ localFin: false,
638
+ remoteFin: false
639
+ });
640
+ this.#events.opened?.(frame.streamId);
641
+ }
642
+ #receiveAck(frame) {
643
+ if (this.role === "computer") throw new YamuxError("phone_ack");
644
+ if (frame.type !== TYPE.windowUpdate || (frame.flags & ~(FLAG.ack | FLAG.fin | FLAG.rst)) !== 0) throw new YamuxError("ack_flags");
645
+ const stream = this.#known(frame.streamId);
646
+ if (!stream) return;
647
+ if (stream.established) throw new YamuxError("duplicate_ack");
648
+ stream.established = true;
649
+ this.#events.accepted?.(frame.streamId);
650
+ this.#receiveOnStream(frame.streamId, stream, { ...frame, flags: frame.flags & ~FLAG.ack });
651
+ }
652
+ #receiveOnStream(streamId, stream, frame) {
653
+ if (frame.flags & FLAG.rst) {
654
+ this.#streams.delete(streamId);
655
+ this.#events.reset(streamId);
656
+ return;
657
+ }
658
+ if (frame.type === TYPE.data) {
659
+ if (stream.remoteFin && (frame.length > 0 || frame.flags & FLAG.fin)) throw new YamuxError("data_after_fin");
660
+ if (frame.length > stream.receiveWindow) throw new YamuxError("window_exceeded");
661
+ stream.receiveWindow -= frame.length;
662
+ if (frame.length > 0) this.#events.data(streamId, frame.data);
663
+ } else if (frame.length > 0) {
664
+ stream.sendWindow += frame.length;
665
+ this.#events.writable(streamId);
666
+ }
667
+ if (frame.flags & FLAG.fin) {
668
+ if (stream.remoteFin) throw new YamuxError("data_after_fin");
669
+ stream.remoteFin = true;
670
+ this.#events.ended(streamId);
671
+ this.#closeIfDone(streamId, stream);
672
+ }
673
+ }
674
+ #closeIfDone(streamId, stream) {
675
+ if (stream.localFin && stream.remoteFin) this.#streams.delete(streamId);
676
+ }
677
+ };
678
+ var SEND_BUFFER_HIGH = 1024 * 1024;
679
+ var SessionClosed = class extends Error {
680
+ code;
681
+ constructor(code) {
682
+ super(`e2e session closed (${code})`);
683
+ this.code = code;
684
+ }
685
+ };
686
+ var noop = () => {
687
+ };
688
+ var MuxStream = class extends Duplex {
689
+ streamId;
690
+ #session;
691
+ #pending = null;
692
+ #pendingDone = null;
693
+ #ungranted = 0;
694
+ constructor(session, streamId) {
695
+ super({ allowHalfOpen: true, readableHighWaterMark: 256 * 1024 });
696
+ this.#session = session;
697
+ this.streamId = streamId;
698
+ this.on("error", noop);
699
+ }
700
+ _write(chunk, _encoding, done) {
701
+ this.#pending = chunk;
702
+ this.#pendingDone = done;
703
+ this.flush();
704
+ }
705
+ _final(done) {
706
+ this.#session.mux?.finish(this.streamId);
707
+ done();
708
+ }
709
+ _read() {
710
+ if (this.#ungranted === 0) return;
711
+ const bytes = this.#ungranted;
712
+ this.#ungranted = 0;
713
+ this.#session.mux?.consumed(this.streamId, bytes);
714
+ }
715
+ _destroy(error2, done) {
716
+ const mux = this.#session.mux;
717
+ if (mux?.has(this.streamId)) mux.reset(this.streamId);
718
+ this.#session.forget(this.streamId);
719
+ const pendingDone = this.#pendingDone;
720
+ this.#pending = null;
721
+ this.#pendingDone = null;
722
+ pendingDone?.(error2 ?? new Error("stream reset"));
723
+ done(error2);
724
+ }
725
+ /** Granted back as the reader takes it, so a slow reader slows the sender. */
726
+ deliver(data) {
727
+ if (this.push(data)) this.#session.mux?.consumed(this.streamId, data.length);
728
+ else this.#ungranted += data.length;
729
+ }
730
+ remoteEnded() {
731
+ this.push(null);
732
+ }
733
+ remoteReset() {
734
+ if (!this.destroyed) this.destroy(new Error("stream reset"));
735
+ }
736
+ flush() {
737
+ const mux = this.#session.mux;
738
+ while (this.#pending && this.#pending.length > 0) {
739
+ if (!mux || !mux.has(this.streamId)) return void this.destroy(new Error("stream reset"));
740
+ if (this.#session.congested) return this.#session.whenDrained(() => this.flush());
741
+ const taken = mux.write(this.streamId, this.#pending);
742
+ if (taken === 0) return;
743
+ this.#pending = this.#pending.subarray(taken);
744
+ }
745
+ const done = this.#pendingDone;
746
+ this.#pending = null;
747
+ this.#pendingDone = null;
748
+ done?.();
749
+ }
750
+ };
751
+ var E2ESession = class _E2ESession {
752
+ role;
753
+ #transport;
754
+ #streams = /* @__PURE__ */ new Map();
755
+ #timers;
756
+ #state = "handshake";
757
+ #keys = null;
758
+ #mux = null;
759
+ #accept = null;
760
+ #deadline = null;
761
+ #pingTimer = null;
762
+ #pongTimer = null;
763
+ #awaitingPong = null;
764
+ #pingCounter = 0;
765
+ #drainWaiters = [];
766
+ #settleReady;
767
+ #settleClosed;
768
+ #connectOrigin = null;
769
+ #removed = false;
770
+ /** The computer side: which device (or pairing window) this is. */
771
+ peer = null;
772
+ ready;
773
+ closed;
774
+ constructor(role, transport, timers) {
775
+ this.role = role;
776
+ this.#transport = transport;
777
+ this.#timers = {
778
+ pingIntervalMs: timers.pingIntervalMs ?? PING_INTERVAL_MS,
779
+ pingTimeoutMs: timers.pingTimeoutMs ?? PING_TIMEOUT_MS,
780
+ handshakeDeadlineMs: timers.handshakeDeadlineMs ?? HANDSHAKE_DEADLINE_MS
781
+ };
782
+ this.ready = new Promise((resolve4, reject) => {
783
+ this.#settleReady = (error2) => error2 ? reject(error2) : resolve4();
784
+ });
785
+ this.ready.catch(noop);
786
+ this.closed = new Promise((resolve4) => {
787
+ this.#settleClosed = resolve4;
788
+ });
789
+ this.#deadline = setTimeout(() => this.close(CLOSE.deadline), this.#timers.handshakeDeadlineMs);
790
+ this.#deadline.unref?.();
791
+ }
792
+ /** Waits for message 1. Anything wrong with it closes 4001 and nothing else is sent. */
793
+ static computer(options) {
794
+ const session = new _E2ESession("computer", options.transport, options);
795
+ session.#connectOrigin = options.connectOrigin;
796
+ session.#accept = (message) => {
797
+ const accepted = computerAccept({ staticKey: options.staticKey, transport: options.transportByte, candidates: options.candidates() }, message);
798
+ if (!accepted) return session.close(CLOSE.rejected);
799
+ options.transport.send(accepted.message2);
800
+ session.peer = accepted.id;
801
+ session.#start(accepted.keys);
802
+ options.opened?.(accepted.id, accepted.payload1);
803
+ };
804
+ return session;
805
+ }
806
+ /** Sends message 1 now. `ready` rejects with a NoiseError when the computer can't be verified: a hard stop. */
807
+ static phone(options) {
808
+ const session = new _E2ESession("phone", options.transport, options);
809
+ const conn = newConn();
810
+ const hello = phoneHello(
811
+ { remoteStatic: options.remoteStatic, psk: options.psk, transport: options.transportByte, ephemeral: options.ephemeral },
812
+ encodePayload1(conn, options.app),
813
+ b64u(conn)
814
+ );
815
+ session.#accept = (message) => {
816
+ let keys;
817
+ try {
818
+ keys = hello.finish(message);
819
+ } catch (error2) {
820
+ session.#settleReady(error2);
821
+ return session.close(CLOSE.protocol);
822
+ }
823
+ session.#start(keys);
824
+ };
825
+ options.transport.send(hello.message1);
826
+ return session;
827
+ }
828
+ get mux() {
829
+ return this.#mux;
830
+ }
831
+ /** The phone side: the computer said, inside the encryption, that this device was removed. Only this, or a 4001 on
832
+ * a direct route, means "pair again"; a bare 4001 through the relay may be the relay's own and means "keep trying". */
833
+ get removed() {
834
+ return this.#removed;
835
+ }
836
+ get state() {
837
+ return this.#state;
838
+ }
839
+ /** Records each way so far, for diagnostics. */
840
+ get records() {
841
+ return { sent: this.#keys?.send.nonce ?? 0, received: this.#keys?.receive.nonce ?? 0 };
842
+ }
843
+ get congested() {
844
+ return this.#transport.bufferedAmount > SEND_BUFFER_HIGH;
845
+ }
846
+ get activeStreams() {
847
+ return this.#streams.size;
848
+ }
849
+ whenDrained(listener) {
850
+ this.#drainWaiters.push(listener);
851
+ if (this.#drainWaiters.length > 1) return;
852
+ this.#transport.onceDrained(() => {
853
+ const waiters = this.#drainWaiters;
854
+ this.#drainWaiters = [];
855
+ for (const waiter of waiters) waiter();
856
+ });
857
+ }
858
+ forget(streamId) {
859
+ this.#streams.delete(streamId);
860
+ }
861
+ /** Every message the transport receives, in order. */
862
+ receive(message, binary) {
863
+ if (this.#state === "closed") return;
864
+ if (this.#state === "handshake") {
865
+ const accept = this.#accept;
866
+ this.#accept = null;
867
+ if (!binary || !accept) return this.close(this.role === "computer" ? CLOSE.rejected : CLOSE.protocol);
868
+ return accept(Buffer.from(message));
869
+ }
870
+ if (!binary) return this.close(CLOSE.protocol);
871
+ let plaintext;
872
+ try {
873
+ plaintext = this.#keys.receive.open(message);
874
+ } catch {
875
+ return this.close(CLOSE.protocol);
876
+ }
877
+ try {
878
+ this.#mux.receive(decodeFrame(plaintext));
879
+ } catch (error2) {
880
+ if (!(error2 instanceof YamuxError)) throw error2;
881
+ this.#mux?.protocolError();
882
+ this.close(CLOSE.protocol);
883
+ }
884
+ }
885
+ /** The phone side. */
886
+ openStream() {
887
+ if (this.#state !== "open" || !this.#mux) throw new SessionClosed(CLOSE.normal);
888
+ const streamId = this.#mux.open();
889
+ const stream = new MuxStream(this, streamId);
890
+ this.#streams.set(streamId, stream);
891
+ return stream;
892
+ }
893
+ /** The computer side: the device was removed. The phone hears it authenticated, then the 4001 close. */
894
+ remove() {
895
+ if (this.#state === "open") this.#mux?.goAway(GO_AWAY.removed);
896
+ this.close(CLOSE.rejected);
897
+ }
898
+ /** A planned close (quit, sleep, restart): GoAway first, so the peer knows it isn't an error. */
899
+ shutdown() {
900
+ if (this.#state === "open") this.#mux?.goAway(GO_AWAY.normal);
901
+ this.close(CLOSE.normal);
902
+ }
903
+ close(code, reason) {
904
+ if (this.#state === "closed") return;
905
+ this.#transport.close(code, reason);
906
+ this.#end(code);
907
+ }
908
+ /** The transport closed under us. */
909
+ transportClosed(code) {
910
+ this.#end(code);
911
+ }
912
+ #end(code) {
913
+ if (this.#state === "closed") return;
914
+ const wasHandshake = this.#state === "handshake";
915
+ this.#state = "closed";
916
+ for (const timer of [this.#deadline, this.#pongTimer]) if (timer) clearTimeout(timer);
917
+ if (this.#pingTimer) clearInterval(this.#pingTimer);
918
+ this.#mux?.closeAll();
919
+ for (const stream of this.#streams.values()) stream.remoteReset();
920
+ this.#streams.clear();
921
+ const waiters = this.#drainWaiters;
922
+ this.#drainWaiters = [];
923
+ for (const waiter of waiters) waiter();
924
+ if (wasHandshake) this.#settleReady(new SessionClosed(code));
925
+ this.#settleClosed(code);
926
+ }
927
+ #start(keys) {
928
+ if (this.#deadline) clearTimeout(this.#deadline);
929
+ this.#keys = keys;
930
+ this.#state = "open";
931
+ this.#mux = new Mux(this.role, {
932
+ send: (frame) => this.#send(frame),
933
+ opened: (streamId) => this.#openOrigin(streamId),
934
+ data: (streamId, data) => this.#streams.get(streamId)?.deliver(Buffer.from(data.buffer, data.byteOffset, data.byteLength)),
935
+ ended: (streamId) => this.#streams.get(streamId)?.remoteEnded(),
936
+ reset: (streamId) => {
937
+ const stream = this.#streams.get(streamId);
938
+ this.#streams.delete(streamId);
939
+ stream?.remoteReset();
940
+ },
941
+ writable: (streamId) => this.#streams.get(streamId)?.flush(),
942
+ goAway: (code) => {
943
+ if (code === GO_AWAY.removed && this.role === "phone") this.#removed = true;
944
+ else if (code !== GO_AWAY.normal) this.close(CLOSE.protocol);
945
+ },
946
+ pong: (value) => {
947
+ if (value !== this.#awaitingPong) return;
948
+ this.#awaitingPong = null;
949
+ if (this.#pongTimer) clearTimeout(this.#pongTimer);
950
+ }
951
+ });
952
+ this.#pingTimer = setInterval(() => this.#ping(), this.#timers.pingIntervalMs);
953
+ this.#pingTimer.unref?.();
954
+ this.#settleReady();
955
+ }
956
+ #ping() {
957
+ if (this.#awaitingPong !== null || !this.#mux) return;
958
+ this.#pingCounter = this.#pingCounter + 1 >>> 0;
959
+ this.#awaitingPong = this.#pingCounter;
960
+ this.#mux.ping(this.#pingCounter);
961
+ this.#pongTimer = setTimeout(() => this.close(CLOSE.goingAway), this.#timers.pingTimeoutMs);
962
+ this.#pongTimer.unref?.();
963
+ }
964
+ #send(frame) {
965
+ if (this.#state !== "open" || !this.#keys) return;
966
+ let record;
967
+ try {
968
+ record = this.#keys.send.seal(encodeFrame(frame));
969
+ } catch (error2) {
970
+ if (error2 instanceof NoiseError) return this.close(CLOSE.normal);
971
+ throw error2;
972
+ }
973
+ this.#transport.send(record);
974
+ if (this.#keys.send.nonce >= NONCE_LIMIT - 1024) queueMicrotask(() => this.close(CLOSE.normal));
975
+ }
976
+ #openOrigin(streamId) {
977
+ const stream = new MuxStream(this, streamId);
978
+ this.#streams.set(streamId, stream);
979
+ const connect = this.#connectOrigin;
980
+ if (!connect) return void stream.destroy();
981
+ connect().then(
982
+ (socket) => {
983
+ if (stream.destroyed || this.#state !== "open") return void socket.destroy();
984
+ this.#mux?.accept(streamId);
985
+ socket.on("error", () => stream.destroy());
986
+ socket.on("close", () => {
987
+ if (!socket.readableEnded) stream.destroy();
988
+ });
989
+ stream.on("close", () => socket.destroy());
990
+ stream.pipe(socket);
991
+ socket.pipe(stream);
992
+ },
993
+ () => stream.destroy()
994
+ );
995
+ }
996
+ };
997
+ var DIRECT_PROTOCOL = "omb-e2e.v1";
998
+
999
+ // src/secure-pairing.ts
1000
+ var PAIRING_TTL_MS = 12e4;
1001
+ var PAIRING_ATTEMPTS = 5;
1002
+ var MAX_PAIRED_CLIENTS = 128;
1003
+ var MAX_INVITES = 20;
1004
+ var digestEqual = (a, b) => /^[0-9a-f]{64}$/.test(a) && /^[0-9a-f]{64}$/.test(b) && timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
1005
+ var nameOf = (value) => String(value ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 60) || "DSH Host";
1006
+ var SecurePairing = class {
1007
+ constructor(db, machineId, commitClient, now = Date.now) {
1008
+ this.db = db;
1009
+ this.machineId = machineId;
1010
+ this.commitClient = commitClient;
1011
+ this.now = now;
1012
+ db.exec(`CREATE TABLE IF NOT EXISTS pairing_invites (
1013
+ id TEXT PRIMARY KEY, client_id TEXT NOT NULL, token_digest TEXT NOT NULL, code_digest TEXT NOT NULL,
1014
+ expires_at INTEGER NOT NULL, attempts_left INTEGER NOT NULL, ceiling TEXT NOT NULL, name TEXT NOT NULL,
1015
+ consumed_request TEXT, consumed_at INTEGER
1016
+ ); CREATE INDEX IF NOT EXISTS pairing_invite_expiry ON pairing_invites(expires_at);`);
1017
+ const generated = generateKeyPair();
1018
+ db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("e2eTransportPrivateKey", b64u(generated.privateKey));
1019
+ const stored = db.prepare("SELECT value FROM meta WHERE key=?").get("e2eTransportPrivateKey")?.value;
1020
+ if (typeof stored !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(stored)) fail("PAIRING_IDENTITY_INVALID");
1021
+ try {
1022
+ this.transportKey = keyPairFromPrivate(Buffer.from(stored, "base64url"));
1023
+ } catch {
1024
+ fail("PAIRING_IDENTITY_INVALID");
1025
+ }
1026
+ }
1027
+ db;
1028
+ machineId;
1029
+ commitClient;
1030
+ now;
1031
+ replay = /* @__PURE__ */ new Map();
1032
+ transportKey;
1033
+ identity() {
1034
+ return { kind: "noise-v1", publicKey: b64u(this.transportKey.publicKey), fingerprint: sha(this.transportKey.publicKey) };
1035
+ }
1036
+ create(clientId, ceiling, options) {
1037
+ identifier(clientId);
1038
+ let endpoint;
1039
+ try {
1040
+ endpoint = new URL(options.endpoint);
1041
+ } catch {
1042
+ fail("PAIRING_ENDPOINT_INVALID");
1043
+ }
1044
+ if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.href.length > 1024) fail("PAIRING_ENDPOINT_INVALID");
1045
+ const now = this.now(), token = `dsh_pair_${secret()}`, code = String(randomInt(1e6)).padStart(6, "0"), id2 = `invite-${randomUUID()}`;
1046
+ return this.db.transaction(() => {
1047
+ this.db.prepare("DELETE FROM pairing_invites WHERE expires_at<=?").run(now - PAIRING_TTL_MS);
1048
+ if (this.db.prepare("SELECT 1 FROM clients WHERE id=? AND revoked=0").get(clientId)) fail("CLIENT_ALREADY_PAIRED", 409);
1049
+ if (this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
1050
+ this.db.prepare("UPDATE pairing_invites SET attempts_left=0 WHERE client_id=? AND consumed_request IS NULL").run(clientId);
1051
+ if (this.db.prepare("SELECT count(*) AS n FROM pairing_invites WHERE expires_at>? AND attempts_left>0 AND consumed_request IS NULL").get(now).n >= MAX_INVITES) fail("PAIRING_WINDOW_LIMIT", 429);
1052
+ this.db.prepare("INSERT INTO pairing_invites VALUES (?,?,?,?,?,?,?,?,NULL,NULL)").run(id2, clientId, sha(token), sha(code), now + PAIRING_TTL_MS, PAIRING_ATTEMPTS, canonical(ceiling), nameOf(options.name));
1053
+ return {
1054
+ format: "dsh-pairing-invite",
1055
+ version: 2,
1056
+ inviteId: id2,
1057
+ machineId: this.machineId,
1058
+ clientId,
1059
+ name: nameOf(options.name),
1060
+ endpoint: endpoint.origin,
1061
+ expiresAt: now + PAIRING_TTL_MS,
1062
+ token,
1063
+ code,
1064
+ transport: this.identity(),
1065
+ ceiling: structuredClone(ceiling)
1066
+ };
1067
+ }).immediate();
1068
+ }
1069
+ candidates() {
1070
+ const now = this.now(), transportId = keyId(this.transportKey.publicKey);
1071
+ for (const [id2, row] of this.replay) if (row.expiresAt <= now) this.replay.delete(id2);
1072
+ const invites = this.db.prepare("SELECT id,token_digest FROM pairing_invites WHERE expires_at>? AND attempts_left>0").all(now).map((row) => ({ id: { kind: "invite", id: row.id, digest: row.token_digest }, hash: row.token_digest }));
1073
+ const clients = this.db.prepare("SELECT id,digest,revision FROM clients WHERE revoked=0").all().map((row) => ({ id: { kind: "client", id: row.id, digest: row.digest, revision: row.revision }, hash: row.digest }));
1074
+ if (clients.length > MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
1075
+ return [...invites, ...clients].map((row) => ({ id: row.id, psk: psk(credentialHashFromHex(row.hash), transportId) }));
1076
+ }
1077
+ validPeer(peer) {
1078
+ if (peer.kind === "client") {
1079
+ const client = this.db.prepare("SELECT digest,revision,revoked FROM clients WHERE id=?").get(peer.id);
1080
+ return Boolean(client && !client.revoked && client.revision === peer.revision && digestEqual(client.digest, peer.digest));
1081
+ }
1082
+ const invite = this.db.prepare("SELECT token_digest,expires_at,attempts_left FROM pairing_invites WHERE id=?").get(peer.id);
1083
+ return Boolean(invite && invite.expires_at > this.now() && invite.attempts_left > 0 && digestEqual(invite.token_digest, peer.digest));
1084
+ }
1085
+ redeem(peer, input) {
1086
+ if (peer.kind !== "invite" || !input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key2) => !["requestId", "code"].includes(key2)) || typeof input.requestId !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(input.requestId) || typeof input.code !== "string" || !/^\d{6}$/.test(input.code)) fail("PAIRING_REQUEST_INVALID");
1087
+ const now = this.now();
1088
+ const decision = this.db.transaction(() => {
1089
+ const row = this.db.prepare("SELECT * FROM pairing_invites WHERE id=?").get(peer.id);
1090
+ if (!row || !digestEqual(row.token_digest, peer.digest) || row.expires_at <= now || row.attempts_left <= 0) return { error: "PAIRING_WINDOW_CLOSED" };
1091
+ if (!digestEqual(row.code_digest, sha(input.code))) {
1092
+ this.db.prepare("UPDATE pairing_invites SET attempts_left=attempts_left-1 WHERE id=?").run(peer.id);
1093
+ return { error: "PAIRING_CODE_INVALID" };
1094
+ }
1095
+ if (row.consumed_request !== null) {
1096
+ const cached = this.replay.get(row.id), client = this.db.prepare("SELECT digest,revoked FROM clients WHERE id=?").get(row.client_id);
1097
+ if (row.consumed_request !== input.requestId || !cached || cached.expiresAt <= now || cached.proof !== peer.digest || cached.requestId !== input.requestId) return { error: "PAIRING_ALREADY_REDEEMED" };
1098
+ if (!client || client.revoked || !digestEqual(client.digest, sha(cached.result.token))) return { error: "PAIRING_DEVICE_CHANGED" };
1099
+ return { result: structuredClone(cached.result), ceiling: JSON.parse(row.ceiling), recovered: true };
1100
+ }
1101
+ const result = this.commitClient(row.client_id, JSON.parse(row.ceiling));
1102
+ this.db.prepare("UPDATE pairing_invites SET consumed_request=?,consumed_at=? WHERE id=?").run(input.requestId, now, row.id);
1103
+ this.replay.set(row.id, { proof: peer.digest, requestId: input.requestId, expiresAt: row.expires_at, result: structuredClone(result) });
1104
+ return { result, ceiling: JSON.parse(row.ceiling), recovered: false };
1105
+ }).immediate();
1106
+ if (decision.error) fail(decision.error, 403);
1107
+ return { ...decision.result, transport: this.identity(), ceiling: decision.ceiling, recovered: decision.recovered };
1108
+ }
1109
+ invalidate(clientId) {
1110
+ this.db.prepare("UPDATE pairing_invites SET attempts_left=0 WHERE client_id=?").run(clientId);
1111
+ for (const [id2, row] of this.replay) if (row.result.clientId === clientId) this.replay.delete(id2);
1112
+ }
1113
+ close() {
1114
+ this.replay.clear();
1115
+ }
1116
+ };
1117
+
1118
+ // src/store.ts
84
1119
  var Store = class {
85
1120
  constructor(dataDir, Database, options = {}) {
86
1121
  this.dataDir = dataDir;
@@ -125,7 +1160,7 @@ var Store = class {
125
1160
  unlinkSync(this.lockPath);
126
1161
  this.lockFd = openSync(this.lockPath, "wx", 384);
127
1162
  }
128
- writeFileSync(this.lockFd, JSON.stringify({ pid: process.pid, instanceId: randomUUID() }));
1163
+ writeFileSync(this.lockFd, JSON.stringify({ pid: process.pid, instanceId: randomUUID2() }));
129
1164
  }
130
1165
  this.db = new Driver(join(dataDir, "agent.sqlite"));
131
1166
  this.db.pragma("journal_mode = WAL");
@@ -139,8 +1174,9 @@ var Store = class {
139
1174
  CREATE TABLE IF NOT EXISTS artifacts(id TEXT PRIMARY KEY,context TEXT NOT NULL,hash TEXT NOT NULL,size INTEGER NOT NULL,media TEXT NOT NULL);
140
1175
  CREATE TABLE IF NOT EXISTS worktrees(id TEXT PRIMARY KEY,context TEXT NOT NULL,root TEXT NOT NULL,path TEXT NOT NULL,head TEXT NOT NULL,source TEXT NOT NULL);
141
1176
  CREATE TABLE IF NOT EXISTS audit(id INTEGER PRIMARY KEY,time INTEGER NOT NULL,code TEXT NOT NULL,actor TEXT,operation TEXT);`);
142
- this.db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("machineId", `machine-${randomUUID()}`);
1177
+ this.db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("machineId", `machine-${randomUUID2()}`);
143
1178
  this.machineId = this.db.prepare("SELECT value FROM meta WHERE key=?").get("machineId").value;
1179
+ this.pairing = new SecurePairing(this.db, this.machineId, (id2, ceiling) => this.commitClient(id2, ceiling));
144
1180
  if (!options.admin) this.db.prepare("UPDATE operations SET status='unknown',error='OUTCOME_UNKNOWN' WHERE status IN ('admitted','running','cancel_pending')").run();
145
1181
  if (!options.admin) this.db.prepare("DELETE FROM grants").run();
146
1182
  } catch (e) {
@@ -154,6 +1190,7 @@ var Store = class {
154
1190
  }
155
1191
  }
156
1192
  dataDir;
1193
+ pairing;
157
1194
  db;
158
1195
  machineId;
159
1196
  lockFd;
@@ -190,7 +1227,7 @@ var Store = class {
190
1227
  }
191
1228
  }
192
1229
  }
193
- pair(id2, ceiling) {
1230
+ approvedCeiling(id2, ceiling) {
194
1231
  identifier(id2);
195
1232
  if (!ceiling || ceiling.hostManagedBots !== void 0 && typeof ceiling.hostManagedBots !== "boolean" || !Array.isArray(ceiling.botIds ?? []) || !Array.isArray(ceiling.roots) || !ceiling.roots.length || !Array.isArray(ceiling.tools) || !ceiling.tools.length || ceiling.tools.some((x) => !TOOL_NAMES.includes(x))) fail("INVALID_CEILING");
196
1233
  const managed = ceiling.hostManagedBots === true, botIds = ceiling.botIds ?? [];
@@ -199,9 +1236,25 @@ var Store = class {
199
1236
  botIds.forEach(identifier);
200
1237
  ceiling.roots.forEach(identifier);
201
1238
  const saved = { botIds, roots: ceiling.roots, tools: ceiling.tools, ...managed ? { hostManagedBots: true } : {} };
202
- const token = secret();
203
- this.db.prepare("INSERT INTO clients VALUES (?,?,1,0,?)").run(id2, sha(token), canonical(saved));
204
- return { clientId: id2, token, machineId: this.machineId, secretRef: `remote-agent/${this.machineId}/${id2}`, hostManagedBots: managed };
1239
+ return saved;
1240
+ }
1241
+ commitClient(id2, ceiling) {
1242
+ return this.db.transaction(() => {
1243
+ const previous = this.db.prepare("SELECT revoked FROM clients WHERE id=?").get(id2);
1244
+ if (previous && !previous.revoked) fail("CLIENT_ALREADY_PAIRED", 409);
1245
+ if (this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
1246
+ const token = secret();
1247
+ if (previous) this.db.prepare("UPDATE clients SET digest=?,revision=revision+1,revoked=0,ceiling=? WHERE id=? AND revoked=1").run(sha(token), canonical(ceiling), id2);
1248
+ else this.db.prepare("INSERT INTO clients VALUES (?,?,1,0,?)").run(id2, sha(token), canonical(ceiling));
1249
+ return { clientId: id2, token, machineId: this.machineId, secretRef: `remote-agent/${this.machineId}/${id2}`, hostManagedBots: ceiling.hostManagedBots === true };
1250
+ }).immediate();
1251
+ }
1252
+ pair(id2, ceiling) {
1253
+ return this.commitClient(id2, this.approvedCeiling(id2, ceiling));
1254
+ }
1255
+ invite(id2, ceiling, options) {
1256
+ if (!this.admin) fail("ADMIN_REQUIRED", 403);
1257
+ return this.pairing.create(id2, this.approvedCeiling(id2, ceiling), options);
205
1258
  }
206
1259
  client(token) {
207
1260
  if (typeof token !== "string" || token.length > 256) fail("AUTH_INVALID", 401);
@@ -211,12 +1264,20 @@ var Store = class {
211
1264
  return row;
212
1265
  }
213
1266
  rotate(id2) {
214
- const token = secret();
215
- if (!this.db.prepare("UPDATE clients SET digest=?,revision=revision+1,revoked=0 WHERE id=?").run(sha(token), identifier(id2)).changes) fail("AUTH_INVALID", 401);
216
- return { clientId: id2, token };
1267
+ return this.db.transaction(() => {
1268
+ identifier(id2);
1269
+ const row = this.db.prepare("SELECT revoked FROM clients WHERE id=?").get(id2);
1270
+ if (!row) fail("AUTH_INVALID", 401);
1271
+ if (row.revoked && this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
1272
+ const token = secret();
1273
+ this.db.prepare("UPDATE clients SET digest=?,revision=revision+1,revoked=0 WHERE id=?").run(sha(token), id2);
1274
+ this.pairing.invalidate(id2);
1275
+ return { clientId: id2, token };
1276
+ }).immediate();
217
1277
  }
218
1278
  revoke(id2) {
219
1279
  this.db.prepare("UPDATE clients SET revoked=1,revision=revision+1 WHERE id=?").run(identifier(id2));
1280
+ this.pairing.invalidate(id2);
220
1281
  }
221
1282
  currentClient(client) {
222
1283
  const row = this.db.prepare("SELECT * FROM clients WHERE id=?").get(client?.id);
@@ -232,7 +1293,7 @@ var Store = class {
232
1293
  if (!Number.isSafeInteger(ttl) || ttl < 1e3 || ttl > 3e5) fail("INVALID_ARGUMENT");
233
1294
  this.db.prepare("DELETE FROM grants WHERE json_extract(context,'$.expiresAt')<=?").run(Date.now());
234
1295
  if (this.db.prepare("SELECT count(*) AS count FROM grants").get().count >= 4096) fail("GRANT_LIMIT", 429);
235
- const context = { clientId: client.id, botId: request.botId, taskId: request.taskId, attemptId: request.attemptId, roots: request.roots, tools: request.tools, expiresAt: Date.now() + ttl, revision: client.revision, grantId: randomUUID() };
1296
+ const context = { clientId: client.id, botId: request.botId, taskId: request.taskId, attemptId: request.attemptId, roots: request.roots, tools: request.tools, expiresAt: Date.now() + ttl, revision: client.revision, grantId: randomUUID2() };
236
1297
  const token = secret();
237
1298
  this.db.prepare("INSERT INTO grants VALUES (?,?)").run(sha(token), canonical(context));
238
1299
  return { grantToken: token, machineId: this.machineId, agentApiMajor: 1, ...context };
@@ -297,6 +1358,7 @@ var Store = class {
297
1358
  close() {
298
1359
  if (this.closed) return;
299
1360
  this.closed = true;
1361
+ this.pairing?.close();
300
1362
  try {
301
1363
  this.db.close();
302
1364
  } finally {
@@ -460,7 +1522,7 @@ var Roots = class {
460
1522
  };
461
1523
 
462
1524
  // src/browser.ts
463
- import { randomUUID as randomUUID2, createHash as createHash2 } from "node:crypto";
1525
+ import { randomUUID as randomUUID3, createHash as createHash3 } from "node:crypto";
464
1526
  import { access, mkdir, readFile, stat as stat2, unlink } from "node:fs/promises";
465
1527
  import { join as join2 } from "node:path";
466
1528
  var BrowserTools = class {
@@ -509,7 +1571,7 @@ var BrowserTools = class {
509
1571
  if (a.profileId !== void 0) {
510
1572
  const root = this.profileRoots[a.profileId];
511
1573
  if (!root) fail("BROWSER_PROFILE_DENIED", 403);
512
- const owner = JSON.stringify([c.clientId, c.botId, a.profileId]), key2 = createHash2("sha256").update(owner).digest("hex");
1574
+ const owner = JSON.stringify([c.clientId, c.botId, a.profileId]), key2 = createHash3("sha256").update(owner).digest("hex");
513
1575
  try {
514
1576
  context = this.profiles.get(key2);
515
1577
  if (!context) {
@@ -541,9 +1603,9 @@ var BrowserTools = class {
541
1603
  });
542
1604
  const page = await context.newPage();
543
1605
  page.on("popup", (p) => void p.close());
544
- const id2 = `browser-${randomUUID2()}`, downloads = /* @__PURE__ */ new Map();
1606
+ const id2 = `browser-${randomUUID3()}`, downloads = /* @__PURE__ */ new Map();
545
1607
  page.on("download", (download2) => {
546
- const downloadId2 = `download-${randomUUID2()}`;
1608
+ const downloadId2 = `download-${randomUUID3()}`;
547
1609
  downloads.set(downloadId2, { download: download2, path: download2.path(), suggestedFilename: download2.suggestedFilename() });
548
1610
  });
549
1611
  this.sessions.set(id2, { context, page, owner: JSON.stringify([c.clientId, c.botId, c.taskId, c.attemptId]), revision: null, persistent, profileId, downloads });
@@ -592,7 +1654,7 @@ var BrowserTools = class {
592
1654
  return { completed: true, url: session.page.url(), result: value ?? null };
593
1655
  }
594
1656
  if (name === "browser_snapshot") {
595
- session.revision = `snapshot-${randomUUID2()}`;
1657
+ session.revision = `snapshot-${randomUUID3()}`;
596
1658
  const nodes = await session.page.locator('a,button,input,textarea,select,[role="button"]').evaluateAll((elements) => elements.slice(0, 300).map((el, i) => {
597
1659
  el.setAttribute("data-dsh-agent-ref", String(i));
598
1660
  return { ref: String(i), tag: el.tagName.toLowerCase(), text: (el.innerText || el.getAttribute("aria-label") || "").slice(0, 300) };
@@ -636,7 +1698,7 @@ var BrowserTools = class {
636
1698
  // src/desktop.ts
637
1699
  import net from "node:net";
638
1700
  import { readFile as readFile2 } from "node:fs/promises";
639
- import { randomUUID as randomUUID3 } from "node:crypto";
1701
+ import { randomUUID as randomUUID4 } from "node:crypto";
640
1702
  var error = (code, possiblyApplied = false, stopConfirmed = false) => Object.assign(new AgentError(code, 409), { possiblyApplied, stopConfirmed });
641
1703
  var DesktopTools = class {
642
1704
  constructor(config, options = {}) {
@@ -645,7 +1707,7 @@ var DesktopTools = class {
645
1707
  }
646
1708
  config;
647
1709
  options;
648
- nonce = randomUUID3();
1710
+ nonce = randomUUID4();
649
1711
  owners = /* @__PURE__ */ new Map();
650
1712
  closing = new AbortController();
651
1713
  active = /* @__PURE__ */ new Set();
@@ -820,7 +1882,7 @@ var DesktopTools = class {
820
1882
  import net2 from "node:net";
821
1883
  import { readFile as readFile3 } from "node:fs/promises";
822
1884
  import { EventEmitter } from "node:events";
823
- import { randomUUID as randomUUID4 } from "node:crypto";
1885
+ import { randomUUID as randomUUID5 } from "node:crypto";
824
1886
  var inputTools = ["desktop_click", "desktop_type", "desktop_key", "desktop_scroll"];
825
1887
  var unknown = () => Object.assign(new AgentError("OUTCOME_UNKNOWN", 409), { possiblyApplied: true, stopConfirmed: false });
826
1888
  var safeOrigin = (value) => {
@@ -990,7 +2052,7 @@ var DesktopSessions = class {
990
2052
  forceKeyframes = /* @__PURE__ */ new Set();
991
2053
  flights = /* @__PURE__ */ new WeakMap();
992
2054
  sessions = /* @__PURE__ */ new Map();
993
- nonce = randomUUID4();
2055
+ nonce = randomUUID5();
994
2056
  closing = false;
995
2057
  timer;
996
2058
  sockets = /* @__PURE__ */ new Set();
@@ -1028,7 +2090,7 @@ var DesktopSessions = class {
1028
2090
  if (mode === "control" && !controlAllowed) fail("SCOPE_DENIED", 403);
1029
2091
  await this.sweep();
1030
2092
  if (this.opening + [...this.sessions.values()].filter((s) => !s.closed).length >= 4 || this.sessions.size >= 256) fail("DESKTOP_SESSION_LIMIT", 429);
1031
- const id2 = `desktop-${randomUUID4()}`, token = secret();
2093
+ const id2 = `desktop-${randomUUID5()}`, token = secret();
1032
2094
  this.opening++;
1033
2095
  let native;
1034
2096
  try {
@@ -1091,6 +2153,13 @@ var DesktopSessions = class {
1091
2153
  const row = this.own(c, request?.sessionId);
1092
2154
  return this.stop(row, "SESSION_CLOSED");
1093
2155
  }
2156
+ async closeClientSession(clientId, request) {
2157
+ if (!request || typeof request !== "object" || Array.isArray(request) || Object.keys(request).some((key2) => !["sessionId", "botId", "taskId", "attemptId"].includes(key2))) fail("INVALID_ARGUMENT");
2158
+ for (const key2 of ["sessionId", "botId", "taskId", "attemptId"]) identifier(request[key2]);
2159
+ const row = this.sessions.get(request.sessionId);
2160
+ if (!row || row.context.clientId !== clientId || row.context.botId !== request.botId || row.context.taskId !== request.taskId || row.context.attemptId !== request.attemptId) fail("SCOPE_DENIED", 403);
2161
+ return this.stop(row, "SESSION_CLOSED");
2162
+ }
1094
2163
  hasOrigin(origin) {
1095
2164
  return [...this.sessions.values()].some((row) => !row.closed && row.origin === origin && row.context.expiresAt > Date.now());
1096
2165
  }
@@ -1206,7 +2275,7 @@ var DesktopSessions = class {
1206
2275
  fail("INVALID_ARGUMENT");
1207
2276
  }
1208
2277
  /** Called only after the HTTP server validates Host/network/TLS on upgrade. */
1209
- accept(socket, origin, allowedOrigins = []) {
2278
+ accept(socket, origin, allowedOrigins = [], peerClientId) {
1210
2279
  this.sockets.add(socket);
1211
2280
  let row, authenticating = false, alive2 = true;
1212
2281
  const authTimer = setTimeout(() => socket.close(1008, "Desktop authentication required"), this.options.authTimeoutMs ?? 5e3);
@@ -1272,6 +2341,7 @@ var DesktopSessions = class {
1272
2341
  try {
1273
2342
  const candidate = this.sessions.get(message.sessionId);
1274
2343
  if (message.type !== "auth" || typeof message.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(message.token) || !candidate || !matches(message.token, candidate.digest)) fail("AUTH_INVALID", 401);
2344
+ if (peerClientId !== void 0 && candidate.context.clientId !== peerClientId) fail("AUTH_INVALID", 401);
1275
2345
  if (candidate.closed || candidate.used) fail("DESKTOP_SESSION_CLOSED", 409);
1276
2346
  this.check(candidate.context);
1277
2347
  if (origin !== void 0 && (candidate.origin ? origin !== candidate.origin : !allowedOrigins.includes(origin))) fail("ORIGIN_DENIED", 403);
@@ -1375,7 +2445,7 @@ var DesktopSessions = class {
1375
2445
  }
1376
2446
  stop(row, reason) {
1377
2447
  this.forceKeyframes.delete(row);
1378
- if (row.stop) return row.stop;
2448
+ if (row.stop) return row.stopped ? Promise.resolve({ stopConfirmed: true }) : row.stop;
1379
2449
  row.closed = true;
1380
2450
  row.closedAt = Date.now();
1381
2451
  clearTimeout(row.frameTimer);
@@ -1426,7 +2496,7 @@ import { execFile, spawn } from "node:child_process";
1426
2496
  import { promisify } from "node:util";
1427
2497
  import { mkdir as mkdir2, open as open2, readFile as readFile4, lstat as lstat2 } from "node:fs/promises";
1428
2498
  import { isAbsolute, join as join3 } from "node:path";
1429
- import { randomUUID as randomUUID5 } from "node:crypto";
2499
+ import { randomUUID as randomUUID6 } from "node:crypto";
1430
2500
 
1431
2501
  // src/container-supervisor.mjs
1432
2502
  async function runContainerSupervisor() {
@@ -1721,7 +2791,7 @@ var ContainerCommands = class {
1721
2791
  }
1722
2792
  }
1723
2793
  async run({ id: id2, c, rootId, workspace, workspaceIdentity, executable, argv, timeout, validate, validateWorkspace, shouldCancel, artifact }) {
1724
- const name = "dsh-worker-" + sha(canonical({ machine: this.store.machineId, id: id2 })).slice(0, 40), nonce = randomUUID5(), digest2 = sha(canonical({ rootId, workspace, workspaceIdentity, executable, argv, image: this.config.image }));
2794
+ const name = "dsh-worker-" + sha(canonical({ machine: this.store.machineId, id: id2 })).slice(0, 40), nonce = randomUUID6(), digest2 = sha(canonical({ rootId, workspace, workspaceIdentity, executable, argv, image: this.config.image }));
1725
2795
  const deadlineAt = Math.min(c.expiresAt, Date.now() + timeout), supervisorFile = await this.supervisor();
1726
2796
  const args = containerArguments({ name, nonce, machineId: this.store.machineId, workspace, workspaceIdentity, config: this.config, executable, argv, deadlineAt, supervisorFile });
1727
2797
  this.store.db.prepare("INSERT INTO container_workers(operation_id,name,nonce,context,root_id,workspace,input_digest,phase,cid,reason,workspace_identity) VALUES (?,?,?,?,?,?,?,'prepared',NULL,NULL,?)").run(id2, name, nonce, canonical(c), rootId, workspace, digest2, workspaceIdentity);
@@ -2179,7 +3249,7 @@ function reconcileRootsGeneration(db, generation) {
2179
3249
  import { constants as constants3 } from "node:fs";
2180
3250
  import { open as open4, lstat as lstat4, stat as stat4, realpath as realpath3, link, unlink as unlink3 } from "node:fs/promises";
2181
3251
  import path3 from "node:path";
2182
- import { randomBytes as randomBytes2 } from "node:crypto";
3252
+ import { randomBytes as randomBytes3 } from "node:crypto";
2183
3253
  var PINNED_WRITE_MAX_BYTES = 1048576;
2184
3254
  var PINNED_WRITE_MAX_LIMIT_BYTES = 16 * 1024 * 1024;
2185
3255
  var PINNED_WRITE_MAX_PATH_BYTES = 4096;
@@ -2418,7 +3488,7 @@ async function pinnedCreateNewFile(request) {
2418
3488
  try {
2419
3489
  await pinned.verifyHostPath(directoryPath);
2420
3490
  if (await pinned.lstatChild(targetName)) failWrite("TARGET_EXISTS", 409, { target: targetPath });
2421
- tempName = `${PINNED_WRITE_TEMP_PREFIX}${randomBytes2(16).toString("hex")}`;
3491
+ tempName = `${PINNED_WRITE_TEMP_PREFIX}${randomBytes3(16).toString("hex")}`;
2422
3492
  const temp = await open4(pinned.childPath(tempName), TEMP_FLAGS, 384).catch((e) => {
2423
3493
  const code = fsCode(e);
2424
3494
  if (code === "EEXIST") failWrite("PINNED_WRITE_FAILED", 500, { reason: "temp-name-collision" });
@@ -2507,15 +3577,15 @@ async function pinnedCreateNewFile(request) {
2507
3577
  }
2508
3578
 
2509
3579
  // src/cli-task.ts
2510
- import { readFile as readFile7, stat as stat6, writeFile as writeFile4 } from "node:fs/promises";
3580
+ import { readFile as readFile8, stat as stat6, writeFile as writeFile4 } from "node:fs/promises";
2511
3581
  import { isAbsolute as isAbsolute6, join as join7, relative, resolve as resolve2 } from "node:path";
2512
3582
 
2513
3583
  // ../packages/cli-runtime/src/agent-codex-runtime.mjs
2514
- import { randomBytes as randomBytes4 } from "node:crypto";
3584
+ import { randomBytes as randomBytes6 } from "node:crypto";
2515
3585
 
2516
3586
  // ../packages/cli-runtime/src/codex-session-core.mjs
2517
- import { createHash as createHash3 } from "node:crypto";
2518
- import { readFile as readFile6, realpath as realpath6, stat as stat5, mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
3587
+ import { createHash as createHash6 } from "node:crypto";
3588
+ import { readFile as readFile7, realpath as realpath6, stat as stat5, mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
2519
3589
  import { spawn as spawn3 } from "node:child_process";
2520
3590
  import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
2521
3591
 
@@ -2523,11 +3593,128 @@ import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
2523
3593
  import { spawn as spawn2 } from "node:child_process";
2524
3594
  import { EventEmitter as EventEmitter2 } from "node:events";
2525
3595
  import { StringDecoder } from "node:string_decoder";
2526
- import { access as access2, realpath as realpath4 } from "node:fs/promises";
2527
- import { isAbsolute as isAbsolute3, join as join4 } from "node:path";
3596
+ import { access as access2, lstat as lstat5, readFile as readFile6, readdir as readdir2, realpath as realpath4, rmdir, unlink as unlink4 } from "node:fs/promises";
3597
+ import { lstatSync as lstatSync2, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
3598
+ import { randomBytes as randomBytes4 } from "node:crypto";
3599
+ import { tmpdir } from "node:os";
3600
+ import { basename, dirname, isAbsolute as isAbsolute3, join as join4 } from "node:path";
2528
3601
  import { fileURLToPath } from "node:url";
3602
+
3603
+ // ../packages/cli-runtime/src/native-cli-output.mjs
3604
+ import { createHash as createHash4 } from "node:crypto";
3605
+ var FRAME = 262144;
3606
+ var RAW_FRAME = 18 * 1024 * 1024;
3607
+ var IMAGE = 6 * 1024 * 1024;
3608
+ var fail2 = () => {
3609
+ throw new Error("CLI_FRAME_LIMIT");
3610
+ };
3611
+ function receipt(frame, rawBytes) {
3612
+ const results = frame?.type === "user" && frame.message?.content;
3613
+ const toolResult = Array.isArray(results) && results.length > 0 && results.every((block) => block?.type === "tool_result" && typeof block.tool_use_id === "string");
3614
+ if (!toolResult) {
3615
+ if (rawBytes > FRAME) fail2();
3616
+ return frame;
3617
+ }
3618
+ let imageBytes = 0, images = 0, nodes = 0;
3619
+ const visit = (value, depth = 0) => {
3620
+ if (++nodes > 3e4 || depth > 24) fail2();
3621
+ if (!value || typeof value !== "object") return value;
3622
+ if (value.type === "image") {
3623
+ const source = value.source;
3624
+ const data = source?.type === "base64" ? source.data : value.data;
3625
+ const mediaType = source?.type === "base64" ? source.media_type : value.mimeType;
3626
+ if (typeof data !== "string" || data.length > IMAGE / 3 * 4 || !["image/png", "image/jpeg", "image/webp"].includes(mediaType)) fail2();
3627
+ const bytes = Buffer.from(data, "base64");
3628
+ if (!bytes.length || bytes.length > IMAGE || bytes.toString("base64") !== data || ++images > 32 || (imageBytes += bytes.length) > IMAGE * 2) fail2();
3629
+ return { type: "image", mediaType, bytes: bytes.length, sha256: createHash4("sha256").update(bytes).digest("hex") };
3630
+ }
3631
+ if (Array.isArray(value)) return value.map((item) => visit(item, depth + 1));
3632
+ return Object.fromEntries(Object.entries(value).map(([key2, item]) => [key2, visit(item, depth + 1)]));
3633
+ };
3634
+ const result = visit(frame);
3635
+ if (rawBytes > FRAME && !images || Buffer.byteLength(JSON.stringify(result)) > FRAME) fail2();
3636
+ return result;
3637
+ }
3638
+ var NativeCliOutput = class {
3639
+ #buffer = "";
3640
+ #bytes = 0;
3641
+ push(text) {
3642
+ this.#buffer += text;
3643
+ const frames = [];
3644
+ let end;
3645
+ while ((end = this.#buffer.indexOf("\n")) >= 0) {
3646
+ const line = this.#buffer.slice(0, end);
3647
+ this.#buffer = this.#buffer.slice(end + 1);
3648
+ const rawBytes = Buffer.byteLength(line);
3649
+ if (rawBytes > RAW_FRAME) fail2();
3650
+ if (!line.trim()) continue;
3651
+ const frame = receipt(JSON.parse(line), rawBytes);
3652
+ this.#bytes += Buffer.byteLength(JSON.stringify(frame));
3653
+ if (this.#bytes > 8 * 1024 * 1024) fail2();
3654
+ frames.push(frame);
3655
+ }
3656
+ if (Buffer.byteLength(this.#buffer) > RAW_FRAME) fail2();
3657
+ return frames;
3658
+ }
3659
+ };
3660
+
3661
+ // ../packages/cli-runtime/src/native-process.mjs
2529
3662
  var HERE = fileURLToPath(new URL(".", import.meta.url));
2530
3663
  var MAX_FRAME = 262144;
3664
+ var COMPILER_ROOT = "DSH_NATIVE_COMPILER_ROOT";
3665
+ var COMPILER_SCRATCH = "DSH_NATIVE_COMPILER_SCRATCH";
3666
+ var inheritedCompilerRoot = process.platform === "win32" ? process.env[COMPILER_ROOT] : void 0;
3667
+ var hostTemp = process.platform === "win32" ? tmpdir() : null;
3668
+ var compilerRoot = null;
3669
+ var compilerFailure = () => Object.assign(new Error("NATIVE_COMPILER_SCRATCH_UNAVAILABLE"), { code: "NATIVE_COMPILER_SCRATCH_UNAVAILABLE" });
3670
+ var sameDirectory = (info, expected) => info.isDirectory() && !info.isSymbolicLink() && info.dev === expected.dev && info.ino === expected.ino;
3671
+ var rootStamp = (path4) => `dsh-native-compiler-v1:${basename(path4)}
3672
+ `;
3673
+ function compilerDirectory() {
3674
+ try {
3675
+ if (!compilerRoot) {
3676
+ if (inheritedCompilerRoot !== void 0) {
3677
+ if (!isAbsolute3(inheritedCompilerRoot) || inheritedCompilerRoot.length > 140 || !/^dshc-[a-f0-9]{16}$/.test(basename(inheritedCompilerRoot))) throw compilerFailure();
3678
+ const info2 = lstatSync2(inheritedCompilerRoot, { bigint: true });
3679
+ if (!info2.isDirectory() || info2.isSymbolicLink() || realpathSync(inheritedCompilerRoot) !== inheritedCompilerRoot || readFileSync2(join4(inheritedCompilerRoot, ".dsh-root"), "utf8") !== rootStamp(inheritedCompilerRoot)) throw compilerFailure();
3680
+ compilerRoot = { path: inheritedCompilerRoot, info: info2 };
3681
+ } else {
3682
+ const parent = realpathSync(hostTemp);
3683
+ const path5 = join4(parent, `dshc-${randomBytes4(8).toString("hex")}`);
3684
+ if (path5.length > 140) throw compilerFailure();
3685
+ mkdirSync2(path5);
3686
+ writeFileSync2(join4(path5, ".dsh-root"), rootStamp(path5), { flag: "wx" });
3687
+ compilerRoot = { path: path5, info: lstatSync2(path5, { bigint: true }) };
3688
+ }
3689
+ }
3690
+ if (!sameDirectory(lstatSync2(compilerRoot.path, { bigint: true }), compilerRoot.info) || realpathSync(compilerRoot.path) !== compilerRoot.path) throw compilerFailure();
3691
+ const path4 = mkdtempSync(join4(compilerRoot.path, "c-"));
3692
+ const info = lstatSync2(path4, { bigint: true }), owner = randomBytes4(16).toString("hex");
3693
+ writeFileSync2(join4(path4, ".dsh-owner"), owner, { flag: "wx" });
3694
+ const root = compilerRoot;
3695
+ const cleanup = async () => {
3696
+ try {
3697
+ if (dirname(path4) !== root.path || !/^c-[A-Za-z0-9]{6}$/.test(basename(path4))) return false;
3698
+ if (!sameDirectory(await lstat5(root.path, { bigint: true }), root.info) || await realpath4(root.path) !== root.path) return false;
3699
+ if (!sameDirectory(await lstat5(path4, { bigint: true }), info) || await realpath4(path4) !== path4 || await readFile6(join4(path4, ".dsh-owner"), "utf8") !== owner) return false;
3700
+ const names = await readdir2(path4);
3701
+ if (names.length > 128) return false;
3702
+ for (const name of names) {
3703
+ const entry = await lstat5(join4(path4, name));
3704
+ if (!entry.isFile() || entry.isSymbolicLink()) return false;
3705
+ }
3706
+ for (const name of names) await unlink4(join4(path4, name));
3707
+ await rmdir(path4);
3708
+ return true;
3709
+ } catch {
3710
+ return false;
3711
+ }
3712
+ };
3713
+ return { root: root.path, path: path4, cleanup };
3714
+ } catch {
3715
+ throw compilerFailure();
3716
+ }
3717
+ }
2531
3718
  function privateEnvironment(home, source = process.env) {
2532
3719
  const env2 = {
2533
3720
  HOME: home,
@@ -2547,6 +3734,7 @@ function privateEnvironment(home, source = process.env) {
2547
3734
  };
2548
3735
  for (const key2 of ["SystemRoot", "windir", "ComSpec", "LANG", "LC_ALL"]) if (source[key2]) env2[key2] = source[key2];
2549
3736
  env2.PATH = process.platform === "win32" ? join4(source.SystemRoot ?? "C:\\Windows", "System32") : "/usr/bin:/bin";
3737
+ if (process.platform === "win32" && (compilerRoot?.path ?? inheritedCompilerRoot)) env2[COMPILER_ROOT] = compilerRoot?.path ?? inheritedCompilerRoot;
2550
3738
  return env2;
2551
3739
  }
2552
3740
  async function resolveNativeHelper({ python = "/usr/bin/python3" } = {}) {
@@ -2562,25 +3750,26 @@ async function resolveNativeHelper({ python = "/usr/bin/python3" } = {}) {
2562
3750
  function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironment(home) } = {}) {
2563
3751
  const events = new EventEmitter2();
2564
3752
  const args = helper.platform === "win32" ? [...helper.prefix, "-Mode", mode] : [...helper.prefix, mode];
2565
- const child = spawn2(helper.executable, args, { cwd: home, env: env2, windowsHide: true, shell: false, stdio: ["pipe", "pipe", "pipe"] });
2566
- let tail = "", buffer = "", bytes = 0, closed2 = false;
3753
+ const compiler = helper.platform === "win32" ? compilerDirectory() : null;
3754
+ const helperEnv = compiler ? Object.fromEntries(Object.entries(env2).filter(([key2]) => ![COMPILER_ROOT, COMPILER_SCRATCH].includes(key2.toUpperCase()))) : env2;
3755
+ if (compiler) {
3756
+ helperEnv[COMPILER_ROOT] = compiler.root;
3757
+ helperEnv[COMPILER_SCRATCH] = compiler.path;
3758
+ }
3759
+ const child = spawn2(helper.executable, args, { cwd: home, env: helperEnv, windowsHide: true, shell: false, stdio: ["pipe", "pipe", "pipe"] });
3760
+ let tail = "", buffer = "", bytes = 0, closed2 = false, treeStopped = false;
2567
3761
  const decoder = new StringDecoder("utf8"), cliDecoder = new StringDecoder("utf8");
2568
- let cliBuffer = "";
3762
+ const cliOutput = new NativeCliOutput();
2569
3763
  const frame = (value) => {
3764
+ if (mode === "supervise" && value.control === "stopped" && value.treeStopped === true) treeStopped = true;
2570
3765
  if (mode === "supervise" && value.type === "cli-output") {
2571
3766
  if (typeof value.data !== "string" || value.data.length > MAX_FRAME * 2) throw new Error("INVALID_CLI_FRAME");
2572
- cliBuffer += cliDecoder.write(Buffer.from(value.data, "base64"));
2573
- if (Buffer.byteLength(cliBuffer) > MAX_FRAME) throw new Error("CLI_FRAME_LIMIT");
2574
- let end;
2575
- while ((end = cliBuffer.indexOf("\n")) >= 0) {
2576
- const line = cliBuffer.slice(0, end);
2577
- cliBuffer = cliBuffer.slice(end + 1);
2578
- if (line.trim()) events.emit("message", JSON.parse(line));
2579
- }
3767
+ for (const message of cliOutput.push(cliDecoder.write(Buffer.from(value.data, "base64")))) events.emit("message", message);
2580
3768
  } else events.emit(value.control ? "control" : "message", value);
2581
3769
  };
2582
- const exit = new Promise((resolve4) => child.once("close", (code, signal) => {
3770
+ const exit = new Promise((resolve4) => child.once("close", async (code, signal) => {
2583
3771
  closed2 = true;
3772
+ if (compiler && code === 0 && signal === null && (mode !== "supervise" || treeStopped)) await compiler.cleanup();
2584
3773
  events.emit("closed", { code, signal });
2585
3774
  resolve4({ code, signal });
2586
3775
  }));
@@ -2593,7 +3782,8 @@ function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironmen
2593
3782
  child.stdout.on("data", (chunk) => {
2594
3783
  bytes += chunk.length;
2595
3784
  buffer += decoder.write(chunk);
2596
- if (bytes > 8 * 1024 * 1024 || Buffer.byteLength(buffer) > MAX_FRAME) {
3785
+ const boundedStream = mode === "client" || mode === "server" || mode === "supervise";
3786
+ if (!boundedStream && bytes > 8 * 1024 * 1024 || Buffer.byteLength(buffer) > MAX_FRAME) {
2597
3787
  events.emit("failure", new Error("OUTPUT_LIMIT"));
2598
3788
  child.stdin.end();
2599
3789
  return;
@@ -2690,9 +3880,9 @@ async function protectProfile(helper, home) {
2690
3880
  }
2691
3881
 
2692
3882
  // ../packages/capability-broker/src/cli-bootstrap.mjs
2693
- import { generateKeyPairSync, sign, randomBytes as randomBytes3 } from "node:crypto";
2694
- import { mkdir as mkdir4, writeFile as writeFile2, mkdtemp, chmod, lstat as lstat5, realpath as realpath5, rmdir } from "node:fs/promises";
2695
- import { tmpdir } from "node:os";
3883
+ import { generateKeyPairSync, sign as sign2, randomBytes as randomBytes5 } from "node:crypto";
3884
+ import { mkdir as mkdir4, writeFile as writeFile2, mkdtemp, chmod, lstat as lstat6, realpath as realpath5, rmdir as rmdir2 } from "node:fs/promises";
3885
+ import { tmpdir as tmpdir2 } from "node:os";
2696
3886
  import { isAbsolute as isAbsolute4, join as join5 } from "node:path";
2697
3887
  import { performance as performance2 } from "node:perf_hooks";
2698
3888
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -2716,14 +3906,65 @@ var PolicyError = class extends Error {
2716
3906
  }
2717
3907
  };
2718
3908
 
3909
+ // ../packages/capability-broker/src/cli-bridge-response.mjs
3910
+ import { createHash as createHash5 } from "node:crypto";
3911
+ var CHUNK_BYTES = 128 * 1024;
3912
+ var MAX_RESPONSE = 9 * 1024 * 1024;
3913
+ var MAX_BUFFERED = 16 * 1024 * 1024;
3914
+ var fail3 = () => {
3915
+ throw new Error("INVALID_BRIDGE_RESPONSE");
3916
+ };
3917
+ var sha2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
3918
+ function* bridgeResponseFrames(id2, value) {
3919
+ let bytes = Buffer.from(JSON.stringify({ type: "response", id: id2, ...value }));
3920
+ if (bytes.length > MAX_RESPONSE) bytes = Buffer.from(JSON.stringify({ type: "response", id: id2, ok: false, code: "OUTPUT_LIMIT" }));
3921
+ if (bytes.length <= CHUNK_BYTES) {
3922
+ yield JSON.parse(bytes.toString("utf8"));
3923
+ return;
3924
+ }
3925
+ yield { type: "response-start", id: id2, bytes: bytes.length };
3926
+ for (let offset = 0, index = 0; offset < bytes.length; offset += CHUNK_BYTES, index++) {
3927
+ yield { type: "response-chunk", id: id2, index, data: bytes.subarray(offset, offset + CHUNK_BYTES).toString("base64") };
3928
+ }
3929
+ yield { type: "response-end", id: id2, sha256: sha2(bytes) };
3930
+ }
3931
+ function normalizeBridgeMcpResult(value) {
3932
+ if (!value || !Array.isArray(value.content) || value.content.length > 32 || Object.keys(value).some((key2) => !["content", "isError"].includes(key2)) || value.isError !== void 0 && typeof value.isError !== "boolean") fail3();
3933
+ for (const block of value.content) {
3934
+ if (block?.type === "text") {
3935
+ if (Object.keys(block).some((key2) => !["type", "text"].includes(key2)) || typeof block.text !== "string") fail3();
3936
+ } else if (block?.type === "image") {
3937
+ if (Object.keys(block).some((key2) => !["type", "data", "mimeType"].includes(key2)) || !["image/png", "image/jpeg", "image/webp"].includes(block.mimeType) || typeof block.data !== "string" || block.data.length > 8 * 1024 * 1024) fail3();
3938
+ const bytes = Buffer.from(block.data, "base64");
3939
+ if (!bytes.length || bytes.toString("base64") !== block.data || bytes.length > 6 * 1024 * 1024) fail3();
3940
+ } else fail3();
3941
+ }
3942
+ return value;
3943
+ }
3944
+
2719
3945
  // ../packages/capability-broker/src/cli-bootstrap.mjs
2720
3946
  var BRIDGE = fileURLToPath2(new URL("./cli-stdio-bridge.mjs", import.meta.url));
2721
3947
  var MAX_BYTES = 262144;
2722
- async function createCliBootstrap({ authority, subject, binding, tools, handlers, home, ttlMs = 3e4, helper }) {
3948
+ var REQUEST_KEYS = ["type", "id", "op", "name", "args"];
3949
+ var capabilityOf = (handler) => {
3950
+ const value = handler.capability ?? handler.definition.name;
3951
+ if (typeof value !== "string" || !value || Buffer.byteLength(value) > 256) throw new TypeError("INVALID_HANDLER_CAPABILITY");
3952
+ return value;
3953
+ };
3954
+ async function createCliBootstrap({ authority, subject, binding, tools, handlers, home, ttlMs = 3e4, helper, contextFor = null }) {
2723
3955
  if (!isAbsolute4(home)) throw new TypeError("An isolated absolute home is required");
3956
+ if (contextFor !== null && typeof contextFor !== "function") throw new TypeError("INVALID_HOST_CONTEXT_CALLBACK");
2724
3957
  const ownedBinding = Object.freeze({ ...binding });
2725
3958
  const registry = new Map(handlers.map((handler) => [handler.definition.name, handler]));
2726
3959
  if (registry.size !== handlers.length || tools.some((name) => !registry.has(name))) throw new TypeError("Unknown or duplicate bridge handler");
3960
+ const selected = new Map(tools.map((name) => [name, { handler: registry.get(name), capability: capabilityOf(registry.get(name)) }]));
3961
+ const leaseTools = [...new Set([...selected.values()].map((entry) => entry.capability))];
3962
+ const byCapability = /* @__PURE__ */ new Map();
3963
+ for (const entry of selected.values()) {
3964
+ const sharing = byCapability.get(entry.capability);
3965
+ if (sharing) sharing.push(entry.handler);
3966
+ else byCapability.set(entry.capability, [entry.handler]);
3967
+ }
2727
3968
  if (!Number.isSafeInteger(ttlMs) || ttlMs < 1e3 || ttlMs > 72e5 || typeof authority.renew !== "function") throw new TypeError("BOUNDED_RENEWABLE_LEASE_REQUIRED");
2728
3969
  const leaseTtlMs = Math.min(3e4, ttlMs);
2729
3970
  const startedAt = performance2.now();
@@ -2732,45 +3973,62 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2732
3973
  const publicSpki = publicKey.export({ type: "spki", format: "der" }).toString("base64url");
2733
3974
  await mkdir4(home, { recursive: true, mode: 448 });
2734
3975
  await mkdir4(join5(home, "tmp"), { recursive: true, mode: 448 });
2735
- const descriptorPath2 = join5(home, `bridge-${randomBytes3(12).toString("hex")}.json`);
3976
+ const descriptorPath2 = join5(home, `bridge-${randomBytes5(12).toString("hex")}.json`);
2736
3977
  let socketDirectory;
2737
- let endpoint = process.platform === "win32" ? `dsh-cli-${randomBytes3(18).toString("hex")}` : join5(home, `p-${randomBytes3(6).toString("hex")}.sock`);
3978
+ let endpoint = process.platform === "win32" ? `dsh-cli-${randomBytes5(18).toString("hex")}` : join5(home, `p-${randomBytes5(6).toString("hex")}.sock`);
2738
3979
  if (process.platform === "linux" && Buffer.byteLength(endpoint) > 103) {
2739
- let parent = await realpath5(tmpdir());
3980
+ let parent = await realpath5(tmpdir2());
2740
3981
  if (Buffer.byteLength(join5(parent, "dsh-cli-XXXXXX", "p.sock")) > 103) parent = await realpath5("/tmp");
2741
3982
  socketDirectory = await mkdtemp(join5(parent, "dsh-cli-"));
2742
3983
  await chmod(socketDirectory, 448);
2743
- const stat8 = await lstat5(socketDirectory);
3984
+ const stat8 = await lstat6(socketDirectory);
2744
3985
  if (!stat8.isDirectory() || stat8.isSymbolicLink() || stat8.uid !== process.getuid() || (stat8.mode & 63) !== 0) {
2745
- await rmdir(socketDirectory);
3986
+ await rmdir2(socketDirectory);
2746
3987
  throw new Error("PRIVATE_SOCKET_DIRECTORY_REQUIRED");
2747
3988
  }
2748
3989
  endpoint = join5(socketDirectory, "p.sock");
2749
3990
  if (Buffer.byteLength(endpoint) > 103) {
2750
- await rmdir(socketDirectory);
3991
+ await rmdir2(socketDirectory);
2751
3992
  throw new Error("PRIVATE_SOCKET_PATH_TOO_LONG");
2752
3993
  }
2753
3994
  }
2754
3995
  let lease;
2755
3996
  try {
2756
- lease = authority.issue(subject, { binding: ownedBinding, tools, ttlMs: leaseTtlMs });
3997
+ lease = authority.issue(subject, { binding: ownedBinding, tools: leaseTools, ttlMs: leaseTtlMs });
2757
3998
  } catch (error2) {
2758
- if (socketDirectory) await rmdir(socketDirectory);
3999
+ if (socketDirectory) await rmdir2(socketDirectory);
2759
4000
  throw error2;
2760
4001
  }
2761
4002
  let relay, closed2 = false, bound = false, connected = false, closing;
2762
- let accepted, rejected = 0, requests = 0, replies = 0;
4003
+ let accepted, rejected = 0, requests = 0, replies = 0, highestRequestId = 0;
2763
4004
  const active = /* @__PURE__ */ new Map();
2764
4005
  const release = () => {
2765
4006
  authority.release(lease.token);
2766
4007
  for (const controller of active.values()) controller.abort(new Error("BRIDGE_CLOSED"));
2767
4008
  };
2768
- const reply = (id2, value) => {
4009
+ const reply = async (id2, value, signal) => {
2769
4010
  if (!relay || relay.closed) return;
2770
4011
  replies++;
2771
- const wire = { type: "response", id: id2, ...value };
2772
- void relay.send(Buffer.byteLength(JSON.stringify(wire)) > MAX_BYTES ? { type: "response", id: id2, ok: false, code: "OUTPUT_LIMIT" } : wire).catch(() => {
2773
- });
4012
+ for (const frame of bridgeResponseFrames(id2, value)) {
4013
+ signal?.throwIfAborted();
4014
+ await relay.send(frame);
4015
+ }
4016
+ };
4017
+ const hostContext = (op, name, id2) => {
4018
+ if (!contextFor) return null;
4019
+ let value;
4020
+ try {
4021
+ value = contextFor({ op, name, id: id2 });
4022
+ } catch (error2) {
4023
+ throw error2 instanceof PolicyError ? error2 : new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-context" });
4024
+ }
4025
+ if (!value || typeof value !== "object" || typeof value.then === "function") throw new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-context" });
4026
+ return value;
4027
+ };
4028
+ const requestSignal = (controller, context) => {
4029
+ const signal = context?.signal ?? null;
4030
+ if (signal !== null && !(signal instanceof AbortSignal)) throw new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-signal" });
4031
+ return signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
2774
4032
  };
2775
4033
  async function request(message) {
2776
4034
  requests++;
@@ -2781,28 +4039,45 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2781
4039
  }
2782
4040
  if (message.type !== "request") return;
2783
4041
  const id2 = message.id;
4042
+ if (id2 <= highestRequestId) {
4043
+ void reply(id2, { ok: false, code: "DUPLICATE_REQUEST" }).catch(() => {
4044
+ });
4045
+ return;
4046
+ }
4047
+ highestRequestId = id2;
2784
4048
  try {
2785
4049
  if (closed2) throw new PolicyError("BRIDGE_CLOSED");
2786
- if (Buffer.byteLength(JSON.stringify(message)) > MAX_BYTES || Object.keys(message).some((key2) => !["type", "id", "op", "name", "args"].includes(key2))) throw new PolicyError("INVALID_REQUEST");
2787
- if (active.has(id2) || active.size >= 16) throw new PolicyError("BUSY");
4050
+ if (Buffer.byteLength(JSON.stringify(message)) > MAX_BYTES || Object.keys(message).some((key2) => !REQUEST_KEYS.includes(key2))) throw new PolicyError("INVALID_REQUEST");
4051
+ if (active.size >= 16) throw new PolicyError("BUSY");
2788
4052
  if (message.op === "list") {
2789
- reply(id2, { ok: true, value: authority.list(lease.token, ownedBinding).map((name) => registry.get(name).definition) });
4053
+ const context2 = hostContext("list", null, id2);
4054
+ requestSignal(new AbortController(), context2).throwIfAborted();
4055
+ await reply(id2, { ok: true, value: authority.list(lease.token, ownedBinding).flatMap((capability) => byCapability.get(capability) ?? []).map((handler) => handler.definition) });
2790
4056
  return;
2791
4057
  }
2792
4058
  if (message.op !== "call") throw new PolicyError("INVALID_REQUEST");
4059
+ const entry = selected.get(message.name);
4060
+ if (!entry) throw new PolicyError("TOOL_DENIED");
4061
+ const context = hostContext("call", message.name, id2);
2793
4062
  const controller = new AbortController();
4063
+ const signal = requestSignal(controller, context);
2794
4064
  active.set(id2, controller);
2795
4065
  try {
2796
- const value = await authority.run(lease.token, ownedBinding, message.name, ({ signal }) => {
2797
- const handler = registry.get(message.name);
2798
- return handler.execute(handler.parse(message.args), { signal, binding: ownedBinding });
2799
- }, { signal: controller.signal });
2800
- reply(id2, { ok: true, value });
4066
+ signal.throwIfAborted();
4067
+ const value = await authority.run(lease.token, ownedBinding, entry.capability, ({ signal: authoritySignal }) => {
4068
+ authoritySignal.throwIfAborted();
4069
+ return entry.handler.execute(entry.handler.parse(message.args), { signal: authoritySignal, binding: ownedBinding, context });
4070
+ }, { signal });
4071
+ signal.throwIfAborted();
4072
+ const mcpResult = entry.handler.render ? normalizeBridgeMcpResult(await entry.handler.render(value)) : null;
4073
+ signal.throwIfAborted();
4074
+ await reply(id2, { ok: true, ...mcpResult ? { mcpResult } : { value } }, signal);
2801
4075
  } finally {
2802
4076
  active.delete(id2);
2803
4077
  }
2804
4078
  } catch (error2) {
2805
- reply(id2, { ok: false, code: error2 instanceof PolicyError ? error2.code : "HANDLER_REFUSED" });
4079
+ await reply(id2, { ok: false, code: error2 instanceof PolicyError ? error2.code : "HANDLER_REFUSED" }).catch(() => {
4080
+ });
2806
4081
  }
2807
4082
  }
2808
4083
  const close = () => {
@@ -2823,7 +4098,7 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2823
4098
  await relay.exit;
2824
4099
  }
2825
4100
  }
2826
- if (socketDirectory) await rmdir(socketDirectory).catch(() => {
4101
+ if (socketDirectory) await rmdir2(socketDirectory).catch(() => {
2827
4102
  });
2828
4103
  })();
2829
4104
  return closing;
@@ -2884,7 +4159,7 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2884
4159
  hostPid: process.pid,
2885
4160
  binding: ownedBinding
2886
4161
  });
2887
- const descriptor = JSON.stringify({ payload, signature: sign(null, Buffer.from(payload), privateKey).toString("base64url") });
4162
+ const descriptor = JSON.stringify({ payload, signature: sign2(null, Buffer.from(payload), privateKey).toString("base64url") });
2888
4163
  await writeFile2(descriptorPath2, descriptor, { flag: "wx", mode: 384 });
2889
4164
  };
2890
4165
  return Object.freeze({
@@ -3020,14 +4295,14 @@ function assertEffectiveProfile(result, { requestedModel, bridge, home, tools, s
3020
4295
  }
3021
4296
 
3022
4297
  // ../packages/cli-runtime/src/codex-session-core.mjs
3023
- var digest = (value) => createHash3("sha256").update(value).digest("hex");
4298
+ var digest = (value) => createHash6("sha256").update(value).digest("hex");
3024
4299
  var id = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
3025
4300
  var validText = (value) => typeof value === "string" && value.trim() && !value.includes("\0") && Buffer.byteLength(value) <= 64e3;
3026
- async function pinCodexExecutable({ executable, sha256, version }, home) {
3027
- if (!isAbsolute5(executable) || !/^[a-f0-9]{64}$/.test(sha256) || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new TypeError("PINNED_NATIVE_ENTRY_REQUIRED");
4301
+ async function pinCodexExecutable({ executable, sha256: sha2562, version }, home) {
4302
+ if (!isAbsolute5(executable) || !/^[a-f0-9]{64}$/.test(sha2562) || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new TypeError("PINNED_NATIVE_ENTRY_REQUIRED");
3028
4303
  const path4 = await realpath6(executable);
3029
4304
  if (!(await stat5(path4)).isFile() || process.platform === "win32" && !path4.toLowerCase().endsWith(".exe")) throw new Error("NATIVE_ENTRY_REQUIRED");
3030
- if (digest(await readFile6(path4)) !== sha256) throw new Error("CLI_ENTRY_CHANGED");
4305
+ if (digest(await readFile7(path4)) !== sha2562) throw new Error("CLI_ENTRY_CHANGED");
3031
4306
  const reported = await new Promise((resolve4, reject) => {
3032
4307
  const child = spawn3(path4, ["--version"], { cwd: home, env: privateEnvironment(home), windowsHide: true, shell: false, stdio: ["ignore", "pipe", "pipe"] });
3033
4308
  let output = "", error2 = "";
@@ -3050,7 +4325,7 @@ async function pinCodexExecutable({ executable, sha256, version }, home) {
3050
4325
  });
3051
4326
  });
3052
4327
  if (reported !== `codex-cli ${version}`) throw new Error("CLI_VERSION_MISMATCH");
3053
- return Object.freeze({ path: path4, sha256, version });
4328
+ return Object.freeze({ path: path4, sha256: sha2562, version });
3054
4329
  }
3055
4330
  async function createCodexSessionCore({
3056
4331
  authority,
@@ -3095,6 +4370,7 @@ async function createCodexSessionCore({
3095
4370
  await protectProfile(probe.helper, home);
3096
4371
  const entry = await pinCodexExecutable(suppliedEntry, home);
3097
4372
  const bridge = await createCliBootstrap({ authority, subject, binding, tools, handlers, home: join6(home, "b"), ttlMs: deadlineMs, helper: probe.helper });
4373
+ const compilerRoot2 = privateEnvironment(home).DSH_NATIVE_COMPILER_ROOT;
3098
4374
  const config = [
3099
4375
  `model = ${JSON.stringify(requestedModel)}`,
3100
4376
  'model_provider = "openai"',
@@ -3124,15 +4400,16 @@ async function createCodexSessionCore({
3124
4400
  `HOME = ${JSON.stringify(home)}`,
3125
4401
  `USERPROFILE = ${JSON.stringify(home)}`,
3126
4402
  `TEMP = ${JSON.stringify(join6(home, "tmp"))}`,
3127
- `TMP = ${JSON.stringify(join6(home, "tmp"))}`
4403
+ `TMP = ${JSON.stringify(join6(home, "tmp"))}`,
4404
+ ...compilerRoot2 ? [`DSH_NATIVE_COMPILER_ROOT = ${JSON.stringify(compilerRoot2)}`] : []
3128
4405
  ].join("\n") + "\n";
3129
4406
  await writeFile3(join6(home, "config.toml"), config, { flag: "wx", mode: 384 });
3130
4407
  if (provisionCredentials) await provisionCredentials(home);
3131
- if (digest(await readFile6(join6(home, "config.toml"))) !== digest(Buffer.from(config))) {
4408
+ if (digest(await readFile7(join6(home, "config.toml"))) !== digest(Buffer.from(config))) {
3132
4409
  await bridge.close();
3133
4410
  throw new Error("CLI_PROFILE_CHANGED_DURING_PROVISIONING");
3134
4411
  }
3135
- if (digest(await readFile6(entry.path)) !== entry.sha256) {
4412
+ if (digest(await readFile7(entry.path)) !== entry.sha256) {
3136
4413
  await bridge.close();
3137
4414
  throw new Error("CLI_ENTRY_CHANGED");
3138
4415
  }
@@ -3402,7 +4679,7 @@ function agentAuthority(context, validate, requestedModel, catalogProvider, cata
3402
4679
  issue(_subject, { binding, tools, ttlMs }) {
3403
4680
  validate(context);
3404
4681
  if (binding?.location !== "remote" || !Array.isArray(tools) || tools.length || !Number.isSafeInteger(ttlMs) || ttlMs < 1 || ttlMs > 3e5) throw new Error("AGENT_CLI_LEASE_DENIED");
3405
- const token = randomBytes4(32).toString("base64url");
4682
+ const token = randomBytes6(32).toString("base64url");
3406
4683
  leases.set(token, { binding: bindingKey(binding), tools: new Set(tools), expiresAt: Math.min(Date.now() + ttlMs, context.expiresAt) });
3407
4684
  return { token };
3408
4685
  },
@@ -3515,7 +4792,7 @@ var CliTasks = class {
3515
4792
  }
3516
4793
  async provision(profile, c, home) {
3517
4794
  const reference = profile.bots[c.botId].credentialRef;
3518
- const bytes = await readFile7(reference);
4795
+ const bytes = await readFile8(reference);
3519
4796
  if (bytes.length > 65536) fail("CLI_CREDENTIAL_PROVISION_FAILED");
3520
4797
  await writeFile4(join7(home, "auth.json"), bytes, { flag: "wx", mode: 384 });
3521
4798
  }
@@ -3611,19 +4888,19 @@ var CliTasks = class {
3611
4888
  if (identity) await identity.pin.close();
3612
4889
  hooks.clearProcess();
3613
4890
  }
3614
- const base = session?.snapshot(), receipt = identity && base ? this.remoteReceipt(identity, base) : null, stopConfirmed = stop?.treeStopped === true;
3615
- if (receipt && stopConfirmed && controller.signal.aborted && !["completed", "failed"].includes(receipt.terminal)) receipt.terminal = "interrupted";
3616
- const terminal2 = receipt?.terminal === "completed" ? "succeeded" : receipt?.terminal === "failed" ? "failed" : receipt?.terminal === "interrupted" ? "cancelled" : "unknown";
4891
+ const base = session?.snapshot(), receipt2 = identity && base ? this.remoteReceipt(identity, base) : null, stopConfirmed = stop?.treeStopped === true;
4892
+ if (receipt2 && stopConfirmed && controller.signal.aborted && !["completed", "failed"].includes(receipt2.terminal)) receipt2.terminal = "interrupted";
4893
+ const terminal2 = receipt2?.terminal === "completed" ? "succeeded" : receipt2?.terminal === "failed" ? "failed" : receipt2?.terminal === "interrupted" ? "cancelled" : "unknown";
3617
4894
  const status = !stopConfirmed ? "unknown" : error2 && terminal2 === "succeeded" ? "unknown" : terminal2;
3618
4895
  const code = status === "unknown" ? "OUTCOME_UNKNOWN" : status === "cancelled" ? "CANCELLED" : status === "failed" ? error2?.code ?? "CLI_TASK_FAILED" : null;
3619
4896
  const result = {
3620
4897
  kind: "remote-cli",
3621
4898
  schemaVersion: 1,
3622
- receipt,
4899
+ receipt: receipt2,
3623
4900
  stopConfirmed,
3624
4901
  stopEvidence: stop ?? { treeStopped: false, status: "unconfirmed" },
3625
4902
  exitCode: Number.isSafeInteger(stop?.exitCode) ? stop.exitCode : null,
3626
- ...receipt?.cliTurnRequestsDispatched === 0 ? { notStarted: true, possiblyApplied: false } : {}
4903
+ ...receipt2?.cliTurnRequestsDispatched === 0 ? { notStarted: true, possiblyApplied: false } : {}
3627
4904
  };
3628
4905
  return { status, result, error: code };
3629
4906
  }
@@ -3848,7 +5125,7 @@ var Agent = class {
3848
5125
  if (data.length > 8 * 1024 * 1024) fail("ARTIFACT_LIMIT", 413);
3849
5126
  const usage = await this.evictArtifacts(data.length);
3850
5127
  if (usage + data.length > ARTIFACT_QUOTA_BYTES) fail("ARTIFACT_QUOTA", 507);
3851
- const id2 = `artifact-${randomUUID6()}`;
5128
+ const id2 = `artifact-${randomUUID7()}`;
3852
5129
  await mkdir6(join8(this.config.dataDir, "artifacts"), { recursive: true, mode: 448 });
3853
5130
  await writeFile5(join8(this.config.dataDir, "artifacts", id2), data, { flag: "wx", mode: 384 });
3854
5131
  this.store.db.prepare("INSERT INTO artifacts VALUES (?,?,?,?,?)").run(id2, canonical(c), sha(data), data.length, media);
@@ -3862,7 +5139,7 @@ var Agent = class {
3862
5139
  if (usage + needed <= ARTIFACT_QUOTA_BYTES) break;
3863
5140
  this.store.db.prepare("DELETE FROM artifacts WHERE id=?").run(row.id);
3864
5141
  usage -= row.size;
3865
- await unlink4(join8(this.config.dataDir, "artifacts", row.id)).catch(() => {
5142
+ await unlink5(join8(this.config.dataDir, "artifacts", row.id)).catch(() => {
3866
5143
  });
3867
5144
  }
3868
5145
  return usage;
@@ -3885,7 +5162,7 @@ var Agent = class {
3885
5162
  if (!row) fail("ARTIFACT_NOT_FOUND", 404);
3886
5163
  this.store.own(row.context, c);
3887
5164
  if (name === "artifact_stat") return { artifactId: row.id, sha256: row.hash, size: row.size, mediaType: row.media };
3888
- const offset = integer(a.offset, 0, Number.MAX_SAFE_INTEGER, 0), length = integer(a.length, 1, 1048576, 65536), data = await readFile8(join8(this.config.dataDir, "artifacts", row.id));
5165
+ const offset = integer(a.offset, 0, Number.MAX_SAFE_INTEGER, 0), length = integer(a.length, 1, 1048576, 65536), data = await readFile9(join8(this.config.dataDir, "artifacts", row.id));
3889
5166
  if (sha(data) !== row.hash) fail("ARTIFACT_CORRUPT");
3890
5167
  return { encoding: "base64", content: data.subarray(offset, offset + length).toString("base64"), eof: offset + length >= row.size };
3891
5168
  }
@@ -3971,9 +5248,9 @@ var Agent = class {
3971
5248
  if (pin) await pin.verify();
3972
5249
  }
3973
5250
  async legacyWrite(a, c, signal, target) {
3974
- const temp = join8(dirname(target), `.dsh-write-${randomUUID6()}`);
5251
+ const temp = join8(dirname2(target), `.dsh-write-${randomUUID7()}`);
3975
5252
  try {
3976
- const parent = await stat7(dirname(target));
5253
+ const parent = await stat7(dirname2(target));
3977
5254
  let previous = null;
3978
5255
  try {
3979
5256
  previous = (await this.roots.read(a.rootId, a.relativePath, 0, 1048577)).data;
@@ -3987,7 +5264,7 @@ var Agent = class {
3987
5264
  if (signal?.aborted) throw Object.assign(new AgentError("WRITE_CANCELLED", 409), { possiblyApplied: false });
3988
5265
  this.store.validate(c);
3989
5266
  await this.roots.resolve(a.rootId, a.relativePath, true);
3990
- const now = await stat7(dirname(target));
5267
+ const now = await stat7(dirname2(target));
3991
5268
  if (now.ino !== parent.ino || now.dev !== parent.dev) fail("FILE_CHANGED", 409);
3992
5269
  let current = null;
3993
5270
  try {
@@ -4000,7 +5277,7 @@ var Agent = class {
4000
5277
  await rename(temp, target);
4001
5278
  return { sha256: sha(data), size: data.length };
4002
5279
  } finally {
4003
- await unlink4(temp).catch(() => {
5280
+ await unlink5(temp).catch(() => {
4004
5281
  });
4005
5282
  }
4006
5283
  }
@@ -4110,7 +5387,7 @@ var Agent = class {
4110
5387
  await this.roots.assertPublicTree(source);
4111
5388
  if (await this.git(source, ["status", "--porcelain"])) fail("WORKSPACE_DIRTY", 409);
4112
5389
  const head = await this.git(source, ["rev-parse", "HEAD"]);
4113
- const id2 = `worktree-${randomUUID6()}`, relative2 = a.worktreeRelativePath;
5390
+ const id2 = `worktree-${randomUUID7()}`, relative2 = a.worktreeRelativePath;
4114
5391
  if (typeof relative2 !== "string") fail("INVALID_ARGUMENT");
4115
5392
  const target = await this.roots.resolve(a.rootId, relative2, true);
4116
5393
  if (target === source || this.roots.inside(source, target)) fail("INVALID_WORKTREE_PATH");
@@ -4131,7 +5408,7 @@ var Agent = class {
4131
5408
  const patch = await this.git(row.path, ["diff", "--binary", row.head]);
4132
5409
  if (name === "worktree_diff") return { sourceHead: row.head, ...await this.artifact(Buffer.from(patch + "\n"), c, "text/x-diff") };
4133
5410
  if (await this.git(row.path, ["rev-parse", "HEAD"]) !== row.head || await this.git(row.source, ["rev-parse", "HEAD"]) !== row.head || await this.git(row.source, ["status", "--porcelain"])) return { status: "integration-pending", reason: "SOURCE_CHANGED", worktreeId: a.worktreeId };
4134
- const patchFile = join8(this.config.dataDir, `patch-${randomUUID6()}`);
5411
+ const patchFile = join8(this.config.dataDir, `patch-${randomUUID7()}`);
4135
5412
  await writeFile5(patchFile, patch + "\n", { mode: 384 });
4136
5413
  try {
4137
5414
  if (!patch) return { status: "no-changes" };
@@ -4143,7 +5420,7 @@ var Agent = class {
4143
5420
  if (e instanceof AgentError) throw e;
4144
5421
  return { status: "integration-pending", reason: "PATCH_CONFLICT", worktreeId: a.worktreeId };
4145
5422
  } finally {
4146
- await unlink4(patchFile);
5423
+ await unlink5(patchFile);
4147
5424
  }
4148
5425
  }
4149
5426
  async close() {
@@ -4161,9 +5438,9 @@ var Agent = class {
4161
5438
  };
4162
5439
 
4163
5440
  // src/transport.ts
4164
- import http from "node:http";
5441
+ import http2 from "node:http";
4165
5442
  import https from "node:https";
4166
- import { readFileSync as readFileSync2 } from "node:fs";
5443
+ import { readFileSync as readFileSync3 } from "node:fs";
4167
5444
  import { networkInterfaces } from "node:os";
4168
5445
  import { isIP } from "node:net";
4169
5446
  import { Server, WebStandardStreamableHTTPServerTransport, SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/server";
@@ -4234,7 +5511,133 @@ var AuthThrottle = class {
4234
5511
  };
4235
5512
 
4236
5513
  // src/transport.ts
5514
+ import { WebSocketServer as WebSocketServer2 } from "ws";
5515
+
5516
+ // src/e2e-server.ts
5517
+ import http from "node:http";
5518
+ import { duplexPair } from "node:stream";
4237
5519
  import { WebSocketServer } from "ws";
5520
+ function secureTransport(pairing, handler, upgrade) {
5521
+ const sockets = new WebSocketServer({
5522
+ noServer: true,
5523
+ maxPayload: 65535,
5524
+ perMessageDeflate: false,
5525
+ handleProtocols: (protocols) => protocols.has(DIRECT_PROTOCOL) ? DIRECT_PROTOCOL : false
5526
+ });
5527
+ const sessions = /* @__PURE__ */ new Set(), attempts = /* @__PURE__ */ new Map();
5528
+ let windowStart = 0, windowCount = 0, closing = false;
5529
+ const sweep = setInterval(() => {
5530
+ for (const session of sessions) if (session.peer && !pairing.validPeer(session.peer)) session.remove();
5531
+ }, 500);
5532
+ sweep.unref();
5533
+ return {
5534
+ accept(req, socket, head) {
5535
+ if (closing || sockets.clients.size >= 16) fail("E2E_CONNECTION_LIMIT", 429);
5536
+ if (req.headers.origin !== void 0 || req.headers["sec-websocket-protocol"] !== DIRECT_PROTOCOL) fail("E2E_CLIENT_REQUIRED", 403);
5537
+ const now = Date.now(), source = req.socket.remoteAddress ?? "unknown";
5538
+ if (now - windowStart >= 6e4 || now < windowStart) {
5539
+ windowStart = now;
5540
+ windowCount = 0;
5541
+ }
5542
+ const previous = attempts.get(source), row = previous && now - previous.start < 6e4 && now >= previous.start ? previous : { start: now, count: 0 };
5543
+ if (++row.count > 30 || ++windowCount > 120) fail("E2E_CONNECTION_RATE_LIMIT", 429);
5544
+ attempts.delete(source);
5545
+ attempts.set(source, row);
5546
+ while (attempts.size > 256) attempts.delete(attempts.keys().next().value);
5547
+ sockets.handleUpgrade(req, socket, head, (ws) => {
5548
+ sockets.emit("connection", ws, req);
5549
+ const streams = /* @__PURE__ */ new Set();
5550
+ let session;
5551
+ const origin = http.createServer((request, response) => {
5552
+ if (!session.peer || !pairing.validPeer(session.peer)) {
5553
+ response.writeHead(403);
5554
+ response.end();
5555
+ return;
5556
+ }
5557
+ handler(request, response, session.peer);
5558
+ });
5559
+ origin.requestTimeout = 3e4;
5560
+ origin.headersTimeout = 1e4;
5561
+ origin.on("upgrade", (request, connection, bytes) => {
5562
+ if (!session.peer || !pairing.validPeer(session.peer)) return connection.destroy();
5563
+ upgrade(request, connection, bytes, session.peer);
5564
+ });
5565
+ const transport = {
5566
+ send: (message) => {
5567
+ if (ws.readyState === 1) ws.send(message);
5568
+ },
5569
+ close: (code) => {
5570
+ if (ws.readyState < 2) ws.close(code === 1e3 || code >= 3e3 && code <= 4999 ? code : 1e3);
5571
+ },
5572
+ get bufferedAmount() {
5573
+ return ws.bufferedAmount;
5574
+ },
5575
+ onceDrained(listener) {
5576
+ const check = () => {
5577
+ if (ws.readyState !== 1 || ws.bufferedAmount <= SEND_BUFFER_HIGH / 4) listener();
5578
+ else {
5579
+ const timer = setTimeout(check, 5);
5580
+ timer.unref();
5581
+ }
5582
+ };
5583
+ check();
5584
+ }
5585
+ };
5586
+ session = E2ESession.computer({
5587
+ transport,
5588
+ transportByte: TRANSPORT_DIRECT,
5589
+ staticKey: pairing.transportKey,
5590
+ candidates: () => pairing.candidates(),
5591
+ async connectOrigin() {
5592
+ if (!session.peer || !pairing.validPeer(session.peer)) throw new Error("Peer removed");
5593
+ const [client, server] = duplexPair();
5594
+ Object.assign(server, { setTimeout() {
5595
+ return server;
5596
+ }, setNoDelay() {
5597
+ return server;
5598
+ }, setKeepAlive() {
5599
+ return server;
5600
+ }, remoteAddress: source });
5601
+ client.on("error", () => {
5602
+ });
5603
+ server.on("error", () => {
5604
+ });
5605
+ client.once("close", () => server.destroy());
5606
+ server.once("close", () => client.destroy());
5607
+ streams.add(server);
5608
+ server.once("close", () => streams.delete(server));
5609
+ origin.emit("connection", server);
5610
+ return client;
5611
+ }
5612
+ });
5613
+ sessions.add(session);
5614
+ ws.on("message", (data, binary) => {
5615
+ try {
5616
+ session.receive(Buffer.from(data), binary);
5617
+ } catch {
5618
+ session.close(4e3);
5619
+ }
5620
+ });
5621
+ ws.on("error", () => session.close(4e3));
5622
+ ws.once("close", (code) => session.transportClosed(code));
5623
+ void session.closed.then(() => {
5624
+ sessions.delete(session);
5625
+ for (const stream of streams) stream.destroy();
5626
+ origin.close();
5627
+ });
5628
+ });
5629
+ },
5630
+ async close() {
5631
+ closing = true;
5632
+ clearInterval(sweep);
5633
+ for (const session of sessions) session.shutdown();
5634
+ for (const socket of sockets.clients) socket.terminate();
5635
+ await new Promise((resolve4) => sockets.close(() => resolve4()));
5636
+ }
5637
+ };
5638
+ }
5639
+
5640
+ // src/transport.ts
4238
5641
  var loopback = (host) => ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(host);
4239
5642
  function validateNetwork(c) {
4240
5643
  const host = c.host ?? "127.0.0.1", mode = c.mode ?? "loopback";
@@ -4267,22 +5670,23 @@ function validateNetwork(c) {
4267
5670
  }
4268
5671
  if (mode === "cloudflare-tunnel" && (!loopback(host) || !c.protectedTunnel)) fail("TUNNEL_BACKEND_MUST_BE_LOOPBACK");
4269
5672
  if (mode === "public-tls" && !c.tls && (!loopback(host) || !c.trustedProxies?.length)) fail("TLS_REQUIRED");
4270
- if (!c.tls && !loopback(host) && !c.protectedTunnel) fail("TLS_REQUIRED");
4271
- if (!["loopback", "wireguard", "tailscale", "cloudflare-tunnel", "public-tls"].includes(mode)) fail("INVALID_NETWORK_MODE");
5673
+ if (!c.tls && !loopback(host) && !c.protectedTunnel && mode !== "e2e") fail("TLS_REQUIRED");
5674
+ if (!["loopback", "wireguard", "tailscale", "cloudflare-tunnel", "public-tls", "e2e"].includes(mode)) fail("INVALID_NETWORK_MODE");
4272
5675
  }
4273
5676
  async function serve(agent, c = {}) {
4274
5677
  c = structuredClone(c);
4275
5678
  validateNetwork(c);
4276
5679
  const throttle = new AuthThrottle(c.authLimits), host = c.host ?? "127.0.0.1";
4277
- const requestBoundary = (req, upgrade = false) => {
5680
+ if (c.mode === "e2e" && !agent.store.pairing) fail("PAIRING_SERVICE_REQUIRED");
5681
+ const requestBoundary = (req, upgrade = false, encrypted2 = false) => {
4278
5682
  const address = server.address(), defaultHost = `${host.includes(":") ? `[${host}]` : host}:${address.port}`;
4279
5683
  if (!(c.allowedHosts ?? [defaultHost]).includes(req.headers.host ?? "")) fail("HOST_DENIED", 403);
4280
5684
  const origin = req.headers.origin;
4281
5685
  if (origin !== void 0 && !(c.allowedOrigins ?? []).includes(origin) && !(upgrade && agent.desktopSessions.hasOrigin(origin))) fail("ORIGIN_DENIED", 403);
4282
- if (c.mode === "public-tls" && !c.tls && (!(c.trustedProxies ?? []).includes(req.socket.remoteAddress ?? "unknown") || req.headers["x-forwarded-proto"] !== "https")) fail("TLS_REQUIRED", 403);
5686
+ if (!encrypted2 && c.mode === "public-tls" && !c.tls && (!(c.trustedProxies ?? []).includes(req.socket.remoteAddress ?? "unknown") || req.headers["x-forwarded-proto"] !== "https")) fail("TLS_REQUIRED", 403);
4283
5687
  return defaultHost;
4284
5688
  };
4285
- const handler = async (req, res) => {
5689
+ const handler = async (req, res, peer) => {
4286
5690
  let context;
4287
5691
  const source = req.socket.remoteAddress ?? "unknown";
4288
5692
  const json = (status, value) => {
@@ -4290,7 +5694,27 @@ async function serve(agent, c = {}) {
4290
5694
  res.end(JSON.stringify(value));
4291
5695
  };
4292
5696
  try {
4293
- const defaultHost = requestBoundary(req);
5697
+ const defaultHost = requestBoundary(req, false, !!peer);
5698
+ if (c.mode === "e2e" && !peer) fail("E2E_REQUIRED", 426);
5699
+ const url = new URL(req.url ?? "/", `http://${defaultHost}`);
5700
+ if (peer && !agent.store.pairing.validPeer(peer)) fail("AUTH_INVALID", 401);
5701
+ if (url.pathname === "/pair/v2/redeem") {
5702
+ if (!peer || peer.kind !== "invite" || req.method !== "POST" || url.search) fail("PAIRING_CHANNEL_REQUIRED", 403);
5703
+ let body2 = Buffer.alloc(0);
5704
+ for await (const chunk of req) {
5705
+ body2 = Buffer.concat([body2, chunk]);
5706
+ if (body2.length > 2048) fail("REQUEST_TOO_LARGE", 413);
5707
+ }
5708
+ let input;
5709
+ try {
5710
+ input = JSON.parse(body2.toString("utf8"));
5711
+ } catch {
5712
+ fail("INVALID_JSON");
5713
+ }
5714
+ json(200, agent.store.pairing.redeem(peer, input));
5715
+ return;
5716
+ }
5717
+ if (peer?.kind === "invite") fail("PAIRING_CHANNEL_ONLY", 403);
4294
5718
  const reject = (decision) => {
4295
5719
  if (decision.audit) agent.store.audit(decision.code);
4296
5720
  if (decision.retryAfterMs) res.setHeader("retry-after", String(Math.max(1, Math.ceil(decision.retryAfterMs / 1e3))));
@@ -4300,12 +5724,13 @@ async function serve(agent, c = {}) {
4300
5724
  if (!/^Bearer [A-Za-z0-9_-]{43}$/.test(auth)) {
4301
5725
  reject(throttle.failure(source));
4302
5726
  }
4303
- const url = new URL(req.url ?? "/", `http://${defaultHost}`);
4304
5727
  let client;
4305
5728
  const authenticate = () => {
4306
5729
  try {
5730
+ if (peer && !agent.store.pairing.validPeer(peer)) fail("AUTH_INVALID", 401);
4307
5731
  if (url.pathname.startsWith("/control/v1/")) client = agent.store.client(auth.slice(7));
4308
5732
  else context = agent.store.grant(auth.slice(7));
5733
+ if (peer && (client?.id ?? context?.clientId) !== peer.id) fail("AUTH_INVALID", 401);
4309
5734
  } catch {
4310
5735
  reject(throttle.failure(source));
4311
5736
  }
@@ -4339,6 +5764,10 @@ async function serve(agent, c = {}) {
4339
5764
  json(200, result);
4340
5765
  return;
4341
5766
  }
5767
+ if (url.pathname === "/control/v1/desktop/close") {
5768
+ json(200, await agent.desktopSessions.closeClientSession(client.id, parsed));
5769
+ return;
5770
+ }
4342
5771
  if (url.pathname === "/desktop/v1/session") {
4343
5772
  json(200, await agent.desktopSessions.open(context, parsed));
4344
5773
  return;
@@ -4383,20 +5812,23 @@ async function serve(agent, c = {}) {
4383
5812
  json(e instanceof AgentError ? e.status : 500, { error: { code: e instanceof AgentError ? e.code : "AGENT_INTERNAL" } });
4384
5813
  }
4385
5814
  };
4386
- const tls = c.tls ? { key: readFileSync2(c.tls.keyFile), cert: readFileSync2(c.tls.certFile), ca: c.tls.caFile ? readFileSync2(c.tls.caFile) : void 0, requestCert: !!c.tls.requireClientCertificate, rejectUnauthorized: !!c.tls.requireClientCertificate } : null;
4387
- const server = tls ? https.createServer(tls, handler) : http.createServer(handler);
5815
+ const tls = c.tls ? { key: readFileSync3(c.tls.keyFile), cert: readFileSync3(c.tls.certFile), ca: c.tls.caFile ? readFileSync3(c.tls.caFile) : void 0, requestCert: !!c.tls.requireClientCertificate, rejectUnauthorized: !!c.tls.requireClientCertificate } : null;
5816
+ const publicHandler = (req, res) => void handler(req, res);
5817
+ const server = tls ? https.createServer(tls, publicHandler) : http2.createServer(publicHandler);
4388
5818
  server.requestTimeout = 3e4;
4389
5819
  server.headersTimeout = 1e4;
4390
5820
  server.maxConnections = 128;
4391
- const websocket = new WebSocketServer({ noServer: true, clientTracking: true, maxPayload: 65536, perMessageDeflate: false });
4392
- server.on("upgrade", (req, socket, head) => {
5821
+ const websocket = new WebSocketServer2({ noServer: true, clientTracking: true, maxPayload: 65536, perMessageDeflate: false });
5822
+ const desktopUpgrade = (req, socket, head, peer) => {
4393
5823
  try {
4394
- const defaultHost = requestBoundary(req, true), url = new URL(req.url ?? "/", `http://${defaultHost}`);
5824
+ const defaultHost = requestBoundary(req, true, !!peer), url = new URL(req.url ?? "/", `http://${defaultHost}`);
5825
+ if (c.mode === "e2e" && !peer) fail("E2E_REQUIRED", 426);
5826
+ if (peer && (peer.kind !== "client" || !agent.store.pairing.validPeer(peer))) fail("AUTH_INVALID", 401);
4395
5827
  if (url.pathname !== "/desktop/v1/socket" || url.search) fail("NOT_FOUND", 404);
4396
5828
  if (websocket.clients.size >= 16) fail("DESKTOP_CONNECTION_LIMIT", 429);
4397
5829
  websocket.handleUpgrade(req, socket, head, (ws) => {
4398
5830
  websocket.emit("connection", ws, req);
4399
- agent.desktopSessions.accept(ws, req.headers.origin, c.allowedOrigins ?? []);
5831
+ agent.desktopSessions.accept(ws, req.headers.origin, c.allowedOrigins ?? [], peer?.id);
4400
5832
  });
4401
5833
  } catch (e) {
4402
5834
  const status = e instanceof AgentError ? e.status : 500;
@@ -4404,17 +5836,42 @@ async function serve(agent, c = {}) {
4404
5836
  Connection: close\r
4405
5837
  Content-Length: 0\r
4406
5838
  \r
5839
+ `);
5840
+ }
5841
+ };
5842
+ const encrypted = agent.store.pairing ? secureTransport(agent.store.pairing, (req, res, peer) => void handler(req, res, peer), desktopUpgrade) : null;
5843
+ server.on("upgrade", (req, socket, head) => {
5844
+ try {
5845
+ const defaultHost = requestBoundary(req, true), url = new URL(req.url ?? "/", `http://${defaultHost}`);
5846
+ if (url.pathname === "/v1/e2e" && !url.search) {
5847
+ if (!encrypted) fail("NOT_FOUND", 404);
5848
+ encrypted.accept(req, socket, head);
5849
+ return;
5850
+ }
5851
+ desktopUpgrade(req, socket, head);
5852
+ } catch (e) {
5853
+ const status = e instanceof AgentError ? e.status : 500;
5854
+ socket.end(`HTTP/1.1 ${status} Rejected\r
5855
+ Connection: close\r
5856
+ Content-Length: 0\r
5857
+ \r
4407
5858
  `);
4408
5859
  }
4409
5860
  });
4410
- await new Promise((resolve4, reject) => {
4411
- server.once("error", reject);
4412
- server.listen(c.port ?? 9701, host, () => resolve4());
4413
- });
5861
+ try {
5862
+ await new Promise((resolve4, reject) => {
5863
+ server.once("error", reject);
5864
+ server.listen(c.port ?? 9701, host, () => resolve4());
5865
+ });
5866
+ } catch (error2) {
5867
+ await encrypted?.close();
5868
+ throw error2;
5869
+ }
4414
5870
  return { server, address: server.address(), supportedMcpVersions: SUPPORTED_PROTOCOL_VERSIONS, close: async () => {
4415
5871
  await agent.desktopSessions.close();
4416
5872
  for (const client of websocket.clients) client.terminate();
4417
5873
  await new Promise((resolve4) => websocket.close(() => resolve4()));
5874
+ await encrypted?.close();
4418
5875
  await new Promise((resolve4, reject) => server.close((e) => e ? reject(e) : resolve4()));
4419
5876
  } };
4420
5877
  }