@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
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
// Generated from frozen OpenMausBot protocol sources; Apache-2.0; see ../SOURCE.json, ../NOTICE and ../vendor/LICENSE.
|
|
2
|
+
// modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts
|
|
3
|
+
import { createCipheriv, createDecipheriv, createECDH, hkdfSync, randomBytes } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
// modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts
|
|
6
|
+
import { createHash, createHmac, createPrivateKey, createPublicKey, sign, timingSafeEqual, verify } from "node:crypto";
|
|
7
|
+
var B64U = /^[A-Za-z0-9_-]*$/;
|
|
8
|
+
var b64u = (bytes) => Buffer.from(bytes).toString("base64url");
|
|
9
|
+
function fromB64u(text) {
|
|
10
|
+
if (!B64U.test(text) || text.length % 4 === 1) return null;
|
|
11
|
+
const bytes = Buffer.from(text, "base64url");
|
|
12
|
+
return bytes.toString("base64url") === text ? bytes : null;
|
|
13
|
+
}
|
|
14
|
+
var sha256 = (bytes) => createHash("sha256").update(bytes).digest();
|
|
15
|
+
var hmacSha256 = (key, message) => createHmac("sha256", key).update(message).digest();
|
|
16
|
+
function sameBytes(a, b) {
|
|
17
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
18
|
+
}
|
|
19
|
+
var KEY_ID_BYTES = 16;
|
|
20
|
+
var HOST_ID_BYTES = 16;
|
|
21
|
+
var PSK_LABEL = Buffer.from("omb-e2e psk v1", "utf8");
|
|
22
|
+
var ADMISSION_LABEL = Buffer.from("omb-relay admission v1", "utf8");
|
|
23
|
+
var credentialHash = (token) => sha256(Buffer.from(token, "utf8"));
|
|
24
|
+
function credentialHashFromHex(hex) {
|
|
25
|
+
return /^[0-9a-f]{64}$/.test(hex) ? Buffer.from(hex, "hex") : null;
|
|
26
|
+
}
|
|
27
|
+
var keyId = (transportPublicKey) => sha256(transportPublicKey).subarray(0, KEY_ID_BYTES);
|
|
28
|
+
var psk = (credHash, transportKeyId) => hmacSha256(credHash, Buffer.concat([PSK_LABEL, transportKeyId]));
|
|
29
|
+
var admission = (credHash, hostId) => hmacSha256(credHash, Buffer.concat([ADMISSION_LABEL, hostId]));
|
|
30
|
+
var admissionHash = (adm) => b64u(sha256(adm));
|
|
31
|
+
var RELAY_TOKEN_TTL_S = 12 * 60 * 60;
|
|
32
|
+
var RELAY_TOKEN_MAX_CHARS = 2048;
|
|
33
|
+
var KID = /^k[0-9]{1,3}$/;
|
|
34
|
+
function ed25519PrivateKey(seed) {
|
|
35
|
+
if (seed.length !== 32) throw new Error("an Ed25519 seed is 32 bytes");
|
|
36
|
+
const der = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]);
|
|
37
|
+
return createPrivateKey({ key: der, format: "der", type: "pkcs8" });
|
|
38
|
+
}
|
|
39
|
+
function ed25519PublicKey(raw) {
|
|
40
|
+
if (raw.length !== 32) throw new Error("an Ed25519 public key is 32 bytes");
|
|
41
|
+
const der = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
|
|
42
|
+
return createPublicKey({ key: der, format: "der", type: "spki" });
|
|
43
|
+
}
|
|
44
|
+
var ed25519RawPublicKey = (key) => Buffer.from(createPublicKey(key).export({ format: "der", type: "spki" })).subarray(-32);
|
|
45
|
+
function mintRelayToken(claims, signingKey) {
|
|
46
|
+
const body = b64u(Buffer.from(JSON.stringify({
|
|
47
|
+
v: claims.v,
|
|
48
|
+
kid: claims.kid,
|
|
49
|
+
hid: claims.hid,
|
|
50
|
+
iid: claims.iid,
|
|
51
|
+
iat: claims.iat,
|
|
52
|
+
exp: claims.exp
|
|
53
|
+
}), "utf8"));
|
|
54
|
+
return `${body}.${b64u(sign(null, Buffer.from(body, "ascii"), signingKey))}`;
|
|
55
|
+
}
|
|
56
|
+
var isSeconds = (value) => Number.isSafeInteger(value) && value > 0;
|
|
57
|
+
function verifyRelayToken(token, keys, nowS, expectHid) {
|
|
58
|
+
const fail = (error) => ({ ok: false, error });
|
|
59
|
+
if (typeof token !== "string" || token.length > RELAY_TOKEN_MAX_CHARS) return fail("malformed");
|
|
60
|
+
const parts = token.split(".");
|
|
61
|
+
if (parts.length !== 2) return fail("malformed");
|
|
62
|
+
const [body, signatureText] = parts;
|
|
63
|
+
const payload = fromB64u(body);
|
|
64
|
+
const signature = fromB64u(signatureText);
|
|
65
|
+
if (!payload || !signature || signature.length !== 64) return fail("malformed");
|
|
66
|
+
let claims;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(payload.toString("utf8"));
|
|
69
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return fail("malformed");
|
|
70
|
+
claims = parsed;
|
|
71
|
+
} catch {
|
|
72
|
+
return fail("malformed");
|
|
73
|
+
}
|
|
74
|
+
if (claims.v !== 1 || typeof claims.kid !== "string" || !KID.test(claims.kid)) return fail("malformed");
|
|
75
|
+
const raw = keys.get(claims.kid);
|
|
76
|
+
if (!raw) return fail("kid");
|
|
77
|
+
if (!verify(null, Buffer.from(body, "ascii"), ed25519PublicKey(raw), signature)) return fail("signature");
|
|
78
|
+
const hid = typeof claims.hid === "string" ? fromB64u(claims.hid) : null;
|
|
79
|
+
if (!hid || hid.length !== HOST_ID_BYTES) return fail("malformed");
|
|
80
|
+
if (typeof claims.iid !== "string" || claims.iid.length === 0 || claims.iid.length > 64) return fail("malformed");
|
|
81
|
+
if (!isSeconds(claims.iat) || !isSeconds(claims.exp) || claims.exp <= claims.iat) return fail("malformed");
|
|
82
|
+
if (claims.exp - claims.iat > RELAY_TOKEN_TTL_S) return fail("malformed");
|
|
83
|
+
if (nowS >= claims.exp) return fail("expired");
|
|
84
|
+
if (expectHid !== void 0 && claims.hid !== expectHid) return fail("hid");
|
|
85
|
+
return { ok: true, claims };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts
|
|
89
|
+
var PROTOCOL_NAME = "Noise_NKpsk0_P256_AESGCM_SHA256";
|
|
90
|
+
var PUBLIC_KEY_BYTES = 65;
|
|
91
|
+
var TAG_BYTES = 16;
|
|
92
|
+
var MAX_RECORD_BYTES = 65535;
|
|
93
|
+
var MAX_RECORD_PLAINTEXT = MAX_RECORD_BYTES - TAG_BYTES;
|
|
94
|
+
var MAX_HANDSHAKE_PAYLOAD = 512;
|
|
95
|
+
var MAX_HANDSHAKE_MESSAGE = PUBLIC_KEY_BYTES + MAX_HANDSHAKE_PAYLOAD + TAG_BYTES;
|
|
96
|
+
var CONN_BYTES = 16;
|
|
97
|
+
var MAX_APP_CHARS = 40;
|
|
98
|
+
var NONCE_LIMIT = 2 ** 32;
|
|
99
|
+
var HANDSHAKE_DEADLINE_MS = 1e4;
|
|
100
|
+
var TRANSPORT_RELAY = 1;
|
|
101
|
+
var TRANSPORT_DIRECT = 2;
|
|
102
|
+
var CLOSE = {
|
|
103
|
+
normal: 1e3,
|
|
104
|
+
goingAway: 1001,
|
|
105
|
+
protocol: 4e3,
|
|
106
|
+
rejected: 4001,
|
|
107
|
+
deadline: 4002
|
|
108
|
+
};
|
|
109
|
+
var NoiseError = class extends Error {
|
|
110
|
+
reason;
|
|
111
|
+
constructor(reason) {
|
|
112
|
+
super(`e2e: ${reason}`);
|
|
113
|
+
this.reason = reason;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
function prologue(transport) {
|
|
117
|
+
return Buffer.concat([Buffer.from("omb-e2e/1", "utf8"), Buffer.from([0, transport])]);
|
|
118
|
+
}
|
|
119
|
+
var ecdh = (privateKey) => {
|
|
120
|
+
const curve = createECDH("prime256v1");
|
|
121
|
+
if (privateKey) curve.setPrivateKey(privateKey);
|
|
122
|
+
else curve.generateKeys();
|
|
123
|
+
return curve;
|
|
124
|
+
};
|
|
125
|
+
function keyPairFromPrivate(privateKey) {
|
|
126
|
+
if (privateKey.length !== 32) throw new NoiseError("bad_private_key");
|
|
127
|
+
return { privateKey: Buffer.from(privateKey), publicKey: ecdh(privateKey).getPublicKey(null, "uncompressed") };
|
|
128
|
+
}
|
|
129
|
+
function generateKeyPair() {
|
|
130
|
+
const curve = ecdh();
|
|
131
|
+
const raw = curve.getPrivateKey();
|
|
132
|
+
return { privateKey: Buffer.concat([Buffer.alloc(32 - raw.length), raw]), publicKey: curve.getPublicKey(null, "uncompressed") };
|
|
133
|
+
}
|
|
134
|
+
function dh(privateKey, publicKey) {
|
|
135
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES || publicKey[0] !== 4) throw new NoiseError("bad_point");
|
|
136
|
+
try {
|
|
137
|
+
return ecdh(privateKey).computeSecret(publicKey);
|
|
138
|
+
} catch {
|
|
139
|
+
throw new NoiseError("bad_point");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
var CHECK_SCALAR = Buffer.alloc(32, 1);
|
|
143
|
+
function isPublicKey(bytes) {
|
|
144
|
+
try {
|
|
145
|
+
dh(CHECK_SCALAR, bytes);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function hkdf(chainingKey, ikm, outputs) {
|
|
152
|
+
const out = Buffer.from(hkdfSync("sha256", ikm, chainingKey, Buffer.alloc(0), 32 * outputs));
|
|
153
|
+
return Array.from({ length: outputs }, (_, index) => out.subarray(index * 32, index * 32 + 32));
|
|
154
|
+
}
|
|
155
|
+
function nonceBytes(n) {
|
|
156
|
+
const nonce = Buffer.alloc(12);
|
|
157
|
+
nonce.writeBigUInt64BE(BigInt(n), 4);
|
|
158
|
+
return nonce;
|
|
159
|
+
}
|
|
160
|
+
function aeadSeal(key, n, ad, plaintext) {
|
|
161
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonceBytes(n), { authTagLength: TAG_BYTES });
|
|
162
|
+
cipher.setAAD(ad);
|
|
163
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
|
|
164
|
+
}
|
|
165
|
+
function aeadOpen(key, n, ad, ciphertext) {
|
|
166
|
+
if (ciphertext.length < TAG_BYTES) throw new NoiseError("short_ciphertext");
|
|
167
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonceBytes(n), { authTagLength: TAG_BYTES });
|
|
168
|
+
decipher.setAAD(ad);
|
|
169
|
+
decipher.setAuthTag(ciphertext.subarray(ciphertext.length - TAG_BYTES));
|
|
170
|
+
try {
|
|
171
|
+
return Buffer.concat([decipher.update(ciphertext.subarray(0, ciphertext.length - TAG_BYTES)), decipher.final()]);
|
|
172
|
+
} catch {
|
|
173
|
+
throw new NoiseError("aead");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
var CipherState = class {
|
|
177
|
+
#key;
|
|
178
|
+
#n;
|
|
179
|
+
/** `n` other than 0 is for tests of the counter limit. */
|
|
180
|
+
constructor(key = null, n = 0) {
|
|
181
|
+
this.#key = key;
|
|
182
|
+
this.#n = n;
|
|
183
|
+
}
|
|
184
|
+
get hasKey() {
|
|
185
|
+
return this.#key !== null;
|
|
186
|
+
}
|
|
187
|
+
get nonce() {
|
|
188
|
+
return this.#n;
|
|
189
|
+
}
|
|
190
|
+
/** Test-only view of the key, for the vectors. */
|
|
191
|
+
get keyBytes() {
|
|
192
|
+
return this.#key;
|
|
193
|
+
}
|
|
194
|
+
encryptWithAd(ad, plaintext) {
|
|
195
|
+
if (!this.#key) return Buffer.from(plaintext);
|
|
196
|
+
if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
|
|
197
|
+
const out = aeadSeal(this.#key, this.#n, ad, plaintext);
|
|
198
|
+
this.#n += 1;
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/** The counter moves only on success; any failure ends the session anyway. */
|
|
202
|
+
decryptWithAd(ad, ciphertext) {
|
|
203
|
+
if (!this.#key) return Buffer.from(ciphertext);
|
|
204
|
+
if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
|
|
205
|
+
const out = aeadOpen(this.#key, this.#n, ad, ciphertext);
|
|
206
|
+
this.#n += 1;
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
var SymmetricState = class _SymmetricState {
|
|
211
|
+
ck;
|
|
212
|
+
h;
|
|
213
|
+
cipher = new CipherState();
|
|
214
|
+
trace;
|
|
215
|
+
constructor(h, ck, trace) {
|
|
216
|
+
this.h = h;
|
|
217
|
+
this.ck = ck;
|
|
218
|
+
this.trace = trace;
|
|
219
|
+
}
|
|
220
|
+
static initialize(trace = null) {
|
|
221
|
+
const name = Buffer.from(PROTOCOL_NAME, "utf8");
|
|
222
|
+
const h = name.length <= 32 ? Buffer.concat([name, Buffer.alloc(32 - name.length)]) : sha256(name);
|
|
223
|
+
return new _SymmetricState(h, Buffer.from(h), trace);
|
|
224
|
+
}
|
|
225
|
+
clone() {
|
|
226
|
+
const copy = new _SymmetricState(Buffer.from(this.h), Buffer.from(this.ck), this.trace ? [...this.trace] : null);
|
|
227
|
+
copy.cipher = this.cipher;
|
|
228
|
+
return copy;
|
|
229
|
+
}
|
|
230
|
+
mark(step) {
|
|
231
|
+
this.trace?.push({ step, h: this.h.toString("hex"), ck: this.ck.toString("hex"), k: this.cipher.keyBytes?.toString("hex") ?? null });
|
|
232
|
+
}
|
|
233
|
+
mixHash(data) {
|
|
234
|
+
this.h = sha256(Buffer.concat([this.h, data]));
|
|
235
|
+
}
|
|
236
|
+
mixKey(ikm) {
|
|
237
|
+
const [ck, key] = hkdf(this.ck, ikm, 2);
|
|
238
|
+
this.ck = ck;
|
|
239
|
+
this.cipher = new CipherState(key);
|
|
240
|
+
}
|
|
241
|
+
mixKeyAndHash(ikm) {
|
|
242
|
+
const [ck, tempH, key] = hkdf(this.ck, ikm, 3);
|
|
243
|
+
this.ck = ck;
|
|
244
|
+
this.mixHash(tempH);
|
|
245
|
+
this.cipher = new CipherState(key);
|
|
246
|
+
}
|
|
247
|
+
encryptAndHash(plaintext) {
|
|
248
|
+
const out = this.cipher.encryptWithAd(this.h, plaintext);
|
|
249
|
+
this.mixHash(out);
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
decryptAndHash(ciphertext) {
|
|
253
|
+
const out = this.cipher.decryptWithAd(this.h, ciphertext);
|
|
254
|
+
this.mixHash(ciphertext);
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/** k_pc then k_cp. */
|
|
258
|
+
split() {
|
|
259
|
+
const [first, second] = hkdf(this.ck, Buffer.alloc(0), 2);
|
|
260
|
+
return [new CipherState(first), new CipherState(second)];
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
function initialized(transport, responderStatic, trace) {
|
|
264
|
+
const state = SymmetricState.initialize(trace);
|
|
265
|
+
state.mark("init");
|
|
266
|
+
state.mixHash(prologue(transport));
|
|
267
|
+
state.mark("prologue");
|
|
268
|
+
state.mixHash(responderStatic);
|
|
269
|
+
state.mark("s");
|
|
270
|
+
return state;
|
|
271
|
+
}
|
|
272
|
+
var RecordCipher = class {
|
|
273
|
+
#state;
|
|
274
|
+
constructor(state) {
|
|
275
|
+
this.#state = state;
|
|
276
|
+
}
|
|
277
|
+
get nonce() {
|
|
278
|
+
return this.#state.nonce;
|
|
279
|
+
}
|
|
280
|
+
seal(plaintext) {
|
|
281
|
+
if (plaintext.length > MAX_RECORD_PLAINTEXT) throw new NoiseError("record_too_large");
|
|
282
|
+
return this.#state.encryptWithAd(Buffer.alloc(0), plaintext);
|
|
283
|
+
}
|
|
284
|
+
open(record) {
|
|
285
|
+
if (record.length > MAX_RECORD_BYTES) throw new NoiseError("record_too_large");
|
|
286
|
+
return this.#state.decryptWithAd(Buffer.alloc(0), record);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function sessionKeys(state, side) {
|
|
290
|
+
const [pc, cp] = state.split();
|
|
291
|
+
const kPc = pc.keyBytes;
|
|
292
|
+
const kCp = cp.keyBytes;
|
|
293
|
+
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 };
|
|
294
|
+
}
|
|
295
|
+
function encodePayload1(conn, app) {
|
|
296
|
+
return Buffer.from(JSON.stringify({ v: 1, conn: b64u(conn), app }), "utf8");
|
|
297
|
+
}
|
|
298
|
+
function encodePayload2(conn) {
|
|
299
|
+
return Buffer.from(JSON.stringify({ v: 1, conn }), "utf8");
|
|
300
|
+
}
|
|
301
|
+
function jsonObject(bytes) {
|
|
302
|
+
try {
|
|
303
|
+
const value = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
304
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
var isConn = (value) => typeof value === "string" && fromB64u(value)?.length === CONN_BYTES;
|
|
310
|
+
function parsePayload1(bytes) {
|
|
311
|
+
if (bytes.length > MAX_HANDSHAKE_PAYLOAD) return null;
|
|
312
|
+
const value = jsonObject(bytes);
|
|
313
|
+
if (!value || value.v !== 1 || !isConn(value.conn)) return null;
|
|
314
|
+
if (value.app !== void 0 && (typeof value.app !== "string" || value.app.length > MAX_APP_CHARS)) return null;
|
|
315
|
+
return { v: 1, conn: value.conn, ...typeof value.app === "string" ? { app: value.app } : {} };
|
|
316
|
+
}
|
|
317
|
+
function checkPayload2(bytes, conn) {
|
|
318
|
+
const value = bytes.length <= MAX_HANDSHAKE_PAYLOAD ? jsonObject(bytes) : null;
|
|
319
|
+
if (!value || value.v !== 1) return "version";
|
|
320
|
+
return value.conn === conn ? "ok" : "conn";
|
|
321
|
+
}
|
|
322
|
+
function phoneHello(options, payload1, conn) {
|
|
323
|
+
if (!isPublicKey(options.remoteStatic)) throw new NoiseError("bad_point");
|
|
324
|
+
if (options.psk.length !== 32) throw new NoiseError("bad_psk");
|
|
325
|
+
if (payload1.length > MAX_HANDSHAKE_PAYLOAD) throw new NoiseError("payload_too_large");
|
|
326
|
+
const state = initialized(options.transport, options.remoteStatic, options.trace ?? null);
|
|
327
|
+
state.mixKeyAndHash(options.psk);
|
|
328
|
+
state.mark("msg1.psk");
|
|
329
|
+
const e = options.ephemeral ?? generateKeyPair();
|
|
330
|
+
state.mixHash(e.publicKey);
|
|
331
|
+
state.mixKey(e.publicKey);
|
|
332
|
+
state.mark("msg1.e");
|
|
333
|
+
state.mixKey(dh(e.privateKey, options.remoteStatic));
|
|
334
|
+
state.mark("msg1.es");
|
|
335
|
+
const message1 = Buffer.concat([e.publicKey, state.encryptAndHash(payload1)]);
|
|
336
|
+
state.mark("msg1.payload");
|
|
337
|
+
let done = false;
|
|
338
|
+
return {
|
|
339
|
+
message1,
|
|
340
|
+
finish(message2) {
|
|
341
|
+
if (done) throw new NoiseError("finished");
|
|
342
|
+
done = true;
|
|
343
|
+
if (message2.length < PUBLIC_KEY_BYTES + TAG_BYTES || message2.length > MAX_HANDSHAKE_MESSAGE) throw new NoiseError("message2_size");
|
|
344
|
+
const re = message2.subarray(0, PUBLIC_KEY_BYTES);
|
|
345
|
+
const ee = dh(e.privateKey, re);
|
|
346
|
+
state.mixHash(re);
|
|
347
|
+
state.mixKey(re);
|
|
348
|
+
state.mark("msg2.e");
|
|
349
|
+
state.mixKey(ee);
|
|
350
|
+
state.mark("msg2.ee");
|
|
351
|
+
const payload2 = state.decryptAndHash(message2.subarray(PUBLIC_KEY_BYTES));
|
|
352
|
+
state.mark("msg2.payload");
|
|
353
|
+
const check = checkPayload2(payload2, conn);
|
|
354
|
+
if (check !== "ok") throw new NoiseError(`payload2_${check}`);
|
|
355
|
+
return sessionKeys(state, "phone");
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function computerAccept(options, message1) {
|
|
360
|
+
if (message1.length < PUBLIC_KEY_BYTES + TAG_BYTES || message1.length > MAX_HANDSHAKE_MESSAGE) return null;
|
|
361
|
+
const re = message1.subarray(0, PUBLIC_KEY_BYTES);
|
|
362
|
+
let es;
|
|
363
|
+
try {
|
|
364
|
+
es = dh(options.staticKey.privateKey, re);
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const base = initialized(options.transport, options.staticKey.publicKey, options.trace ?? null);
|
|
369
|
+
let match = null;
|
|
370
|
+
for (const candidate of options.candidates) {
|
|
371
|
+
if (candidate.psk.length !== 32) continue;
|
|
372
|
+
const state2 = base.clone();
|
|
373
|
+
state2.mixKeyAndHash(candidate.psk);
|
|
374
|
+
state2.mark("msg1.psk");
|
|
375
|
+
state2.mixHash(re);
|
|
376
|
+
state2.mixKey(re);
|
|
377
|
+
state2.mark("msg1.e");
|
|
378
|
+
state2.mixKey(es);
|
|
379
|
+
state2.mark("msg1.es");
|
|
380
|
+
try {
|
|
381
|
+
const payload = state2.decryptAndHash(message1.subarray(PUBLIC_KEY_BYTES));
|
|
382
|
+
state2.mark("msg1.payload");
|
|
383
|
+
match ??= { id: candidate.id, state: state2, payload };
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!match) return null;
|
|
388
|
+
const payload1 = parsePayload1(match.payload);
|
|
389
|
+
if (!payload1) return null;
|
|
390
|
+
const { state } = match;
|
|
391
|
+
const e = options.ephemeral ?? generateKeyPair();
|
|
392
|
+
state.mixHash(e.publicKey);
|
|
393
|
+
state.mixKey(e.publicKey);
|
|
394
|
+
state.mark("msg2.e");
|
|
395
|
+
state.mixKey(dh(e.privateKey, re));
|
|
396
|
+
state.mark("msg2.ee");
|
|
397
|
+
const payload2 = options.payload2 ? options.payload2(payload1) : encodePayload2(payload1.conn);
|
|
398
|
+
const message2 = Buffer.concat([e.publicKey, state.encryptAndHash(payload2)]);
|
|
399
|
+
state.mark("msg2.payload");
|
|
400
|
+
if (options.trace && state.trace) options.trace.splice(0, options.trace.length, ...state.trace);
|
|
401
|
+
return { id: match.id, payload1, message2, keys: sessionKeys(state, "computer") };
|
|
402
|
+
}
|
|
403
|
+
var newConn = () => randomBytes(CONN_BYTES);
|
|
404
|
+
|
|
405
|
+
// modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts
|
|
406
|
+
var DESCRIPTOR_VERSION = 1;
|
|
407
|
+
var MAX_DESCRIPTOR_CHARS = 1024;
|
|
408
|
+
var MAX_RELAY_NODES = 4;
|
|
409
|
+
var RELAY_HOST = /^relay[1-9][0-9]?\.openmausbot\.com$/;
|
|
410
|
+
function encodeDescriptor(descriptor) {
|
|
411
|
+
const relay = descriptor.h && descriptor.r ? { h: descriptor.h, r: descriptor.r } : {};
|
|
412
|
+
const json = JSON.stringify({ v: 1, k: descriptor.k, ...relay, ...descriptor.routes ? { routes: descriptor.routes } : {} });
|
|
413
|
+
return b64u(Buffer.from(json, "utf8"));
|
|
414
|
+
}
|
|
415
|
+
function descriptorFor(transportPublicKey, relay, routes) {
|
|
416
|
+
const base = relay && relay.nodes.length > 0 ? { v: 1, k: b64u(transportPublicKey), h: b64u(relay.hostId), r: [...relay.nodes] } : { v: 1, k: b64u(transportPublicKey) };
|
|
417
|
+
return routes === "local" ? { ...base, routes: "local" } : base;
|
|
418
|
+
}
|
|
419
|
+
function parseDescriptor(input) {
|
|
420
|
+
const reject = (reason) => ({ outcome: "reject", reason });
|
|
421
|
+
let value = input;
|
|
422
|
+
if (typeof input === "string") {
|
|
423
|
+
if (input.length > MAX_DESCRIPTOR_CHARS) return reject("too_long");
|
|
424
|
+
const bytes = fromB64u(input);
|
|
425
|
+
if (!bytes) return reject("base64");
|
|
426
|
+
try {
|
|
427
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
428
|
+
} catch {
|
|
429
|
+
return reject("json");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("json");
|
|
433
|
+
const fields = value;
|
|
434
|
+
if (!Number.isSafeInteger(fields.v) || fields.v < 1) return reject("version");
|
|
435
|
+
if (fields.v > DESCRIPTOR_VERSION) return { outcome: "update" };
|
|
436
|
+
const transportKey = typeof fields.k === "string" ? fromB64u(fields.k) : null;
|
|
437
|
+
if (!transportKey || transportKey.length !== PUBLIC_KEY_BYTES || !isPublicKey(transportKey)) return reject("key");
|
|
438
|
+
const hasHost = fields.h !== void 0;
|
|
439
|
+
const hasRelays = fields.r !== void 0;
|
|
440
|
+
if (hasHost !== hasRelays) return reject("relay_pair");
|
|
441
|
+
if (fields.routes !== void 0 && fields.routes !== "any" && fields.routes !== "local") return reject("routes");
|
|
442
|
+
const routes = fields.routes === void 0 ? {} : { routes: fields.routes };
|
|
443
|
+
if (!hasHost) return { outcome: "ok", descriptor: { v: 1, k: fields.k, ...routes }, transportKey, hostId: null };
|
|
444
|
+
const hostId = typeof fields.h === "string" ? fromB64u(fields.h) : null;
|
|
445
|
+
if (!hostId || hostId.length !== HOST_ID_BYTES) return reject("host_id");
|
|
446
|
+
const relays = fields.r;
|
|
447
|
+
if (!Array.isArray(relays) || relays.length < 1 || relays.length > MAX_RELAY_NODES) return reject("relays");
|
|
448
|
+
if (!relays.every((host) => typeof host === "string" && RELAY_HOST.test(host))) return reject("relay_host");
|
|
449
|
+
return { outcome: "ok", descriptor: { v: 1, k: fields.k, h: fields.h, r: [...relays], ...routes }, transportKey, hostId };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts
|
|
453
|
+
var VERSION = 0;
|
|
454
|
+
var TYPE = { data: 0, windowUpdate: 1, ping: 2, goAway: 3 };
|
|
455
|
+
var FLAG = { syn: 1, ack: 2, fin: 4, rst: 8 };
|
|
456
|
+
var KNOWN_FLAGS = 15;
|
|
457
|
+
var GO_AWAY = { normal: 0, protocol: 1, internal: 2, removed: 3 };
|
|
458
|
+
var HEADER_BYTES = 12;
|
|
459
|
+
var MAX_DATA_PAYLOAD = 65507;
|
|
460
|
+
var INITIAL_WINDOW = 256 * 1024;
|
|
461
|
+
var STREAM_WINDOW = 2 * 1024 * 1024;
|
|
462
|
+
var OPEN_WINDOW_DELTA = STREAM_WINDOW - INITIAL_WINDOW;
|
|
463
|
+
var WINDOW_UPDATE_AFTER = 1024 * 1024;
|
|
464
|
+
var MAX_STREAMS = 64;
|
|
465
|
+
var MAX_STREAM_ID = 2 ** 31 - 1;
|
|
466
|
+
var PING_INTERVAL_MS = 2e4;
|
|
467
|
+
var PING_TIMEOUT_MS = 1e4;
|
|
468
|
+
var YamuxError = class extends Error {
|
|
469
|
+
reason;
|
|
470
|
+
constructor(reason) {
|
|
471
|
+
super(`yamux: ${reason}`);
|
|
472
|
+
this.reason = reason;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
function encodeFrame(frame) {
|
|
476
|
+
const payload = frame.type === TYPE.data ? frame.data ?? new Uint8Array(0) : new Uint8Array(0);
|
|
477
|
+
if (frame.type === TYPE.data && payload.length !== frame.length) throw new YamuxError("data_length");
|
|
478
|
+
if (payload.length > MAX_DATA_PAYLOAD) throw new YamuxError("data_too_large");
|
|
479
|
+
const out = Buffer.alloc(HEADER_BYTES + payload.length);
|
|
480
|
+
out.writeUInt8(VERSION, 0);
|
|
481
|
+
out.writeUInt8(frame.type, 1);
|
|
482
|
+
out.writeUInt16BE(frame.flags, 2);
|
|
483
|
+
out.writeUInt32BE(frame.streamId, 4);
|
|
484
|
+
out.writeUInt32BE(frame.length, 8);
|
|
485
|
+
out.set(payload, HEADER_BYTES);
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
function decodeFrame(bytes) {
|
|
489
|
+
if (bytes.length < HEADER_BYTES) throw new YamuxError("short_frame");
|
|
490
|
+
const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
491
|
+
if (view.readUInt8(0) !== VERSION) throw new YamuxError("version");
|
|
492
|
+
const type = view.readUInt8(1);
|
|
493
|
+
if (type > TYPE.goAway) throw new YamuxError("type");
|
|
494
|
+
const flags = view.readUInt16BE(2);
|
|
495
|
+
if ((flags & ~KNOWN_FLAGS) !== 0) throw new YamuxError("flags");
|
|
496
|
+
const streamId = view.readUInt32BE(4);
|
|
497
|
+
const length = view.readUInt32BE(8);
|
|
498
|
+
if (type === TYPE.data) {
|
|
499
|
+
if (length > MAX_DATA_PAYLOAD) throw new YamuxError("data_too_large");
|
|
500
|
+
if (bytes.length !== HEADER_BYTES + length) throw new YamuxError("data_length");
|
|
501
|
+
if (streamId === 0) throw new YamuxError("stream_zero");
|
|
502
|
+
return { type, flags, streamId, length, data: view.subarray(HEADER_BYTES) };
|
|
503
|
+
}
|
|
504
|
+
if (bytes.length !== HEADER_BYTES) throw new YamuxError("trailing_bytes");
|
|
505
|
+
if (type === TYPE.windowUpdate) {
|
|
506
|
+
if (streamId === 0) throw new YamuxError("stream_zero");
|
|
507
|
+
} else {
|
|
508
|
+
if (streamId !== 0) throw new YamuxError("session_frame_stream");
|
|
509
|
+
if (type === TYPE.ping && flags !== FLAG.syn && flags !== FLAG.ack) throw new YamuxError("ping_flags");
|
|
510
|
+
if (type === TYPE.goAway && flags !== 0) throw new YamuxError("goaway_flags");
|
|
511
|
+
}
|
|
512
|
+
return { type, flags, streamId, length };
|
|
513
|
+
}
|
|
514
|
+
var openFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.syn, streamId, length: OPEN_WINDOW_DELTA });
|
|
515
|
+
var acceptFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.ack, streamId, length: OPEN_WINDOW_DELTA });
|
|
516
|
+
var resetFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.rst, streamId, length: 0 });
|
|
517
|
+
var finFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.fin, streamId, length: 0 });
|
|
518
|
+
var windowFrame = (streamId, delta) => ({ type: TYPE.windowUpdate, flags: 0, streamId, length: delta });
|
|
519
|
+
var dataFrame = (streamId, data, flags = 0) => ({ type: TYPE.data, flags, streamId, length: data.length, data });
|
|
520
|
+
var pingFrame = (value, flags = FLAG.syn) => ({ type: TYPE.ping, flags, streamId: 0, length: value >>> 0 });
|
|
521
|
+
var goAwayFrame = (code) => ({ type: TYPE.goAway, flags: 0, streamId: 0, length: code });
|
|
522
|
+
var Mux = class {
|
|
523
|
+
role;
|
|
524
|
+
#events;
|
|
525
|
+
#streams = /* @__PURE__ */ new Map();
|
|
526
|
+
/** The highest ID the phone has opened. */
|
|
527
|
+
#highestId = 0;
|
|
528
|
+
#peerGoneAway = false;
|
|
529
|
+
#localGoneAway = false;
|
|
530
|
+
constructor(role, events) {
|
|
531
|
+
this.role = role;
|
|
532
|
+
this.#events = events;
|
|
533
|
+
}
|
|
534
|
+
get activeStreams() {
|
|
535
|
+
return this.#streams.size;
|
|
536
|
+
}
|
|
537
|
+
get peerGoneAway() {
|
|
538
|
+
return this.#peerGoneAway;
|
|
539
|
+
}
|
|
540
|
+
has(streamId) {
|
|
541
|
+
return this.#streams.has(streamId);
|
|
542
|
+
}
|
|
543
|
+
sendWindow(streamId) {
|
|
544
|
+
return this.#streams.get(streamId)?.sendWindow ?? 0;
|
|
545
|
+
}
|
|
546
|
+
/** The phone side. Data may follow at once, within the first 256 KiB. */
|
|
547
|
+
open() {
|
|
548
|
+
if (this.role !== "phone") throw new YamuxError("computer_open");
|
|
549
|
+
if (this.#localGoneAway || this.#peerGoneAway) throw new YamuxError("gone_away");
|
|
550
|
+
if (this.#streams.size >= MAX_STREAMS) throw new YamuxError("too_many_streams");
|
|
551
|
+
const streamId = this.#highestId === 0 ? 1 : this.#highestId + 2;
|
|
552
|
+
if (streamId > MAX_STREAM_ID) throw new YamuxError("ids_exhausted");
|
|
553
|
+
this.#highestId = streamId;
|
|
554
|
+
this.#streams.set(streamId, {
|
|
555
|
+
established: false,
|
|
556
|
+
receiveWindow: INITIAL_WINDOW + OPEN_WINDOW_DELTA,
|
|
557
|
+
sendWindow: INITIAL_WINDOW,
|
|
558
|
+
uncredited: 0,
|
|
559
|
+
localFin: false,
|
|
560
|
+
remoteFin: false
|
|
561
|
+
});
|
|
562
|
+
this.#events.send(openFrame(streamId));
|
|
563
|
+
return streamId;
|
|
564
|
+
}
|
|
565
|
+
/** The computer side, once the origin socket is connected. */
|
|
566
|
+
accept(streamId) {
|
|
567
|
+
const stream = this.#streams.get(streamId);
|
|
568
|
+
if (!stream || stream.established || this.role !== "computer") return;
|
|
569
|
+
stream.established = true;
|
|
570
|
+
stream.receiveWindow += OPEN_WINDOW_DELTA;
|
|
571
|
+
this.#events.send(acceptFrame(streamId));
|
|
572
|
+
}
|
|
573
|
+
/** Sends up to the window and the frame cap; returns the bytes taken. */
|
|
574
|
+
write(streamId, data) {
|
|
575
|
+
const stream = this.#streams.get(streamId);
|
|
576
|
+
if (!stream || stream.localFin || this.role === "computer" && !stream.established) return 0;
|
|
577
|
+
const size = Math.min(data.length, stream.sendWindow, MAX_DATA_PAYLOAD);
|
|
578
|
+
if (size === 0) return 0;
|
|
579
|
+
stream.sendWindow -= size;
|
|
580
|
+
this.#events.send(dataFrame(streamId, data.subarray(0, size)));
|
|
581
|
+
return size;
|
|
582
|
+
}
|
|
583
|
+
finish(streamId) {
|
|
584
|
+
const stream = this.#streams.get(streamId);
|
|
585
|
+
if (!stream || stream.localFin) return;
|
|
586
|
+
stream.localFin = true;
|
|
587
|
+
this.#events.send(finFrame(streamId));
|
|
588
|
+
this.#closeIfDone(streamId, stream);
|
|
589
|
+
}
|
|
590
|
+
reset(streamId) {
|
|
591
|
+
if (!this.#streams.delete(streamId)) return;
|
|
592
|
+
this.#events.send(resetFrame(streamId));
|
|
593
|
+
this.#events.reset(streamId);
|
|
594
|
+
}
|
|
595
|
+
/** The receiver handed `bytes` on (to the origin socket, or the HTTP stack). Grants them back in 1 MiB steps. */
|
|
596
|
+
consumed(streamId, bytes) {
|
|
597
|
+
const stream = this.#streams.get(streamId);
|
|
598
|
+
if (!stream || stream.remoteFin) return;
|
|
599
|
+
stream.uncredited += bytes;
|
|
600
|
+
if (stream.uncredited < WINDOW_UPDATE_AFTER) return;
|
|
601
|
+
const delta = stream.uncredited;
|
|
602
|
+
stream.uncredited = 0;
|
|
603
|
+
stream.receiveWindow += delta;
|
|
604
|
+
this.#events.send(windowFrame(streamId, delta));
|
|
605
|
+
}
|
|
606
|
+
ping(value) {
|
|
607
|
+
this.#events.send(pingFrame(value));
|
|
608
|
+
}
|
|
609
|
+
goAway(code = GO_AWAY.normal) {
|
|
610
|
+
this.#localGoneAway = true;
|
|
611
|
+
this.#events.send(goAwayFrame(code));
|
|
612
|
+
}
|
|
613
|
+
/** Sends GoAway(1); the caller closes with 4000. */
|
|
614
|
+
protocolError() {
|
|
615
|
+
this.goAway(GO_AWAY.protocol);
|
|
616
|
+
this.closeAll();
|
|
617
|
+
}
|
|
618
|
+
closeAll() {
|
|
619
|
+
const ids = [...this.#streams.keys()];
|
|
620
|
+
this.#streams.clear();
|
|
621
|
+
for (const id of ids) this.#events.reset(id);
|
|
622
|
+
}
|
|
623
|
+
receive(frame) {
|
|
624
|
+
if (frame.type === TYPE.ping) {
|
|
625
|
+
if (frame.flags === FLAG.syn) this.#events.send(pingFrame(frame.length, FLAG.ack));
|
|
626
|
+
else this.#events.pong?.(frame.length);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (frame.type === TYPE.goAway) {
|
|
630
|
+
this.#peerGoneAway = true;
|
|
631
|
+
this.#events.goAway?.(frame.length);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (frame.flags & FLAG.syn) {
|
|
635
|
+
this.#receiveOpen(frame);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (frame.flags & FLAG.ack) {
|
|
639
|
+
this.#receiveAck(frame);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const stream = this.#known(frame.streamId);
|
|
643
|
+
if (!stream) return;
|
|
644
|
+
this.#receiveOnStream(frame.streamId, stream, frame);
|
|
645
|
+
}
|
|
646
|
+
/** A stream that ended is ignored: an RST and the peer's last frames cross. An ID never opened is an error. */
|
|
647
|
+
#known(streamId) {
|
|
648
|
+
if (streamId % 2 === 0 || streamId > this.#highestId) throw new YamuxError("unknown_stream");
|
|
649
|
+
return this.#streams.get(streamId) ?? null;
|
|
650
|
+
}
|
|
651
|
+
#receiveOpen(frame) {
|
|
652
|
+
if (this.role === "phone") throw new YamuxError("computer_opened_stream");
|
|
653
|
+
if (frame.type !== TYPE.windowUpdate || frame.flags !== FLAG.syn) throw new YamuxError("open_flags");
|
|
654
|
+
if (frame.streamId % 2 === 0) throw new YamuxError("even_stream");
|
|
655
|
+
if (frame.streamId <= this.#highestId) throw new YamuxError("stream_reused");
|
|
656
|
+
this.#highestId = frame.streamId;
|
|
657
|
+
if (this.#localGoneAway || this.#streams.size >= MAX_STREAMS) {
|
|
658
|
+
this.#events.send(resetFrame(frame.streamId));
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
this.#streams.set(frame.streamId, {
|
|
662
|
+
established: false,
|
|
663
|
+
receiveWindow: INITIAL_WINDOW,
|
|
664
|
+
sendWindow: INITIAL_WINDOW + frame.length,
|
|
665
|
+
uncredited: 0,
|
|
666
|
+
localFin: false,
|
|
667
|
+
remoteFin: false
|
|
668
|
+
});
|
|
669
|
+
this.#events.opened?.(frame.streamId);
|
|
670
|
+
}
|
|
671
|
+
#receiveAck(frame) {
|
|
672
|
+
if (this.role === "computer") throw new YamuxError("phone_ack");
|
|
673
|
+
if (frame.type !== TYPE.windowUpdate || (frame.flags & ~(FLAG.ack | FLAG.fin | FLAG.rst)) !== 0) throw new YamuxError("ack_flags");
|
|
674
|
+
const stream = this.#known(frame.streamId);
|
|
675
|
+
if (!stream) return;
|
|
676
|
+
if (stream.established) throw new YamuxError("duplicate_ack");
|
|
677
|
+
stream.established = true;
|
|
678
|
+
this.#events.accepted?.(frame.streamId);
|
|
679
|
+
this.#receiveOnStream(frame.streamId, stream, { ...frame, flags: frame.flags & ~FLAG.ack });
|
|
680
|
+
}
|
|
681
|
+
#receiveOnStream(streamId, stream, frame) {
|
|
682
|
+
if (frame.flags & FLAG.rst) {
|
|
683
|
+
this.#streams.delete(streamId);
|
|
684
|
+
this.#events.reset(streamId);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (frame.type === TYPE.data) {
|
|
688
|
+
if (stream.remoteFin && (frame.length > 0 || frame.flags & FLAG.fin)) throw new YamuxError("data_after_fin");
|
|
689
|
+
if (frame.length > stream.receiveWindow) throw new YamuxError("window_exceeded");
|
|
690
|
+
stream.receiveWindow -= frame.length;
|
|
691
|
+
if (frame.length > 0) this.#events.data(streamId, frame.data);
|
|
692
|
+
} else if (frame.length > 0) {
|
|
693
|
+
stream.sendWindow += frame.length;
|
|
694
|
+
this.#events.writable(streamId);
|
|
695
|
+
}
|
|
696
|
+
if (frame.flags & FLAG.fin) {
|
|
697
|
+
if (stream.remoteFin) throw new YamuxError("data_after_fin");
|
|
698
|
+
stream.remoteFin = true;
|
|
699
|
+
this.#events.ended(streamId);
|
|
700
|
+
this.#closeIfDone(streamId, stream);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
#closeIfDone(streamId, stream) {
|
|
704
|
+
if (stream.localFin && stream.remoteFin) this.#streams.delete(streamId);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// modules/openmaus-protocol/vendor/companion/src/e2e/session.ts
|
|
709
|
+
import { Duplex } from "node:stream";
|
|
710
|
+
var SEND_BUFFER_HIGH = 1024 * 1024;
|
|
711
|
+
var SessionClosed = class extends Error {
|
|
712
|
+
code;
|
|
713
|
+
constructor(code) {
|
|
714
|
+
super(`e2e session closed (${code})`);
|
|
715
|
+
this.code = code;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
var noop = () => {
|
|
719
|
+
};
|
|
720
|
+
var MuxStream = class extends Duplex {
|
|
721
|
+
streamId;
|
|
722
|
+
#session;
|
|
723
|
+
#pending = null;
|
|
724
|
+
#pendingDone = null;
|
|
725
|
+
#ungranted = 0;
|
|
726
|
+
constructor(session, streamId) {
|
|
727
|
+
super({ allowHalfOpen: true, readableHighWaterMark: 256 * 1024 });
|
|
728
|
+
this.#session = session;
|
|
729
|
+
this.streamId = streamId;
|
|
730
|
+
this.on("error", noop);
|
|
731
|
+
}
|
|
732
|
+
_write(chunk, _encoding, done) {
|
|
733
|
+
this.#pending = chunk;
|
|
734
|
+
this.#pendingDone = done;
|
|
735
|
+
this.flush();
|
|
736
|
+
}
|
|
737
|
+
_final(done) {
|
|
738
|
+
this.#session.mux?.finish(this.streamId);
|
|
739
|
+
done();
|
|
740
|
+
}
|
|
741
|
+
_read() {
|
|
742
|
+
if (this.#ungranted === 0) return;
|
|
743
|
+
const bytes = this.#ungranted;
|
|
744
|
+
this.#ungranted = 0;
|
|
745
|
+
this.#session.mux?.consumed(this.streamId, bytes);
|
|
746
|
+
}
|
|
747
|
+
_destroy(error, done) {
|
|
748
|
+
const mux = this.#session.mux;
|
|
749
|
+
if (mux?.has(this.streamId)) mux.reset(this.streamId);
|
|
750
|
+
this.#session.forget(this.streamId);
|
|
751
|
+
const pendingDone = this.#pendingDone;
|
|
752
|
+
this.#pending = null;
|
|
753
|
+
this.#pendingDone = null;
|
|
754
|
+
pendingDone?.(error ?? new Error("stream reset"));
|
|
755
|
+
done(error);
|
|
756
|
+
}
|
|
757
|
+
/** Granted back as the reader takes it, so a slow reader slows the sender. */
|
|
758
|
+
deliver(data) {
|
|
759
|
+
if (this.push(data)) this.#session.mux?.consumed(this.streamId, data.length);
|
|
760
|
+
else this.#ungranted += data.length;
|
|
761
|
+
}
|
|
762
|
+
remoteEnded() {
|
|
763
|
+
this.push(null);
|
|
764
|
+
}
|
|
765
|
+
remoteReset() {
|
|
766
|
+
if (!this.destroyed) this.destroy(new Error("stream reset"));
|
|
767
|
+
}
|
|
768
|
+
flush() {
|
|
769
|
+
const mux = this.#session.mux;
|
|
770
|
+
while (this.#pending && this.#pending.length > 0) {
|
|
771
|
+
if (!mux || !mux.has(this.streamId)) return void this.destroy(new Error("stream reset"));
|
|
772
|
+
if (this.#session.congested) return this.#session.whenDrained(() => this.flush());
|
|
773
|
+
const taken = mux.write(this.streamId, this.#pending);
|
|
774
|
+
if (taken === 0) return;
|
|
775
|
+
this.#pending = this.#pending.subarray(taken);
|
|
776
|
+
}
|
|
777
|
+
const done = this.#pendingDone;
|
|
778
|
+
this.#pending = null;
|
|
779
|
+
this.#pendingDone = null;
|
|
780
|
+
done?.();
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
var E2ESession = class _E2ESession {
|
|
784
|
+
role;
|
|
785
|
+
#transport;
|
|
786
|
+
#streams = /* @__PURE__ */ new Map();
|
|
787
|
+
#timers;
|
|
788
|
+
#state = "handshake";
|
|
789
|
+
#keys = null;
|
|
790
|
+
#mux = null;
|
|
791
|
+
#accept = null;
|
|
792
|
+
#deadline = null;
|
|
793
|
+
#pingTimer = null;
|
|
794
|
+
#pongTimer = null;
|
|
795
|
+
#awaitingPong = null;
|
|
796
|
+
#pingCounter = 0;
|
|
797
|
+
#drainWaiters = [];
|
|
798
|
+
#settleReady;
|
|
799
|
+
#settleClosed;
|
|
800
|
+
#connectOrigin = null;
|
|
801
|
+
#removed = false;
|
|
802
|
+
/** The computer side: which device (or pairing window) this is. */
|
|
803
|
+
peer = null;
|
|
804
|
+
ready;
|
|
805
|
+
closed;
|
|
806
|
+
constructor(role, transport, timers) {
|
|
807
|
+
this.role = role;
|
|
808
|
+
this.#transport = transport;
|
|
809
|
+
this.#timers = {
|
|
810
|
+
pingIntervalMs: timers.pingIntervalMs ?? PING_INTERVAL_MS,
|
|
811
|
+
pingTimeoutMs: timers.pingTimeoutMs ?? PING_TIMEOUT_MS,
|
|
812
|
+
handshakeDeadlineMs: timers.handshakeDeadlineMs ?? HANDSHAKE_DEADLINE_MS
|
|
813
|
+
};
|
|
814
|
+
this.ready = new Promise((resolve, reject) => {
|
|
815
|
+
this.#settleReady = (error) => error ? reject(error) : resolve();
|
|
816
|
+
});
|
|
817
|
+
this.ready.catch(noop);
|
|
818
|
+
this.closed = new Promise((resolve) => {
|
|
819
|
+
this.#settleClosed = resolve;
|
|
820
|
+
});
|
|
821
|
+
this.#deadline = setTimeout(() => this.close(CLOSE.deadline), this.#timers.handshakeDeadlineMs);
|
|
822
|
+
this.#deadline.unref?.();
|
|
823
|
+
}
|
|
824
|
+
/** Waits for message 1. Anything wrong with it closes 4001 and nothing else is sent. */
|
|
825
|
+
static computer(options) {
|
|
826
|
+
const session = new _E2ESession("computer", options.transport, options);
|
|
827
|
+
session.#connectOrigin = options.connectOrigin;
|
|
828
|
+
session.#accept = (message) => {
|
|
829
|
+
const accepted = computerAccept({ staticKey: options.staticKey, transport: options.transportByte, candidates: options.candidates() }, message);
|
|
830
|
+
if (!accepted) return session.close(CLOSE.rejected);
|
|
831
|
+
options.transport.send(accepted.message2);
|
|
832
|
+
session.peer = accepted.id;
|
|
833
|
+
session.#start(accepted.keys);
|
|
834
|
+
options.opened?.(accepted.id, accepted.payload1);
|
|
835
|
+
};
|
|
836
|
+
return session;
|
|
837
|
+
}
|
|
838
|
+
/** Sends message 1 now. `ready` rejects with a NoiseError when the computer can't be verified: a hard stop. */
|
|
839
|
+
static phone(options) {
|
|
840
|
+
const session = new _E2ESession("phone", options.transport, options);
|
|
841
|
+
const conn = newConn();
|
|
842
|
+
const hello = phoneHello(
|
|
843
|
+
{ remoteStatic: options.remoteStatic, psk: options.psk, transport: options.transportByte, ephemeral: options.ephemeral },
|
|
844
|
+
encodePayload1(conn, options.app),
|
|
845
|
+
b64u(conn)
|
|
846
|
+
);
|
|
847
|
+
session.#accept = (message) => {
|
|
848
|
+
let keys;
|
|
849
|
+
try {
|
|
850
|
+
keys = hello.finish(message);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
session.#settleReady(error);
|
|
853
|
+
return session.close(CLOSE.protocol);
|
|
854
|
+
}
|
|
855
|
+
session.#start(keys);
|
|
856
|
+
};
|
|
857
|
+
options.transport.send(hello.message1);
|
|
858
|
+
return session;
|
|
859
|
+
}
|
|
860
|
+
get mux() {
|
|
861
|
+
return this.#mux;
|
|
862
|
+
}
|
|
863
|
+
/** The phone side: the computer said, inside the encryption, that this device was removed. Only this, or a 4001 on
|
|
864
|
+
* a direct route, means "pair again"; a bare 4001 through the relay may be the relay's own and means "keep trying". */
|
|
865
|
+
get removed() {
|
|
866
|
+
return this.#removed;
|
|
867
|
+
}
|
|
868
|
+
get state() {
|
|
869
|
+
return this.#state;
|
|
870
|
+
}
|
|
871
|
+
/** Records each way so far, for diagnostics. */
|
|
872
|
+
get records() {
|
|
873
|
+
return { sent: this.#keys?.send.nonce ?? 0, received: this.#keys?.receive.nonce ?? 0 };
|
|
874
|
+
}
|
|
875
|
+
get congested() {
|
|
876
|
+
return this.#transport.bufferedAmount > SEND_BUFFER_HIGH;
|
|
877
|
+
}
|
|
878
|
+
get activeStreams() {
|
|
879
|
+
return this.#streams.size;
|
|
880
|
+
}
|
|
881
|
+
whenDrained(listener) {
|
|
882
|
+
this.#drainWaiters.push(listener);
|
|
883
|
+
if (this.#drainWaiters.length > 1) return;
|
|
884
|
+
this.#transport.onceDrained(() => {
|
|
885
|
+
const waiters = this.#drainWaiters;
|
|
886
|
+
this.#drainWaiters = [];
|
|
887
|
+
for (const waiter of waiters) waiter();
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
forget(streamId) {
|
|
891
|
+
this.#streams.delete(streamId);
|
|
892
|
+
}
|
|
893
|
+
/** Every message the transport receives, in order. */
|
|
894
|
+
receive(message, binary) {
|
|
895
|
+
if (this.#state === "closed") return;
|
|
896
|
+
if (this.#state === "handshake") {
|
|
897
|
+
const accept = this.#accept;
|
|
898
|
+
this.#accept = null;
|
|
899
|
+
if (!binary || !accept) return this.close(this.role === "computer" ? CLOSE.rejected : CLOSE.protocol);
|
|
900
|
+
return accept(Buffer.from(message));
|
|
901
|
+
}
|
|
902
|
+
if (!binary) return this.close(CLOSE.protocol);
|
|
903
|
+
let plaintext;
|
|
904
|
+
try {
|
|
905
|
+
plaintext = this.#keys.receive.open(message);
|
|
906
|
+
} catch {
|
|
907
|
+
return this.close(CLOSE.protocol);
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
this.#mux.receive(decodeFrame(plaintext));
|
|
911
|
+
} catch (error) {
|
|
912
|
+
if (!(error instanceof YamuxError)) throw error;
|
|
913
|
+
this.#mux?.protocolError();
|
|
914
|
+
this.close(CLOSE.protocol);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
/** The phone side. */
|
|
918
|
+
openStream() {
|
|
919
|
+
if (this.#state !== "open" || !this.#mux) throw new SessionClosed(CLOSE.normal);
|
|
920
|
+
const streamId = this.#mux.open();
|
|
921
|
+
const stream = new MuxStream(this, streamId);
|
|
922
|
+
this.#streams.set(streamId, stream);
|
|
923
|
+
return stream;
|
|
924
|
+
}
|
|
925
|
+
/** The computer side: the device was removed. The phone hears it authenticated, then the 4001 close. */
|
|
926
|
+
remove() {
|
|
927
|
+
if (this.#state === "open") this.#mux?.goAway(GO_AWAY.removed);
|
|
928
|
+
this.close(CLOSE.rejected);
|
|
929
|
+
}
|
|
930
|
+
/** A planned close (quit, sleep, restart): GoAway first, so the peer knows it isn't an error. */
|
|
931
|
+
shutdown() {
|
|
932
|
+
if (this.#state === "open") this.#mux?.goAway(GO_AWAY.normal);
|
|
933
|
+
this.close(CLOSE.normal);
|
|
934
|
+
}
|
|
935
|
+
close(code, reason) {
|
|
936
|
+
if (this.#state === "closed") return;
|
|
937
|
+
this.#transport.close(code, reason);
|
|
938
|
+
this.#end(code);
|
|
939
|
+
}
|
|
940
|
+
/** The transport closed under us. */
|
|
941
|
+
transportClosed(code) {
|
|
942
|
+
this.#end(code);
|
|
943
|
+
}
|
|
944
|
+
#end(code) {
|
|
945
|
+
if (this.#state === "closed") return;
|
|
946
|
+
const wasHandshake = this.#state === "handshake";
|
|
947
|
+
this.#state = "closed";
|
|
948
|
+
for (const timer of [this.#deadline, this.#pongTimer]) if (timer) clearTimeout(timer);
|
|
949
|
+
if (this.#pingTimer) clearInterval(this.#pingTimer);
|
|
950
|
+
this.#mux?.closeAll();
|
|
951
|
+
for (const stream of this.#streams.values()) stream.remoteReset();
|
|
952
|
+
this.#streams.clear();
|
|
953
|
+
const waiters = this.#drainWaiters;
|
|
954
|
+
this.#drainWaiters = [];
|
|
955
|
+
for (const waiter of waiters) waiter();
|
|
956
|
+
if (wasHandshake) this.#settleReady(new SessionClosed(code));
|
|
957
|
+
this.#settleClosed(code);
|
|
958
|
+
}
|
|
959
|
+
#start(keys) {
|
|
960
|
+
if (this.#deadline) clearTimeout(this.#deadline);
|
|
961
|
+
this.#keys = keys;
|
|
962
|
+
this.#state = "open";
|
|
963
|
+
this.#mux = new Mux(this.role, {
|
|
964
|
+
send: (frame) => this.#send(frame),
|
|
965
|
+
opened: (streamId) => this.#openOrigin(streamId),
|
|
966
|
+
data: (streamId, data) => this.#streams.get(streamId)?.deliver(Buffer.from(data.buffer, data.byteOffset, data.byteLength)),
|
|
967
|
+
ended: (streamId) => this.#streams.get(streamId)?.remoteEnded(),
|
|
968
|
+
reset: (streamId) => {
|
|
969
|
+
const stream = this.#streams.get(streamId);
|
|
970
|
+
this.#streams.delete(streamId);
|
|
971
|
+
stream?.remoteReset();
|
|
972
|
+
},
|
|
973
|
+
writable: (streamId) => this.#streams.get(streamId)?.flush(),
|
|
974
|
+
goAway: (code) => {
|
|
975
|
+
if (code === GO_AWAY.removed && this.role === "phone") this.#removed = true;
|
|
976
|
+
else if (code !== GO_AWAY.normal) this.close(CLOSE.protocol);
|
|
977
|
+
},
|
|
978
|
+
pong: (value) => {
|
|
979
|
+
if (value !== this.#awaitingPong) return;
|
|
980
|
+
this.#awaitingPong = null;
|
|
981
|
+
if (this.#pongTimer) clearTimeout(this.#pongTimer);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
this.#pingTimer = setInterval(() => this.#ping(), this.#timers.pingIntervalMs);
|
|
985
|
+
this.#pingTimer.unref?.();
|
|
986
|
+
this.#settleReady();
|
|
987
|
+
}
|
|
988
|
+
#ping() {
|
|
989
|
+
if (this.#awaitingPong !== null || !this.#mux) return;
|
|
990
|
+
this.#pingCounter = this.#pingCounter + 1 >>> 0;
|
|
991
|
+
this.#awaitingPong = this.#pingCounter;
|
|
992
|
+
this.#mux.ping(this.#pingCounter);
|
|
993
|
+
this.#pongTimer = setTimeout(() => this.close(CLOSE.goingAway), this.#timers.pingTimeoutMs);
|
|
994
|
+
this.#pongTimer.unref?.();
|
|
995
|
+
}
|
|
996
|
+
#send(frame) {
|
|
997
|
+
if (this.#state !== "open" || !this.#keys) return;
|
|
998
|
+
let record;
|
|
999
|
+
try {
|
|
1000
|
+
record = this.#keys.send.seal(encodeFrame(frame));
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
if (error instanceof NoiseError) return this.close(CLOSE.normal);
|
|
1003
|
+
throw error;
|
|
1004
|
+
}
|
|
1005
|
+
this.#transport.send(record);
|
|
1006
|
+
if (this.#keys.send.nonce >= NONCE_LIMIT - 1024) queueMicrotask(() => this.close(CLOSE.normal));
|
|
1007
|
+
}
|
|
1008
|
+
#openOrigin(streamId) {
|
|
1009
|
+
const stream = new MuxStream(this, streamId);
|
|
1010
|
+
this.#streams.set(streamId, stream);
|
|
1011
|
+
const connect = this.#connectOrigin;
|
|
1012
|
+
if (!connect) return void stream.destroy();
|
|
1013
|
+
connect().then(
|
|
1014
|
+
(socket) => {
|
|
1015
|
+
if (stream.destroyed || this.#state !== "open") return void socket.destroy();
|
|
1016
|
+
this.#mux?.accept(streamId);
|
|
1017
|
+
socket.on("error", () => stream.destroy());
|
|
1018
|
+
socket.on("close", () => {
|
|
1019
|
+
if (!socket.readableEnded) stream.destroy();
|
|
1020
|
+
});
|
|
1021
|
+
stream.on("close", () => socket.destroy());
|
|
1022
|
+
stream.pipe(socket);
|
|
1023
|
+
socket.pipe(stream);
|
|
1024
|
+
},
|
|
1025
|
+
() => stream.destroy()
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts
|
|
1031
|
+
var DIRECT_PROTOCOL = "omb-e2e.v1";
|
|
1032
|
+
var RELAY_PROTOCOL = "omb-relay.v1";
|
|
1033
|
+
var DRAIN_POLL_MS = 5;
|
|
1034
|
+
var sendable = (code) => code === 1e3 || code >= 3e3 && code <= 4999;
|
|
1035
|
+
function webSocketTransport(socket) {
|
|
1036
|
+
return {
|
|
1037
|
+
send: (message) => socket.send(message),
|
|
1038
|
+
close: (code, reason) => sendable(code) ? socket.close(code, reason) : socket.close(),
|
|
1039
|
+
get bufferedAmount() {
|
|
1040
|
+
return socket.bufferedAmount;
|
|
1041
|
+
},
|
|
1042
|
+
// The built-in client has no drain event.
|
|
1043
|
+
onceDrained(listener) {
|
|
1044
|
+
const check = () => {
|
|
1045
|
+
if (socket.readyState !== WebSocket.OPEN || socket.bufferedAmount <= SEND_BUFFER_HIGH / 4) listener();
|
|
1046
|
+
else setTimeout(check, DRAIN_POLL_MS);
|
|
1047
|
+
};
|
|
1048
|
+
check();
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function dial(options) {
|
|
1053
|
+
const relayed = Boolean(options.relay);
|
|
1054
|
+
if (relayed && options.transportByte !== TRANSPORT_RELAY) throw new Error("the relay route uses the relay prologue");
|
|
1055
|
+
const socket = new WebSocket(options.url, [relayed ? RELAY_PROTOCOL : DIRECT_PROTOCOL]);
|
|
1056
|
+
socket.binaryType = "arraybuffer";
|
|
1057
|
+
const transport = webSocketTransport(socket);
|
|
1058
|
+
return new Promise((resolve, reject) => {
|
|
1059
|
+
let session = null;
|
|
1060
|
+
const timer = setTimeout(() => {
|
|
1061
|
+
reject(new SessionClosed(4002));
|
|
1062
|
+
socket.close();
|
|
1063
|
+
}, (options.timeoutMs ?? HANDSHAKE_DEADLINE_MS) + (relayed ? 1e4 : 0));
|
|
1064
|
+
const start = () => {
|
|
1065
|
+
session = E2ESession.phone({ ...options, transport });
|
|
1066
|
+
session.ready.then(
|
|
1067
|
+
() => {
|
|
1068
|
+
clearTimeout(timer);
|
|
1069
|
+
resolve(session);
|
|
1070
|
+
},
|
|
1071
|
+
(error) => {
|
|
1072
|
+
clearTimeout(timer);
|
|
1073
|
+
reject(error);
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
};
|
|
1077
|
+
socket.onopen = () => {
|
|
1078
|
+
if (!options.relay) return start();
|
|
1079
|
+
socket.send(JSON.stringify({ t: "phone", v: 1, hid: b64u(options.relay.hostId), adm: b64u(options.relay.adm) }));
|
|
1080
|
+
};
|
|
1081
|
+
socket.onmessage = (event) => {
|
|
1082
|
+
const binary = typeof event.data !== "string";
|
|
1083
|
+
const data = binary ? new Uint8Array(event.data) : Buffer.from(event.data, "utf8");
|
|
1084
|
+
if (session) return session.receive(data, binary);
|
|
1085
|
+
let ready = false;
|
|
1086
|
+
try {
|
|
1087
|
+
ready = !binary && JSON.parse(String(event.data)).t === "ready";
|
|
1088
|
+
} catch {
|
|
1089
|
+
ready = false;
|
|
1090
|
+
}
|
|
1091
|
+
if (!ready) {
|
|
1092
|
+
clearTimeout(timer);
|
|
1093
|
+
reject(new SessionClosed(4400));
|
|
1094
|
+
return socket.close();
|
|
1095
|
+
}
|
|
1096
|
+
start();
|
|
1097
|
+
};
|
|
1098
|
+
socket.onclose = (event) => {
|
|
1099
|
+
clearTimeout(timer);
|
|
1100
|
+
if (session) session.transportClosed(event.code);
|
|
1101
|
+
else reject(new SessionClosed(event.code));
|
|
1102
|
+
};
|
|
1103
|
+
socket.onerror = () => {
|
|
1104
|
+
};
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
export {
|
|
1108
|
+
CLOSE,
|
|
1109
|
+
CONN_BYTES,
|
|
1110
|
+
CipherState,
|
|
1111
|
+
DESCRIPTOR_VERSION,
|
|
1112
|
+
DIRECT_PROTOCOL,
|
|
1113
|
+
E2ESession,
|
|
1114
|
+
FLAG,
|
|
1115
|
+
GO_AWAY,
|
|
1116
|
+
HANDSHAKE_DEADLINE_MS,
|
|
1117
|
+
HEADER_BYTES,
|
|
1118
|
+
HOST_ID_BYTES,
|
|
1119
|
+
INITIAL_WINDOW,
|
|
1120
|
+
KEY_ID_BYTES,
|
|
1121
|
+
MAX_APP_CHARS,
|
|
1122
|
+
MAX_DATA_PAYLOAD,
|
|
1123
|
+
MAX_DESCRIPTOR_CHARS,
|
|
1124
|
+
MAX_HANDSHAKE_MESSAGE,
|
|
1125
|
+
MAX_HANDSHAKE_PAYLOAD,
|
|
1126
|
+
MAX_RECORD_BYTES,
|
|
1127
|
+
MAX_RECORD_PLAINTEXT,
|
|
1128
|
+
MAX_RELAY_NODES,
|
|
1129
|
+
MAX_STREAMS,
|
|
1130
|
+
MAX_STREAM_ID,
|
|
1131
|
+
Mux,
|
|
1132
|
+
MuxStream,
|
|
1133
|
+
NONCE_LIMIT,
|
|
1134
|
+
NoiseError,
|
|
1135
|
+
OPEN_WINDOW_DELTA,
|
|
1136
|
+
PING_INTERVAL_MS,
|
|
1137
|
+
PING_TIMEOUT_MS,
|
|
1138
|
+
PROTOCOL_NAME,
|
|
1139
|
+
PUBLIC_KEY_BYTES,
|
|
1140
|
+
RELAY_HOST,
|
|
1141
|
+
RELAY_PROTOCOL,
|
|
1142
|
+
RELAY_TOKEN_MAX_CHARS,
|
|
1143
|
+
RELAY_TOKEN_TTL_S,
|
|
1144
|
+
RecordCipher,
|
|
1145
|
+
SEND_BUFFER_HIGH,
|
|
1146
|
+
STREAM_WINDOW,
|
|
1147
|
+
SessionClosed,
|
|
1148
|
+
SymmetricState,
|
|
1149
|
+
TAG_BYTES,
|
|
1150
|
+
TRANSPORT_DIRECT,
|
|
1151
|
+
TRANSPORT_RELAY,
|
|
1152
|
+
TYPE,
|
|
1153
|
+
VERSION,
|
|
1154
|
+
WINDOW_UPDATE_AFTER,
|
|
1155
|
+
YamuxError,
|
|
1156
|
+
acceptFrame,
|
|
1157
|
+
admission,
|
|
1158
|
+
admissionHash,
|
|
1159
|
+
aeadOpen,
|
|
1160
|
+
aeadSeal,
|
|
1161
|
+
b64u,
|
|
1162
|
+
checkPayload2,
|
|
1163
|
+
computerAccept,
|
|
1164
|
+
credentialHash,
|
|
1165
|
+
credentialHashFromHex,
|
|
1166
|
+
dataFrame,
|
|
1167
|
+
decodeFrame,
|
|
1168
|
+
descriptorFor,
|
|
1169
|
+
dh,
|
|
1170
|
+
dial,
|
|
1171
|
+
ed25519PrivateKey,
|
|
1172
|
+
ed25519PublicKey,
|
|
1173
|
+
ed25519RawPublicKey,
|
|
1174
|
+
encodeDescriptor,
|
|
1175
|
+
encodeFrame,
|
|
1176
|
+
encodePayload1,
|
|
1177
|
+
encodePayload2,
|
|
1178
|
+
finFrame,
|
|
1179
|
+
fromB64u,
|
|
1180
|
+
generateKeyPair,
|
|
1181
|
+
goAwayFrame,
|
|
1182
|
+
hkdf,
|
|
1183
|
+
hmacSha256,
|
|
1184
|
+
isPublicKey,
|
|
1185
|
+
keyId,
|
|
1186
|
+
keyPairFromPrivate,
|
|
1187
|
+
mintRelayToken,
|
|
1188
|
+
newConn,
|
|
1189
|
+
nonceBytes,
|
|
1190
|
+
openFrame,
|
|
1191
|
+
parseDescriptor,
|
|
1192
|
+
parsePayload1,
|
|
1193
|
+
phoneHello,
|
|
1194
|
+
pingFrame,
|
|
1195
|
+
prologue,
|
|
1196
|
+
psk,
|
|
1197
|
+
resetFrame,
|
|
1198
|
+
sameBytes,
|
|
1199
|
+
sha256,
|
|
1200
|
+
verifyRelayToken,
|
|
1201
|
+
webSocketTransport,
|
|
1202
|
+
windowFrame
|
|
1203
|
+
};
|