@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,456 @@
|
|
|
1
|
+
// Noise_NKpsk0_P256_AESGCM_SHA256 (Noise Protocol Framework rev 34, with P-256 as the DH function) and the
|
|
2
|
+
// transport records that follow it. Wire spec: companion/src/e2e/README.md, "Handshake" and "Transport messages".
|
|
3
|
+
import { createCipheriv, createDecipheriv, createECDH, hkdfSync, randomBytes } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import { b64u, fromB64u, sha256 } from "./credentials.ts";
|
|
6
|
+
|
|
7
|
+
export const PROTOCOL_NAME = "Noise_NKpsk0_P256_AESGCM_SHA256";
|
|
8
|
+
export const PUBLIC_KEY_BYTES = 65;
|
|
9
|
+
export const TAG_BYTES = 16;
|
|
10
|
+
export const MAX_RECORD_BYTES = 65_535;
|
|
11
|
+
export const MAX_RECORD_PLAINTEXT = MAX_RECORD_BYTES - TAG_BYTES;
|
|
12
|
+
export const MAX_HANDSHAKE_PAYLOAD = 512;
|
|
13
|
+
export const MAX_HANDSHAKE_MESSAGE = PUBLIC_KEY_BYTES + MAX_HANDSHAKE_PAYLOAD + TAG_BYTES;
|
|
14
|
+
export const CONN_BYTES = 16;
|
|
15
|
+
export const MAX_APP_CHARS = 40;
|
|
16
|
+
/** A sender closes the session (1000) before a direction's counter reaches this. */
|
|
17
|
+
export const NONCE_LIMIT = 2 ** 32;
|
|
18
|
+
export const HANDSHAKE_DEADLINE_MS = 10_000;
|
|
19
|
+
|
|
20
|
+
export const TRANSPORT_RELAY = 0x01;
|
|
21
|
+
export const TRANSPORT_DIRECT = 0x02;
|
|
22
|
+
export type TransportByte = typeof TRANSPORT_RELAY | typeof TRANSPORT_DIRECT;
|
|
23
|
+
|
|
24
|
+
export const CLOSE = {
|
|
25
|
+
normal: 1000,
|
|
26
|
+
goingAway: 1001,
|
|
27
|
+
protocol: 4000,
|
|
28
|
+
rejected: 4001,
|
|
29
|
+
deadline: 4002,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/** Every handshake or record failure. The reason is for tests and local logs; it never goes on the wire. */
|
|
33
|
+
export class NoiseError extends Error {
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
constructor(reason: string) {
|
|
36
|
+
super(`e2e: ${reason}`);
|
|
37
|
+
this.reason = reason;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function prologue(transport: TransportByte): Buffer {
|
|
42
|
+
return Buffer.concat([Buffer.from("omb-e2e/1", "utf8"), Buffer.from([0x00, transport])]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface KeyPair {
|
|
46
|
+
privateKey: Buffer;
|
|
47
|
+
publicKey: Buffer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ecdh = (privateKey?: Uint8Array) => {
|
|
51
|
+
const curve = createECDH("prime256v1");
|
|
52
|
+
if (privateKey) curve.setPrivateKey(privateKey);
|
|
53
|
+
else curve.generateKeys();
|
|
54
|
+
return curve;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Throws for a scalar outside [1, n). */
|
|
58
|
+
export function keyPairFromPrivate(privateKey: Uint8Array): KeyPair {
|
|
59
|
+
if (privateKey.length !== 32) throw new NoiseError("bad_private_key");
|
|
60
|
+
return { privateKey: Buffer.from(privateKey), publicKey: ecdh(privateKey).getPublicKey(null, "uncompressed") };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function generateKeyPair(): KeyPair {
|
|
64
|
+
const curve = ecdh();
|
|
65
|
+
const raw = curve.getPrivateKey();
|
|
66
|
+
return { privateKey: Buffer.concat([Buffer.alloc(32 - raw.length), raw]), publicKey: curve.getPublicKey(null, "uncompressed") };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The 32-byte X coordinate of the shared point. Only the 65-byte uncompressed form: OpenSSL also accepts
|
|
70
|
+
* compressed and hybrid encodings, which the wire forbids. OpenSSL rejects off-curve points and coordinates at
|
|
71
|
+
* or above p. */
|
|
72
|
+
export function dh(privateKey: Uint8Array, publicKey: Uint8Array): Buffer {
|
|
73
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES || publicKey[0] !== 0x04) throw new NoiseError("bad_point");
|
|
74
|
+
try {
|
|
75
|
+
return ecdh(privateKey).computeSecret(publicKey);
|
|
76
|
+
} catch {
|
|
77
|
+
throw new NoiseError("bad_point");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const CHECK_SCALAR = Buffer.alloc(32, 1);
|
|
82
|
+
|
|
83
|
+
export function isPublicKey(bytes: Uint8Array): boolean {
|
|
84
|
+
try {
|
|
85
|
+
dh(CHECK_SCALAR, bytes);
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Noise's HKDF is RFC 5869 HKDF-SHA256 with the chaining key as salt and empty info. */
|
|
93
|
+
export function hkdf(chainingKey: Uint8Array, ikm: Uint8Array, outputs: 2 | 3): Buffer[] {
|
|
94
|
+
const out = Buffer.from(hkdfSync("sha256", ikm, chainingKey, Buffer.alloc(0), 32 * outputs));
|
|
95
|
+
return Array.from({ length: outputs }, (_, index) => out.subarray(index * 32, index * 32 + 32));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function nonceBytes(n: number): Buffer {
|
|
99
|
+
const nonce = Buffer.alloc(12);
|
|
100
|
+
nonce.writeBigUInt64BE(BigInt(n), 4);
|
|
101
|
+
return nonce;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function aeadSeal(key: Uint8Array, n: number, ad: Uint8Array, plaintext: Uint8Array): Buffer {
|
|
105
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonceBytes(n), { authTagLength: TAG_BYTES });
|
|
106
|
+
cipher.setAAD(ad);
|
|
107
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function aeadOpen(key: Uint8Array, n: number, ad: Uint8Array, ciphertext: Uint8Array): Buffer {
|
|
111
|
+
if (ciphertext.length < TAG_BYTES) throw new NoiseError("short_ciphertext");
|
|
112
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonceBytes(n), { authTagLength: TAG_BYTES });
|
|
113
|
+
decipher.setAAD(ad);
|
|
114
|
+
decipher.setAuthTag(ciphertext.subarray(ciphertext.length - TAG_BYTES));
|
|
115
|
+
try {
|
|
116
|
+
return Buffer.concat([decipher.update(ciphertext.subarray(0, ciphertext.length - TAG_BYTES)), decipher.final()]);
|
|
117
|
+
} catch {
|
|
118
|
+
throw new NoiseError("aead");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class CipherState {
|
|
123
|
+
#key: Buffer | null;
|
|
124
|
+
#n: number;
|
|
125
|
+
|
|
126
|
+
/** `n` other than 0 is for tests of the counter limit. */
|
|
127
|
+
constructor(key: Buffer | null = null, n = 0) {
|
|
128
|
+
this.#key = key;
|
|
129
|
+
this.#n = n;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get hasKey(): boolean {
|
|
133
|
+
return this.#key !== null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get nonce(): number {
|
|
137
|
+
return this.#n;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Test-only view of the key, for the vectors. */
|
|
141
|
+
get keyBytes(): Buffer | null {
|
|
142
|
+
return this.#key;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
encryptWithAd(ad: Uint8Array, plaintext: Uint8Array): Buffer {
|
|
146
|
+
if (!this.#key) return Buffer.from(plaintext);
|
|
147
|
+
if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
|
|
148
|
+
const out = aeadSeal(this.#key, this.#n, ad, plaintext);
|
|
149
|
+
this.#n += 1;
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The counter moves only on success; any failure ends the session anyway. */
|
|
154
|
+
decryptWithAd(ad: Uint8Array, ciphertext: Uint8Array): Buffer {
|
|
155
|
+
if (!this.#key) return Buffer.from(ciphertext);
|
|
156
|
+
if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
|
|
157
|
+
const out = aeadOpen(this.#key, this.#n, ad, ciphertext);
|
|
158
|
+
this.#n += 1;
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface TraceStep {
|
|
164
|
+
step: string;
|
|
165
|
+
h: string;
|
|
166
|
+
ck: string;
|
|
167
|
+
/** The cipher key after this step, or null before the first MixKey. */
|
|
168
|
+
k: string | null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class SymmetricState {
|
|
172
|
+
ck: Buffer;
|
|
173
|
+
h: Buffer;
|
|
174
|
+
cipher = new CipherState();
|
|
175
|
+
readonly trace: TraceStep[] | null;
|
|
176
|
+
|
|
177
|
+
constructor(h: Buffer, ck: Buffer, trace: TraceStep[] | null) {
|
|
178
|
+
this.h = h;
|
|
179
|
+
this.ck = ck;
|
|
180
|
+
this.trace = trace;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static initialize(trace: TraceStep[] | null = null): SymmetricState {
|
|
184
|
+
const name = Buffer.from(PROTOCOL_NAME, "utf8");
|
|
185
|
+
const h = name.length <= 32 ? Buffer.concat([name, Buffer.alloc(32 - name.length)]) : sha256(name);
|
|
186
|
+
return new SymmetricState(h, Buffer.from(h), trace);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
clone(): SymmetricState {
|
|
190
|
+
const copy = new SymmetricState(Buffer.from(this.h), Buffer.from(this.ck), this.trace ? [...this.trace] : null);
|
|
191
|
+
copy.cipher = this.cipher;
|
|
192
|
+
return copy;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
mark(step: string): void {
|
|
196
|
+
this.trace?.push({ step, h: this.h.toString("hex"), ck: this.ck.toString("hex"), k: this.cipher.keyBytes?.toString("hex") ?? null });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
mixHash(data: Uint8Array): void {
|
|
200
|
+
this.h = sha256(Buffer.concat([this.h, data]));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
mixKey(ikm: Uint8Array): void {
|
|
204
|
+
const [ck, key] = hkdf(this.ck, ikm, 2);
|
|
205
|
+
this.ck = ck;
|
|
206
|
+
this.cipher = new CipherState(key);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
mixKeyAndHash(ikm: Uint8Array): void {
|
|
210
|
+
const [ck, tempH, key] = hkdf(this.ck, ikm, 3);
|
|
211
|
+
this.ck = ck;
|
|
212
|
+
this.mixHash(tempH);
|
|
213
|
+
this.cipher = new CipherState(key);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
encryptAndHash(plaintext: Uint8Array): Buffer {
|
|
217
|
+
const out = this.cipher.encryptWithAd(this.h, plaintext);
|
|
218
|
+
this.mixHash(out);
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
decryptAndHash(ciphertext: Uint8Array): Buffer {
|
|
223
|
+
const out = this.cipher.decryptWithAd(this.h, ciphertext);
|
|
224
|
+
this.mixHash(ciphertext);
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** k_pc then k_cp. */
|
|
229
|
+
split(): [CipherState, CipherState] {
|
|
230
|
+
const [first, second] = hkdf(this.ck, Buffer.alloc(0), 2);
|
|
231
|
+
return [new CipherState(first), new CipherState(second)];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Both sides, before message 1: the prologue, then the responder's pre-message `s`. */
|
|
236
|
+
function initialized(transport: TransportByte, responderStatic: Uint8Array, trace: TraceStep[] | null): SymmetricState {
|
|
237
|
+
const state = SymmetricState.initialize(trace);
|
|
238
|
+
state.mark("init");
|
|
239
|
+
state.mixHash(prologue(transport));
|
|
240
|
+
state.mark("prologue");
|
|
241
|
+
state.mixHash(responderStatic);
|
|
242
|
+
state.mark("s");
|
|
243
|
+
return state;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** One direction's records. Each binary WebSocket message after the handshake is exactly one. */
|
|
247
|
+
export class RecordCipher {
|
|
248
|
+
readonly #state: CipherState;
|
|
249
|
+
|
|
250
|
+
constructor(state: CipherState) {
|
|
251
|
+
this.#state = state;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
get nonce(): number {
|
|
255
|
+
return this.#state.nonce;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
seal(plaintext: Uint8Array): Buffer {
|
|
259
|
+
if (plaintext.length > MAX_RECORD_PLAINTEXT) throw new NoiseError("record_too_large");
|
|
260
|
+
return this.#state.encryptWithAd(Buffer.alloc(0), plaintext);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
open(record: Uint8Array): Buffer {
|
|
264
|
+
if (record.length > MAX_RECORD_BYTES) throw new NoiseError("record_too_large");
|
|
265
|
+
return this.#state.decryptWithAd(Buffer.alloc(0), record);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface SessionKeys {
|
|
270
|
+
send: RecordCipher;
|
|
271
|
+
receive: RecordCipher;
|
|
272
|
+
handshakeHash: Buffer;
|
|
273
|
+
/** Test-only, for the vectors. */
|
|
274
|
+
kPc: Buffer;
|
|
275
|
+
kCp: Buffer;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function sessionKeys(state: SymmetricState, side: "phone" | "computer"): SessionKeys {
|
|
279
|
+
const [pc, cp] = state.split();
|
|
280
|
+
const kPc = pc.keyBytes as Buffer;
|
|
281
|
+
const kCp = cp.keyBytes as Buffer;
|
|
282
|
+
return side === "phone"
|
|
283
|
+
? { send: new RecordCipher(pc), receive: new RecordCipher(cp), handshakeHash: state.h, kPc, kCp }
|
|
284
|
+
: { send: new RecordCipher(cp), receive: new RecordCipher(pc), handshakeHash: state.h, kPc, kCp };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface Payload1 {
|
|
288
|
+
v: 1;
|
|
289
|
+
conn: string;
|
|
290
|
+
app?: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function encodePayload1(conn: Uint8Array, app: string): Buffer {
|
|
294
|
+
return Buffer.from(JSON.stringify({ v: 1, conn: b64u(conn), app }), "utf8");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function encodePayload2(conn: string): Buffer {
|
|
298
|
+
return Buffer.from(JSON.stringify({ v: 1, conn }), "utf8");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function jsonObject(bytes: Uint8Array): Record<string, unknown> | null {
|
|
302
|
+
try {
|
|
303
|
+
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
304
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const isConn = (value: unknown): value is string =>
|
|
311
|
+
typeof value === "string" && fromB64u(value)?.length === CONN_BYTES;
|
|
312
|
+
|
|
313
|
+
/** Key order and whitespace are free; the fields are not. */
|
|
314
|
+
export function parsePayload1(bytes: Uint8Array): Payload1 | null {
|
|
315
|
+
if (bytes.length > MAX_HANDSHAKE_PAYLOAD) return null;
|
|
316
|
+
const value = jsonObject(bytes);
|
|
317
|
+
if (!value || value.v !== 1 || !isConn(value.conn)) return null;
|
|
318
|
+
if (value.app !== undefined && (typeof value.app !== "string" || value.app.length > MAX_APP_CHARS)) return null;
|
|
319
|
+
return { v: 1, conn: value.conn, ...(typeof value.app === "string" ? { app: value.app } : {}) };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export type Payload2Check = "ok" | "version" | "conn";
|
|
323
|
+
|
|
324
|
+
export function checkPayload2(bytes: Uint8Array, conn: string): Payload2Check {
|
|
325
|
+
const value = bytes.length <= MAX_HANDSHAKE_PAYLOAD ? jsonObject(bytes) : null;
|
|
326
|
+
if (!value || value.v !== 1) return "version";
|
|
327
|
+
return value.conn === conn ? "ok" : "conn";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface PhoneHandshakeOptions {
|
|
331
|
+
/** The pinned transport key `T.pub`. */
|
|
332
|
+
remoteStatic: Uint8Array;
|
|
333
|
+
psk: Uint8Array;
|
|
334
|
+
transport: TransportByte;
|
|
335
|
+
/** Fixed in tests and vectors; random otherwise. */
|
|
336
|
+
ephemeral?: KeyPair;
|
|
337
|
+
trace?: TraceStep[];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface PhoneHello {
|
|
341
|
+
message1: Buffer;
|
|
342
|
+
/** Throws NoiseError on any failure: every one is a hard stop. */
|
|
343
|
+
finish(message2: Uint8Array): SessionKeys;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The initiator. `payload1` is exact bytes so vectors can pin them; `conn` is what message 2 must echo. */
|
|
347
|
+
export function phoneHello(options: PhoneHandshakeOptions, payload1: Uint8Array, conn: string): PhoneHello {
|
|
348
|
+
if (!isPublicKey(options.remoteStatic)) throw new NoiseError("bad_point");
|
|
349
|
+
if (options.psk.length !== 32) throw new NoiseError("bad_psk");
|
|
350
|
+
if (payload1.length > MAX_HANDSHAKE_PAYLOAD) throw new NoiseError("payload_too_large");
|
|
351
|
+
const state = initialized(options.transport, options.remoteStatic, options.trace ?? null);
|
|
352
|
+
state.mixKeyAndHash(options.psk);
|
|
353
|
+
state.mark("msg1.psk");
|
|
354
|
+
const e = options.ephemeral ?? generateKeyPair();
|
|
355
|
+
state.mixHash(e.publicKey);
|
|
356
|
+
state.mixKey(e.publicKey);
|
|
357
|
+
state.mark("msg1.e");
|
|
358
|
+
state.mixKey(dh(e.privateKey, options.remoteStatic));
|
|
359
|
+
state.mark("msg1.es");
|
|
360
|
+
const message1 = Buffer.concat([e.publicKey, state.encryptAndHash(payload1)]);
|
|
361
|
+
state.mark("msg1.payload");
|
|
362
|
+
let done = false;
|
|
363
|
+
return {
|
|
364
|
+
message1,
|
|
365
|
+
finish(message2) {
|
|
366
|
+
if (done) throw new NoiseError("finished");
|
|
367
|
+
done = true;
|
|
368
|
+
if (message2.length < PUBLIC_KEY_BYTES + TAG_BYTES || message2.length > MAX_HANDSHAKE_MESSAGE) throw new NoiseError("message2_size");
|
|
369
|
+
const re = message2.subarray(0, PUBLIC_KEY_BYTES);
|
|
370
|
+
const ee = dh(e.privateKey, re);
|
|
371
|
+
state.mixHash(re);
|
|
372
|
+
state.mixKey(re);
|
|
373
|
+
state.mark("msg2.e");
|
|
374
|
+
state.mixKey(ee);
|
|
375
|
+
state.mark("msg2.ee");
|
|
376
|
+
const payload2 = state.decryptAndHash(message2.subarray(PUBLIC_KEY_BYTES));
|
|
377
|
+
state.mark("msg2.payload");
|
|
378
|
+
const check = checkPayload2(payload2, conn);
|
|
379
|
+
if (check !== "ok") throw new NoiseError(`payload2_${check}`);
|
|
380
|
+
return sessionKeys(state, "phone");
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export interface PskCandidate<Id> {
|
|
386
|
+
id: Id;
|
|
387
|
+
psk: Uint8Array;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface ComputerAccept<Id> {
|
|
391
|
+
id: Id;
|
|
392
|
+
payload1: Payload1;
|
|
393
|
+
message2: Buffer;
|
|
394
|
+
keys: SessionKeys;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface ComputerHandshakeOptions<Id> {
|
|
398
|
+
staticKey: KeyPair;
|
|
399
|
+
transport: TransportByte;
|
|
400
|
+
/** Every device allowed on this transport, plus the open pairing window. */
|
|
401
|
+
candidates: ReadonlyArray<PskCandidate<Id>>;
|
|
402
|
+
ephemeral?: KeyPair;
|
|
403
|
+
trace?: TraceStep[];
|
|
404
|
+
/** Test-only: forges payload2 for the must-fail vectors. */
|
|
405
|
+
payload2?: (payload1: Payload1) => Uint8Array;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The responder. Null means close with 4001 and send nothing else: no psk matched, a bad point, a bad size or a
|
|
409
|
+
* bad payload1. Every candidate is tried, so time does not depend on which one matched. */
|
|
410
|
+
export function computerAccept<Id>(options: ComputerHandshakeOptions<Id>, message1: Uint8Array): ComputerAccept<Id> | null {
|
|
411
|
+
if (message1.length < PUBLIC_KEY_BYTES + TAG_BYTES || message1.length > MAX_HANDSHAKE_MESSAGE) return null;
|
|
412
|
+
const re = message1.subarray(0, PUBLIC_KEY_BYTES);
|
|
413
|
+
let es: Buffer;
|
|
414
|
+
try {
|
|
415
|
+
es = dh(options.staticKey.privateKey, re);
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const base = initialized(options.transport, options.staticKey.publicKey, options.trace ?? null);
|
|
420
|
+
let match: { id: Id; state: SymmetricState; payload: Buffer } | null = null;
|
|
421
|
+
for (const candidate of options.candidates) {
|
|
422
|
+
if (candidate.psk.length !== 32) continue;
|
|
423
|
+
const state = base.clone();
|
|
424
|
+
state.mixKeyAndHash(candidate.psk);
|
|
425
|
+
state.mark("msg1.psk");
|
|
426
|
+
state.mixHash(re);
|
|
427
|
+
state.mixKey(re);
|
|
428
|
+
state.mark("msg1.e");
|
|
429
|
+
state.mixKey(es);
|
|
430
|
+
state.mark("msg1.es");
|
|
431
|
+
try {
|
|
432
|
+
const payload = state.decryptAndHash(message1.subarray(PUBLIC_KEY_BYTES));
|
|
433
|
+
state.mark("msg1.payload");
|
|
434
|
+
match ??= { id: candidate.id, state, payload };
|
|
435
|
+
} catch {
|
|
436
|
+
// Not this device.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!match) return null;
|
|
440
|
+
const payload1 = parsePayload1(match.payload);
|
|
441
|
+
if (!payload1) return null;
|
|
442
|
+
const { state } = match;
|
|
443
|
+
const e = options.ephemeral ?? generateKeyPair();
|
|
444
|
+
state.mixHash(e.publicKey);
|
|
445
|
+
state.mixKey(e.publicKey);
|
|
446
|
+
state.mark("msg2.e");
|
|
447
|
+
state.mixKey(dh(e.privateKey, re));
|
|
448
|
+
state.mark("msg2.ee");
|
|
449
|
+
const payload2 = options.payload2 ? options.payload2(payload1) : encodePayload2(payload1.conn);
|
|
450
|
+
const message2 = Buffer.concat([e.publicKey, state.encryptAndHash(payload2)]);
|
|
451
|
+
state.mark("msg2.payload");
|
|
452
|
+
if (options.trace && state.trace) options.trace.splice(0, options.trace.length, ...state.trace);
|
|
453
|
+
return { id: match.id, payload1, message2, keys: sessionKeys(state, "computer") };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export const newConn = (): Buffer => randomBytes(CONN_BYTES);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The reference phone: dials a route with Node's built-in WebSocket client and returns an open session. Used by the
|
|
2
|
+
// interop and integration tests, and by a desktop reaching another desktop.
|
|
3
|
+
import { b64u } from "./credentials.ts";
|
|
4
|
+
import { E2ESession, SEND_BUFFER_HIGH, SessionClosed, type RecordTransport } from "./session.ts";
|
|
5
|
+
import { HANDSHAKE_DEADLINE_MS, TRANSPORT_RELAY, type KeyPair, type TransportByte } from "./noise.ts";
|
|
6
|
+
|
|
7
|
+
export const DIRECT_PROTOCOL = "omb-e2e.v1";
|
|
8
|
+
export const RELAY_PROTOCOL = "omb-relay.v1";
|
|
9
|
+
const DRAIN_POLL_MS = 5;
|
|
10
|
+
|
|
11
|
+
export interface DialOptions {
|
|
12
|
+
/** `ws://host:port/v1/e2e` for Wi-Fi and Tailscale, `wss://relayN.openmausbot.com/v1/phone` for the relay. */
|
|
13
|
+
url: string;
|
|
14
|
+
transportByte: TransportByte;
|
|
15
|
+
remoteStatic: Uint8Array;
|
|
16
|
+
psk: Uint8Array;
|
|
17
|
+
app: string;
|
|
18
|
+
/** On the relay: the hostId and this device's admission value, sent before the handshake. */
|
|
19
|
+
relay?: { hostId: Uint8Array; adm: Uint8Array };
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Test-only. */
|
|
22
|
+
ephemeral?: KeyPair;
|
|
23
|
+
pingIntervalMs?: number;
|
|
24
|
+
pingTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Codes a WebSocket client may send; any other closes without one. */
|
|
28
|
+
const sendable = (code: number) => code === 1000 || (code >= 3000 && code <= 4999);
|
|
29
|
+
|
|
30
|
+
/** A built-in WebSocket client as a record transport. */
|
|
31
|
+
export function webSocketTransport(socket: WebSocket): RecordTransport {
|
|
32
|
+
return {
|
|
33
|
+
send: (message) => socket.send(message),
|
|
34
|
+
close: (code, reason) => (sendable(code) ? socket.close(code, reason) : socket.close()),
|
|
35
|
+
get bufferedAmount() {
|
|
36
|
+
return socket.bufferedAmount;
|
|
37
|
+
},
|
|
38
|
+
// The built-in client has no drain event.
|
|
39
|
+
onceDrained(listener) {
|
|
40
|
+
const check = () => {
|
|
41
|
+
if (socket.readyState !== WebSocket.OPEN || socket.bufferedAmount <= SEND_BUFFER_HIGH / 4) listener();
|
|
42
|
+
else setTimeout(check, DRAIN_POLL_MS);
|
|
43
|
+
};
|
|
44
|
+
check();
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Rejects with SessionClosed (the close code, e.g. 4001 or a relay 4404) or a NoiseError (a hard stop). */
|
|
50
|
+
export function dial(options: DialOptions): Promise<E2ESession<never>> {
|
|
51
|
+
const relayed = Boolean(options.relay);
|
|
52
|
+
if (relayed && options.transportByte !== TRANSPORT_RELAY) throw new Error("the relay route uses the relay prologue");
|
|
53
|
+
const socket = new WebSocket(options.url, [relayed ? RELAY_PROTOCOL : DIRECT_PROTOCOL]);
|
|
54
|
+
socket.binaryType = "arraybuffer";
|
|
55
|
+
const transport = webSocketTransport(socket);
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let session: E2ESession<never> | null = null;
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
reject(new SessionClosed(4002));
|
|
60
|
+
socket.close();
|
|
61
|
+
}, (options.timeoutMs ?? HANDSHAKE_DEADLINE_MS) + (relayed ? 10_000 : 0));
|
|
62
|
+
const start = () => {
|
|
63
|
+
session = E2ESession.phone({ ...options, transport });
|
|
64
|
+
session.ready.then(
|
|
65
|
+
() => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
resolve(session as E2ESession<never>);
|
|
68
|
+
},
|
|
69
|
+
(error: Error) => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
reject(error);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
socket.onopen = () => {
|
|
76
|
+
if (!options.relay) return start();
|
|
77
|
+
socket.send(JSON.stringify({ t: "phone", v: 1, hid: b64u(options.relay.hostId), adm: b64u(options.relay.adm) }));
|
|
78
|
+
};
|
|
79
|
+
socket.onmessage = (event: MessageEvent) => {
|
|
80
|
+
const binary = typeof event.data !== "string";
|
|
81
|
+
const data = binary ? new Uint8Array(event.data as ArrayBuffer) : Buffer.from(event.data as string, "utf8");
|
|
82
|
+
if (session) return session.receive(data, binary);
|
|
83
|
+
// Before the session: only the relay's ready.
|
|
84
|
+
let ready = false;
|
|
85
|
+
try {
|
|
86
|
+
ready = !binary && (JSON.parse(String(event.data)) as { t?: unknown }).t === "ready";
|
|
87
|
+
} catch {
|
|
88
|
+
ready = false;
|
|
89
|
+
}
|
|
90
|
+
if (!ready) {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
reject(new SessionClosed(4400));
|
|
93
|
+
return socket.close();
|
|
94
|
+
}
|
|
95
|
+
start();
|
|
96
|
+
};
|
|
97
|
+
socket.onclose = (event: CloseEvent) => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
if (session) session.transportClosed(event.code);
|
|
100
|
+
else reject(new SessionClosed(event.code));
|
|
101
|
+
};
|
|
102
|
+
socket.onerror = () => {};
|
|
103
|
+
});
|
|
104
|
+
}
|