@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.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// src/agent.ts
|
|
2
|
-
import { readFile as
|
|
3
|
-
import { join as join8, dirname, resolve as resolve3 } from "node:path";
|
|
4
|
-
import { randomUUID as
|
|
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.
|
|
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:
|
|
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-${
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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-${
|
|
1606
|
+
const id2 = `browser-${randomUUID3()}`, downloads = /* @__PURE__ */ new Map();
|
|
545
1607
|
page.on("download", (download2) => {
|
|
546
|
-
const downloadId2 = `download-${
|
|
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-${
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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-${
|
|
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
|
|
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 =
|
|
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
|
|
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}${
|
|
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
|
|
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
|
|
3584
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
2515
3585
|
|
|
2516
3586
|
// ../packages/cli-runtime/src/codex-session-core.mjs
|
|
2517
|
-
import { createHash as
|
|
2518
|
-
import { readFile as
|
|
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 {
|
|
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
|
|
2566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2694
|
-
import { mkdir as mkdir4, writeFile as writeFile2, mkdtemp, chmod, lstat as
|
|
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
|
-
|
|
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-${
|
|
3976
|
+
const descriptorPath2 = join5(home, `bridge-${randomBytes5(12).toString("hex")}.json`);
|
|
2736
3977
|
let socketDirectory;
|
|
2737
|
-
let endpoint = process.platform === "win32" ? `dsh-cli-${
|
|
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(
|
|
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
|
|
3984
|
+
const stat8 = await lstat6(socketDirectory);
|
|
2744
3985
|
if (!stat8.isDirectory() || stat8.isSymbolicLink() || stat8.uid !== process.getuid() || (stat8.mode & 63) !== 0) {
|
|
2745
|
-
await
|
|
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
|
|
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
|
|
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
|
|
2772
|
-
|
|
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) => !
|
|
2787
|
-
if (active.
|
|
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
|
-
|
|
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
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
|
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:
|
|
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) =>
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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(),
|
|
3615
|
-
if (
|
|
3616
|
-
const terminal2 =
|
|
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
|
-
...
|
|
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-${
|
|
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
|
|
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
|
|
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(
|
|
5251
|
+
const temp = join8(dirname2(target), `.dsh-write-${randomUUID7()}`);
|
|
3975
5252
|
try {
|
|
3976
|
-
const parent = await stat7(
|
|
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(
|
|
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
|
|
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-${
|
|
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-${
|
|
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
|
|
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
|
|
5441
|
+
import http2 from "node:http";
|
|
4165
5442
|
import https from "node:https";
|
|
4166
|
-
import { readFileSync as
|
|
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
|
-
|
|
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:
|
|
4387
|
-
const
|
|
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
|
|
4392
|
-
|
|
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
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
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
|
}
|