@ljwei-stak/dsh-model-route-bot 0.66.4 → 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 (271) 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 +18 -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/package.json +1 -1
  178. package/packages/orchestration/src/binding-product.mjs +19 -0
  179. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  180. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  181. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  182. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  183. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  184. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  185. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  186. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  187. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  188. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  189. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  190. package/packages/orchestration/src/bot-loop.mjs +152 -19
  191. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  192. package/packages/orchestration/src/bot-policy.mjs +43 -4
  193. package/packages/orchestration/src/bot-profile.mjs +171 -0
  194. package/packages/orchestration/src/bot-routine.mjs +4 -4
  195. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  196. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  197. package/packages/orchestration/src/bot-serving.mjs +164 -0
  198. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  199. package/packages/orchestration/src/bot-settings.mjs +50 -11
  200. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  201. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  202. package/packages/orchestration/src/bot-threads.mjs +790 -0
  203. package/packages/orchestration/src/data-product-access.mjs +37 -0
  204. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  205. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  206. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  207. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  208. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  209. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  210. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  211. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  212. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  213. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  214. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  215. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  216. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  217. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  218. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  219. package/packages/orchestration/src/model-accounting.mjs +99 -35
  220. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  221. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  222. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  223. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  224. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  225. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  226. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  227. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  228. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  229. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  230. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  231. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  232. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  233. package/packages/orchestration/src/remote-machines.mjs +82 -14
  234. package/packages/orchestration/src/routine-execution.mjs +5 -3
  235. package/packages/orchestration/src/routine-session.mjs +19 -13
  236. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  237. package/packages/orchestration/src/task-journal.mjs +29 -0
  238. package/packages/orchestration/src/task-product.mjs +17 -0
  239. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  240. package/packages/orchestration/src/team-bridge.mjs +282 -0
  241. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  242. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  243. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  244. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  245. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  246. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  247. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  248. package/remote-computer-agent/README.md +39 -29
  249. package/remote-computer-agent/build.mjs +3 -0
  250. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  253. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  255. package/remote-computer-agent/dist/cli.mjs +1688 -168
  256. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  257. package/remote-computer-agent/dist/index.mjs +1579 -122
  258. package/remote-computer-agent/dist/index.mjs.map +4 -4
  259. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  260. package/remote-computer-agent/package-lock.json +308 -2
  261. package/remote-computer-agent/package.json +2 -1
  262. package/remote-computer-agent/pair-device.ps1 +20 -0
  263. package/remote-computer-agent/setup.ps1 +38 -16
  264. package/remote-computer-agent/src/cli.ts +24 -3
  265. package/remote-computer-agent/src/desktop-session.ts +11 -3
  266. package/remote-computer-agent/src/e2e-server.ts +81 -0
  267. package/remote-computer-agent/src/pairing-page.ts +35 -0
  268. package/remote-computer-agent/src/protocol.ts +25 -25
  269. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  270. package/remote-computer-agent/src/store.ts +20 -5
  271. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,417 @@
1
+ // One encrypted session over any message transport: the handshake, then records carrying yamux frames, each stream
2
+ // a Node Duplex. The computer pipes every stream to its own origin socket; the phone hands streams to an HTTP stack.
3
+ import { Duplex } from "node:stream";
4
+
5
+ import {
6
+ CLOSE,
7
+ computerAccept,
8
+ encodePayload1,
9
+ HANDSHAKE_DEADLINE_MS,
10
+ newConn,
11
+ NoiseError,
12
+ NONCE_LIMIT,
13
+ phoneHello,
14
+ type KeyPair,
15
+ type Payload1,
16
+ type PhoneHello,
17
+ type PskCandidate,
18
+ type SessionKeys,
19
+ type TransportByte,
20
+ } from "./noise.ts";
21
+ import { b64u } from "./credentials.ts";
22
+ import { decodeFrame, encodeFrame, GO_AWAY, Mux, PING_INTERVAL_MS, PING_TIMEOUT_MS, YamuxError, type Frame, type Role } from "./yamux.ts";
23
+
24
+ /** A sender stops writing records while the socket holds more than this. */
25
+ export const SEND_BUFFER_HIGH = 1024 * 1024;
26
+
27
+ /** A WebSocket, as the session needs it: binary messages out, a close, and backpressure. */
28
+ export interface RecordTransport {
29
+ send(message: Uint8Array): void;
30
+ close(code: number, reason?: string): void;
31
+ readonly bufferedAmount: number;
32
+ onceDrained(listener: () => void): void;
33
+ }
34
+
35
+ export class SessionClosed extends Error {
36
+ readonly code: number;
37
+ constructor(code: number) {
38
+ super(`e2e session closed (${code})`);
39
+ this.code = code;
40
+ }
41
+ }
42
+
43
+ interface Timers {
44
+ pingIntervalMs?: number;
45
+ pingTimeoutMs?: number;
46
+ handshakeDeadlineMs?: number;
47
+ }
48
+
49
+ export interface ComputerSessionOptions<Id> extends Timers {
50
+ transport: RecordTransport;
51
+ transportByte: TransportByte;
52
+ staticKey: KeyPair;
53
+ /** Read when message 1 arrives, so a revoke or a closed pairing window applies at once. */
54
+ candidates(): ReadonlyArray<PskCandidate<Id>>;
55
+ /** A fresh connection to the private origin listener, per stream. */
56
+ connectOrigin(): Promise<Duplex>;
57
+ opened?(id: Id, payload1: Payload1): void;
58
+ }
59
+
60
+ export interface PhoneSessionOptions extends Timers {
61
+ transport: RecordTransport;
62
+ transportByte: TransportByte;
63
+ remoteStatic: Uint8Array;
64
+ psk: Uint8Array;
65
+ app: string;
66
+ /** Test-only. */
67
+ ephemeral?: KeyPair;
68
+ }
69
+
70
+ const noop = () => {};
71
+
72
+ export class MuxStream extends Duplex {
73
+ readonly streamId: number;
74
+ readonly #session: E2ESession<unknown>;
75
+ #pending: Buffer | null = null;
76
+ #pendingDone: ((error?: Error | null) => void) | null = null;
77
+ #ungranted = 0;
78
+
79
+ constructor(session: E2ESession<unknown>, streamId: number) {
80
+ super({ allowHalfOpen: true, readableHighWaterMark: 256 * 1024 });
81
+ this.#session = session;
82
+ this.streamId = streamId;
83
+ // A reset reaches whoever listens; without a listener it must not take the process down.
84
+ this.on("error", noop);
85
+ }
86
+
87
+ override _write(chunk: Buffer, _encoding: BufferEncoding, done: (error?: Error | null) => void): void {
88
+ this.#pending = chunk;
89
+ this.#pendingDone = done;
90
+ this.flush();
91
+ }
92
+
93
+ override _final(done: (error?: Error | null) => void): void {
94
+ this.#session.mux?.finish(this.streamId);
95
+ done();
96
+ }
97
+
98
+ override _read(): void {
99
+ if (this.#ungranted === 0) return;
100
+ const bytes = this.#ungranted;
101
+ this.#ungranted = 0;
102
+ this.#session.mux?.consumed(this.streamId, bytes);
103
+ }
104
+
105
+ override _destroy(error: Error | null, done: (error?: Error | null) => void): void {
106
+ const mux = this.#session.mux;
107
+ if (mux?.has(this.streamId)) mux.reset(this.streamId);
108
+ this.#session.forget(this.streamId);
109
+ const pendingDone = this.#pendingDone;
110
+ this.#pending = null;
111
+ this.#pendingDone = null;
112
+ pendingDone?.(error ?? new Error("stream reset"));
113
+ done(error);
114
+ }
115
+
116
+ /** Granted back as the reader takes it, so a slow reader slows the sender. */
117
+ deliver(data: Buffer): void {
118
+ if (this.push(data)) this.#session.mux?.consumed(this.streamId, data.length);
119
+ else this.#ungranted += data.length;
120
+ }
121
+
122
+ remoteEnded(): void {
123
+ this.push(null);
124
+ }
125
+
126
+ remoteReset(): void {
127
+ if (!this.destroyed) this.destroy(new Error("stream reset"));
128
+ }
129
+
130
+ flush(): void {
131
+ const mux = this.#session.mux;
132
+ while (this.#pending && this.#pending.length > 0) {
133
+ if (!mux || !mux.has(this.streamId)) return void this.destroy(new Error("stream reset"));
134
+ if (this.#session.congested) return this.#session.whenDrained(() => this.flush());
135
+
136
+ const taken = mux.write(this.streamId, this.#pending);
137
+ if (taken === 0) return;
138
+ this.#pending = this.#pending.subarray(taken);
139
+ }
140
+ const done = this.#pendingDone;
141
+ this.#pending = null;
142
+ this.#pendingDone = null;
143
+ done?.();
144
+ }
145
+ }
146
+
147
+ type State = "handshake" | "open" | "closed";
148
+
149
+ export class E2ESession<Id> {
150
+ readonly role: Role;
151
+ readonly #transport: RecordTransport;
152
+ readonly #streams = new Map<number, MuxStream>();
153
+ readonly #timers: Required<Timers>;
154
+ #state: State = "handshake";
155
+ #keys: SessionKeys | null = null;
156
+ #mux: Mux | null = null;
157
+ #accept: ((message: Buffer) => void) | null = null;
158
+ #deadline: ReturnType<typeof setTimeout> | null = null;
159
+ #pingTimer: ReturnType<typeof setInterval> | null = null;
160
+ #pongTimer: ReturnType<typeof setTimeout> | null = null;
161
+ #awaitingPong: number | null = null;
162
+ #pingCounter = 0;
163
+ #drainWaiters: Array<() => void> = [];
164
+ #settleReady!: (error?: Error) => void;
165
+ #settleClosed!: (code: number) => void;
166
+ #connectOrigin: (() => Promise<Duplex>) | null = null;
167
+ #removed = false;
168
+ /** The computer side: which device (or pairing window) this is. */
169
+ peer: Id | null = null;
170
+ readonly ready: Promise<void>;
171
+ readonly closed: Promise<number>;
172
+
173
+ private constructor(role: Role, transport: RecordTransport, timers: Timers) {
174
+ this.role = role;
175
+ this.#transport = transport;
176
+ this.#timers = {
177
+ pingIntervalMs: timers.pingIntervalMs ?? PING_INTERVAL_MS,
178
+ pingTimeoutMs: timers.pingTimeoutMs ?? PING_TIMEOUT_MS,
179
+ handshakeDeadlineMs: timers.handshakeDeadlineMs ?? HANDSHAKE_DEADLINE_MS,
180
+ };
181
+ this.ready = new Promise((resolve, reject) => {
182
+ this.#settleReady = (error) => (error ? reject(error) : resolve());
183
+ });
184
+ this.ready.catch(noop);
185
+ this.closed = new Promise((resolve) => {
186
+ this.#settleClosed = resolve;
187
+ });
188
+ this.#deadline = setTimeout(() => this.close(CLOSE.deadline), this.#timers.handshakeDeadlineMs);
189
+ this.#deadline.unref?.();
190
+ }
191
+
192
+ /** Waits for message 1. Anything wrong with it closes 4001 and nothing else is sent. */
193
+ static computer<Id>(options: ComputerSessionOptions<Id>): E2ESession<Id> {
194
+ const session = new E2ESession<Id>("computer", options.transport, options);
195
+ session.#connectOrigin = options.connectOrigin;
196
+ session.#accept = (message) => {
197
+ const accepted = computerAccept({ staticKey: options.staticKey, transport: options.transportByte, candidates: options.candidates() }, message);
198
+ if (!accepted) return session.close(CLOSE.rejected);
199
+ options.transport.send(accepted.message2);
200
+ session.peer = accepted.id;
201
+ session.#start(accepted.keys);
202
+ options.opened?.(accepted.id, accepted.payload1);
203
+ };
204
+ return session;
205
+ }
206
+
207
+ /** Sends message 1 now. `ready` rejects with a NoiseError when the computer can't be verified: a hard stop. */
208
+ static phone(options: PhoneSessionOptions): E2ESession<never> {
209
+ const session = new E2ESession<never>("phone", options.transport, options);
210
+ const conn = newConn();
211
+ const hello: PhoneHello = phoneHello(
212
+ { remoteStatic: options.remoteStatic, psk: options.psk, transport: options.transportByte, ephemeral: options.ephemeral },
213
+ encodePayload1(conn, options.app),
214
+ b64u(conn),
215
+ );
216
+ session.#accept = (message) => {
217
+ let keys: SessionKeys;
218
+ try {
219
+ keys = hello.finish(message);
220
+ } catch (error) {
221
+ session.#settleReady(error as Error);
222
+ return session.close(CLOSE.protocol);
223
+ }
224
+ session.#start(keys);
225
+ };
226
+ options.transport.send(hello.message1);
227
+ return session;
228
+ }
229
+
230
+ get mux(): Mux | null {
231
+ return this.#mux;
232
+ }
233
+
234
+ /** The phone side: the computer said, inside the encryption, that this device was removed. Only this, or a 4001 on
235
+ * a direct route, means "pair again"; a bare 4001 through the relay may be the relay's own and means "keep trying". */
236
+ get removed(): boolean {
237
+ return this.#removed;
238
+ }
239
+
240
+ get state(): State {
241
+ return this.#state;
242
+ }
243
+
244
+ /** Records each way so far, for diagnostics. */
245
+ get records(): { sent: number; received: number } {
246
+ return { sent: this.#keys?.send.nonce ?? 0, received: this.#keys?.receive.nonce ?? 0 };
247
+ }
248
+
249
+ get congested(): boolean {
250
+ return this.#transport.bufferedAmount > SEND_BUFFER_HIGH;
251
+ }
252
+
253
+ get activeStreams(): number {
254
+ return this.#streams.size;
255
+ }
256
+
257
+ whenDrained(listener: () => void): void {
258
+ this.#drainWaiters.push(listener);
259
+ if (this.#drainWaiters.length > 1) return;
260
+ this.#transport.onceDrained(() => {
261
+ const waiters = this.#drainWaiters;
262
+ this.#drainWaiters = [];
263
+ for (const waiter of waiters) waiter();
264
+ });
265
+ }
266
+
267
+ forget(streamId: number): void {
268
+ this.#streams.delete(streamId);
269
+ }
270
+
271
+ /** Every message the transport receives, in order. */
272
+ receive(message: Uint8Array, binary: boolean): void {
273
+ if (this.#state === "closed") return;
274
+ if (this.#state === "handshake") {
275
+ const accept = this.#accept;
276
+ this.#accept = null;
277
+ if (!binary || !accept) return this.close(this.role === "computer" ? CLOSE.rejected : CLOSE.protocol);
278
+ return accept(Buffer.from(message));
279
+ }
280
+ if (!binary) return this.close(CLOSE.protocol);
281
+ let plaintext: Buffer;
282
+ try {
283
+ plaintext = (this.#keys as SessionKeys).receive.open(message);
284
+ } catch {
285
+ return this.close(CLOSE.protocol);
286
+ }
287
+ try {
288
+ (this.#mux as Mux).receive(decodeFrame(plaintext));
289
+ } catch (error) {
290
+ if (!(error instanceof YamuxError)) throw error;
291
+ this.#mux?.protocolError();
292
+ this.close(CLOSE.protocol);
293
+ }
294
+ }
295
+
296
+ /** The phone side. */
297
+ openStream(): MuxStream {
298
+ if (this.#state !== "open" || !this.#mux) throw new SessionClosed(CLOSE.normal);
299
+ const streamId = this.#mux.open();
300
+ const stream = new MuxStream(this as E2ESession<unknown>, streamId);
301
+ this.#streams.set(streamId, stream);
302
+ return stream;
303
+ }
304
+
305
+ /** The computer side: the device was removed. The phone hears it authenticated, then the 4001 close. */
306
+ remove(): void {
307
+ if (this.#state === "open") this.#mux?.goAway(GO_AWAY.removed);
308
+ this.close(CLOSE.rejected);
309
+ }
310
+
311
+ /** A planned close (quit, sleep, restart): GoAway first, so the peer knows it isn't an error. */
312
+ shutdown(): void {
313
+ if (this.#state === "open") this.#mux?.goAway(GO_AWAY.normal);
314
+ this.close(CLOSE.normal);
315
+ }
316
+
317
+ close(code: number, reason?: string): void {
318
+ if (this.#state === "closed") return;
319
+ this.#transport.close(code, reason);
320
+ this.#end(code);
321
+ }
322
+
323
+ /** The transport closed under us. */
324
+ transportClosed(code: number): void {
325
+ this.#end(code);
326
+ }
327
+
328
+ #end(code: number): void {
329
+ if (this.#state === "closed") return;
330
+ const wasHandshake = this.#state === "handshake";
331
+ this.#state = "closed";
332
+ for (const timer of [this.#deadline, this.#pongTimer]) if (timer) clearTimeout(timer);
333
+ if (this.#pingTimer) clearInterval(this.#pingTimer);
334
+ this.#mux?.closeAll();
335
+ for (const stream of this.#streams.values()) stream.remoteReset();
336
+ this.#streams.clear();
337
+ const waiters = this.#drainWaiters;
338
+ this.#drainWaiters = [];
339
+ for (const waiter of waiters) waiter();
340
+ if (wasHandshake) this.#settleReady(new SessionClosed(code));
341
+ this.#settleClosed(code);
342
+ }
343
+
344
+ #start(keys: SessionKeys): void {
345
+ if (this.#deadline) clearTimeout(this.#deadline);
346
+ this.#keys = keys;
347
+ this.#state = "open";
348
+ this.#mux = new Mux(this.role, {
349
+ send: (frame) => this.#send(frame),
350
+ opened: (streamId) => this.#openOrigin(streamId),
351
+ data: (streamId, data) => this.#streams.get(streamId)?.deliver(Buffer.from(data.buffer, data.byteOffset, data.byteLength)),
352
+ ended: (streamId) => this.#streams.get(streamId)?.remoteEnded(),
353
+ reset: (streamId) => {
354
+ const stream = this.#streams.get(streamId);
355
+ this.#streams.delete(streamId);
356
+ stream?.remoteReset();
357
+ },
358
+ writable: (streamId) => this.#streams.get(streamId)?.flush(),
359
+ goAway: (code) => {
360
+ if (code === GO_AWAY.removed && this.role === "phone") this.#removed = true;
361
+ else if (code !== GO_AWAY.normal) this.close(CLOSE.protocol);
362
+ },
363
+ pong: (value) => {
364
+ if (value !== this.#awaitingPong) return;
365
+ this.#awaitingPong = null;
366
+ if (this.#pongTimer) clearTimeout(this.#pongTimer);
367
+ },
368
+ });
369
+ this.#pingTimer = setInterval(() => this.#ping(), this.#timers.pingIntervalMs);
370
+ this.#pingTimer.unref?.();
371
+ this.#settleReady();
372
+ }
373
+
374
+ #ping(): void {
375
+ if (this.#awaitingPong !== null || !this.#mux) return;
376
+ this.#pingCounter = (this.#pingCounter + 1) >>> 0;
377
+ this.#awaitingPong = this.#pingCounter;
378
+ this.#mux.ping(this.#pingCounter);
379
+ this.#pongTimer = setTimeout(() => this.close(CLOSE.goingAway), this.#timers.pingTimeoutMs);
380
+ this.#pongTimer.unref?.();
381
+ }
382
+
383
+ #send(frame: Frame): void {
384
+ if (this.#state !== "open" || !this.#keys) return;
385
+ let record: Buffer;
386
+ try {
387
+ record = this.#keys.send.seal(encodeFrame(frame));
388
+ } catch (error) {
389
+ if (error instanceof NoiseError) return this.close(CLOSE.normal);
390
+ throw error;
391
+ }
392
+ this.#transport.send(record);
393
+ // Well before the counter could repeat; the phone reconnects.
394
+ if (this.#keys.send.nonce >= NONCE_LIMIT - 1024) queueMicrotask(() => this.close(CLOSE.normal));
395
+ }
396
+
397
+ #openOrigin(streamId: number): void {
398
+ const stream = new MuxStream(this as E2ESession<unknown>, streamId);
399
+ this.#streams.set(streamId, stream);
400
+ const connect = this.#connectOrigin;
401
+ if (!connect) return void stream.destroy();
402
+ connect().then(
403
+ (socket) => {
404
+ if (stream.destroyed || this.#state !== "open") return void socket.destroy();
405
+ this.#mux?.accept(streamId);
406
+ socket.on("error", () => stream.destroy());
407
+ socket.on("close", () => {
408
+ if (!socket.readableEnded) stream.destroy();
409
+ });
410
+ stream.on("close", () => socket.destroy());
411
+ stream.pipe(socket);
412
+ socket.pipe(stream);
413
+ },
414
+ () => stream.destroy(),
415
+ );
416
+ }
417
+ }