handmux 0.23.0 → 0.25.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/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PrivateStateStore } from '../privateStateStore.js';
|
|
2
|
+
function copyState(value) {
|
|
3
|
+
return structuredClone(value);
|
|
4
|
+
}
|
|
5
|
+
export class MemoryBridgeStateStore {
|
|
6
|
+
#state = null;
|
|
7
|
+
load() {
|
|
8
|
+
return this.#state ? copyState(this.#state) : null;
|
|
9
|
+
}
|
|
10
|
+
save(state) {
|
|
11
|
+
this.#state = copyState(state);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class FileBridgeStateStore {
|
|
15
|
+
#store;
|
|
16
|
+
constructor(file) {
|
|
17
|
+
this.#store = new PrivateStateStore(file);
|
|
18
|
+
}
|
|
19
|
+
load() {
|
|
20
|
+
return this.#store.readStrict();
|
|
21
|
+
}
|
|
22
|
+
save(state) {
|
|
23
|
+
this.#store.write(state);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DEFAULT_BRIDGE_LIMITS } from './bridgeTypes.js';
|
|
6
|
+
const PROTOCOL_VERSION = 1;
|
|
7
|
+
const NAME_RE = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
8
|
+
const MAX_HANDSHAKE_BYTES = 64 * 1024;
|
|
9
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
10
|
+
const WIRE_OVERHEAD_BYTES = 16 * 1024;
|
|
11
|
+
function record(value) {
|
|
12
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
? value : null;
|
|
14
|
+
}
|
|
15
|
+
function bounded(value, max) {
|
|
16
|
+
return typeof value === 'string' && value.length > 0 && value.length <= max;
|
|
17
|
+
}
|
|
18
|
+
function validChannelName(value) {
|
|
19
|
+
return typeof value === 'string' && NAME_RE.test(value) && !value.startsWith('handmux.');
|
|
20
|
+
}
|
|
21
|
+
function errorMessage(error) {
|
|
22
|
+
return (error instanceof Error ? error.message : String(error)).slice(0, 4096);
|
|
23
|
+
}
|
|
24
|
+
function nonceProof(authToken, nonce) {
|
|
25
|
+
return crypto.createHmac('sha256', authToken).update(nonce).digest('base64url');
|
|
26
|
+
}
|
|
27
|
+
function equalProof(actual, expected) {
|
|
28
|
+
if (typeof actual !== 'string')
|
|
29
|
+
return false;
|
|
30
|
+
const first = Buffer.from(actual);
|
|
31
|
+
const second = Buffer.from(expected);
|
|
32
|
+
return first.length === second.length && crypto.timingSafeEqual(first, second);
|
|
33
|
+
}
|
|
34
|
+
function wirePeer(socket, maxBytes, onMessage) {
|
|
35
|
+
const peer = {
|
|
36
|
+
socket,
|
|
37
|
+
maxBytes,
|
|
38
|
+
buffer: Buffer.alloc(0),
|
|
39
|
+
closed: false,
|
|
40
|
+
send(value) {
|
|
41
|
+
if (peer.closed || socket.destroyed)
|
|
42
|
+
throw new Error('Bridge transport is closed');
|
|
43
|
+
const encoded = Buffer.from(`${JSON.stringify(value)}\n`);
|
|
44
|
+
if (encoded.length > peer.maxBytes)
|
|
45
|
+
throw new Error('Bridge transport frame exceeds limit');
|
|
46
|
+
socket.write(encoded);
|
|
47
|
+
},
|
|
48
|
+
close() {
|
|
49
|
+
if (peer.closed)
|
|
50
|
+
return;
|
|
51
|
+
peer.closed = true;
|
|
52
|
+
socket.destroy();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
socket.on('data', (chunk) => {
|
|
56
|
+
if (peer.closed)
|
|
57
|
+
return;
|
|
58
|
+
peer.buffer = Buffer.concat([peer.buffer, chunk]);
|
|
59
|
+
if (peer.buffer.length > peer.maxBytes && !peer.buffer.includes(0x0a)) {
|
|
60
|
+
peer.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
while (true) {
|
|
64
|
+
const newline = peer.buffer.indexOf(0x0a);
|
|
65
|
+
if (newline < 0)
|
|
66
|
+
break;
|
|
67
|
+
const frame = peer.buffer.subarray(0, newline);
|
|
68
|
+
peer.buffer = peer.buffer.subarray(newline + 1);
|
|
69
|
+
if (!frame.length)
|
|
70
|
+
continue;
|
|
71
|
+
if (frame.length > peer.maxBytes) {
|
|
72
|
+
peer.close();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
onMessage(JSON.parse(frame.toString('utf8')));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
peer.close();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return peer;
|
|
85
|
+
}
|
|
86
|
+
function validCandidate(value) {
|
|
87
|
+
const candidate = record(value);
|
|
88
|
+
const process = record(candidate?.process);
|
|
89
|
+
if (!candidate || !bounded(candidate.paneId, 256) || !bounded(candidate.attachmentId, 512)
|
|
90
|
+
|| (candidate.sessionId !== undefined && !bounded(candidate.sessionId, 1024))
|
|
91
|
+
|| (candidate.implementationVersion !== undefined
|
|
92
|
+
&& (!Number.isSafeInteger(candidate.implementationVersion)
|
|
93
|
+
|| Number(candidate.implementationVersion) <= 0))
|
|
94
|
+
|| !process || !Number.isSafeInteger(process.pid) || Number(process.pid) <= 0)
|
|
95
|
+
return false;
|
|
96
|
+
return (process.startedAt === undefined
|
|
97
|
+
|| (typeof process.startedAt === 'number' && Number.isFinite(process.startedAt)))
|
|
98
|
+
&& (process.tty === undefined || bounded(process.tty, 1024));
|
|
99
|
+
}
|
|
100
|
+
function validRun(value, adapterId, paneId) {
|
|
101
|
+
const run = record(value);
|
|
102
|
+
return run !== null && run.agentId === adapterId && run.paneId === paneId
|
|
103
|
+
&& bounded(run.runId, 256)
|
|
104
|
+
&& (run.implementationVersion === undefined
|
|
105
|
+
|| (Number.isSafeInteger(run.implementationVersion)
|
|
106
|
+
&& Number(run.implementationVersion) > 0))
|
|
107
|
+
&& (run.sessionId === undefined || bounded(run.sessionId, 1024));
|
|
108
|
+
}
|
|
109
|
+
function validLimits(value) {
|
|
110
|
+
const limits = record(value);
|
|
111
|
+
if (!limits)
|
|
112
|
+
return false;
|
|
113
|
+
return Object.entries(DEFAULT_BRIDGE_LIMITS).every(([name, maximum]) => {
|
|
114
|
+
const actual = limits[name];
|
|
115
|
+
return typeof actual === 'number' && Number.isFinite(actual) && actual > 0 && actual <= maximum;
|
|
116
|
+
}) && Number(limits.defaultRequestTimeoutMs) <= Number(limits.maxRequestTimeoutMs);
|
|
117
|
+
}
|
|
118
|
+
function result(peer, id, value) {
|
|
119
|
+
if (bounded(id, 256))
|
|
120
|
+
peer.send({ type: 'result', id, ok: true, value });
|
|
121
|
+
}
|
|
122
|
+
function failure(peer, id, error) {
|
|
123
|
+
if (bounded(id, 256))
|
|
124
|
+
peer.send({ type: 'result', id, ok: false, error: errorMessage(error) });
|
|
125
|
+
}
|
|
126
|
+
async function liveSocket(socketPath) {
|
|
127
|
+
if (!fs.existsSync(socketPath))
|
|
128
|
+
return false;
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const socket = net.createConnection(socketPath);
|
|
131
|
+
let settled = false;
|
|
132
|
+
const finish = (value, error) => {
|
|
133
|
+
if (settled)
|
|
134
|
+
return;
|
|
135
|
+
settled = true;
|
|
136
|
+
socket.destroy();
|
|
137
|
+
if (error)
|
|
138
|
+
reject(error);
|
|
139
|
+
else
|
|
140
|
+
resolve(value);
|
|
141
|
+
};
|
|
142
|
+
socket.once('connect', () => finish(true));
|
|
143
|
+
socket.once('error', (error) => {
|
|
144
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT')
|
|
145
|
+
finish(false);
|
|
146
|
+
else
|
|
147
|
+
finish(false, error);
|
|
148
|
+
});
|
|
149
|
+
socket.setTimeout(500, () => finish(false, new Error('Timed out probing existing Bridge socket')));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function ensurePrivateDirectory(directory) {
|
|
153
|
+
try {
|
|
154
|
+
const stat = fs.lstatSync(directory);
|
|
155
|
+
if (!stat.isDirectory() || (stat.mode & 0o077) !== 0) {
|
|
156
|
+
throw new Error('Bridge transport directory must be a private directory');
|
|
157
|
+
}
|
|
158
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
159
|
+
throw new Error('Bridge transport directory must be owned by the current user');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT')
|
|
164
|
+
throw error;
|
|
165
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
166
|
+
const stat = fs.lstatSync(directory);
|
|
167
|
+
if (!stat.isDirectory() || (stat.mode & 0o077) !== 0) {
|
|
168
|
+
throw new Error('Bridge transport directory could not be created privately');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export class LocalAgentBridgeTransportServer {
|
|
173
|
+
socketPath;
|
|
174
|
+
#bridge;
|
|
175
|
+
#authToken;
|
|
176
|
+
#authorize;
|
|
177
|
+
#connected;
|
|
178
|
+
#newNonce;
|
|
179
|
+
#handshakeTimeoutMs;
|
|
180
|
+
#sessions = new Set();
|
|
181
|
+
#server;
|
|
182
|
+
constructor({ socketPath, authToken, bridge, authorize, connected, newNonce = () => crypto.randomBytes(32).toString('base64url'), handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS, }) {
|
|
183
|
+
if (!path.isAbsolute(socketPath) || !bounded(authToken, 1024) || authToken.length < 32
|
|
184
|
+
|| !bridge || typeof authorize !== 'function' || !Number.isSafeInteger(handshakeTimeoutMs)
|
|
185
|
+
|| handshakeTimeoutMs <= 0 || (connected !== undefined && typeof connected !== 'function')) {
|
|
186
|
+
throw new TypeError('Bridge transport requires an absolute socket path, Bridge, and authorizer');
|
|
187
|
+
}
|
|
188
|
+
this.socketPath = socketPath;
|
|
189
|
+
this.#authToken = authToken;
|
|
190
|
+
this.#bridge = bridge;
|
|
191
|
+
this.#authorize = authorize;
|
|
192
|
+
this.#connected = connected;
|
|
193
|
+
this.#newNonce = newNonce;
|
|
194
|
+
this.#handshakeTimeoutMs = handshakeTimeoutMs;
|
|
195
|
+
}
|
|
196
|
+
async start() {
|
|
197
|
+
if (this.#server)
|
|
198
|
+
return;
|
|
199
|
+
ensurePrivateDirectory(path.dirname(this.socketPath));
|
|
200
|
+
if (await liveSocket(this.socketPath)) {
|
|
201
|
+
throw new Error('Bridge transport socket is already owned by a live server');
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
fs.unlinkSync(this.socketPath);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT')
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
const server = net.createServer((socket) => this.#accept(socket));
|
|
211
|
+
this.#server = server;
|
|
212
|
+
try {
|
|
213
|
+
await new Promise((resolve, reject) => {
|
|
214
|
+
server.once('error', reject);
|
|
215
|
+
server.listen(this.socketPath, () => {
|
|
216
|
+
server.removeListener('error', reject);
|
|
217
|
+
resolve();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (this.#server === server)
|
|
223
|
+
this.#server = undefined;
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
fs.chmodSync(this.socketPath, 0o600);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
await this.close();
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async close() {
|
|
235
|
+
const server = this.#server;
|
|
236
|
+
if (!server)
|
|
237
|
+
return;
|
|
238
|
+
this.#server = undefined;
|
|
239
|
+
for (const session of [...this.#sessions])
|
|
240
|
+
this.#closeSession(session);
|
|
241
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
242
|
+
try {
|
|
243
|
+
fs.unlinkSync(this.socketPath);
|
|
244
|
+
}
|
|
245
|
+
catch { /* already removed */ }
|
|
246
|
+
}
|
|
247
|
+
#accept(socket) {
|
|
248
|
+
socket.setNoDelay(true);
|
|
249
|
+
const nonce = this.#newNonce();
|
|
250
|
+
const session = {};
|
|
251
|
+
const peer = wirePeer(socket, MAX_HANDSHAKE_BYTES, (value) => {
|
|
252
|
+
const operation = session.tail.then(() => this.#message(session, nonce, value));
|
|
253
|
+
session.tail = operation.catch((error) => {
|
|
254
|
+
try {
|
|
255
|
+
failure(peer, record(value)?.id, error);
|
|
256
|
+
}
|
|
257
|
+
catch { /* closing */ }
|
|
258
|
+
if (!session.connection)
|
|
259
|
+
this.#closeSession(session);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
const handshakeTimer = setTimeout(() => this.#closeSession(session), this.#handshakeTimeoutMs);
|
|
263
|
+
handshakeTimer.unref?.();
|
|
264
|
+
Object.assign(session, {
|
|
265
|
+
peer, unregister: new Map(), pending: new Map(), handshakeTimer, tail: Promise.resolve(),
|
|
266
|
+
});
|
|
267
|
+
this.#sessions.add(session);
|
|
268
|
+
socket.once('close', () => this.#closeSession(session));
|
|
269
|
+
socket.once('error', () => this.#closeSession(session));
|
|
270
|
+
peer.send({ type: 'challenge', protocolVersion: PROTOCOL_VERSION, nonce });
|
|
271
|
+
}
|
|
272
|
+
async #message(session, nonce, raw) {
|
|
273
|
+
const value = record(raw);
|
|
274
|
+
if (!value || typeof value.type !== 'string')
|
|
275
|
+
throw new Error('Invalid Bridge transport frame');
|
|
276
|
+
if (!session.connection) {
|
|
277
|
+
if (value.type !== 'hello' || value.protocolVersion !== PROTOCOL_VERSION
|
|
278
|
+
|| !equalProof(value.proof, nonceProof(this.#authToken, nonce))
|
|
279
|
+
|| !bounded(value.adapterId, 64) || !NAME_RE.test(value.adapterId)
|
|
280
|
+
|| !validCandidate(value.candidate))
|
|
281
|
+
throw new Error('Invalid Bridge transport handshake');
|
|
282
|
+
const replacing = value.newGeneration === true;
|
|
283
|
+
if ((value.newGeneration !== undefined && typeof value.newGeneration !== 'boolean')
|
|
284
|
+
|| (replacing && !bounded(value.generationId, 256))
|
|
285
|
+
|| (!replacing && value.generationId !== undefined)) {
|
|
286
|
+
throw new Error('Invalid Bridge transport generation');
|
|
287
|
+
}
|
|
288
|
+
const lease = await this.#authorize(value.adapterId, structuredClone(value.candidate), replacing ? { id: value.generationId, replace: true } : undefined);
|
|
289
|
+
if (lease.ref.agentId !== value.adapterId)
|
|
290
|
+
throw new Error('Bridge transport authorization mismatch');
|
|
291
|
+
if (!this.#sessions.has(session) || session.peer.closed) {
|
|
292
|
+
throw new Error('Bridge transport closed during authorization');
|
|
293
|
+
}
|
|
294
|
+
session.lease = lease;
|
|
295
|
+
session.connection = this.#bridge.connect(lease);
|
|
296
|
+
session.connection.signal.addEventListener('abort', () => this.#closeSession(session), { once: true });
|
|
297
|
+
await this.#connected?.(lease, session.connection);
|
|
298
|
+
if (!this.#sessions.has(session) || session.peer.closed || session.connection.signal.aborted) {
|
|
299
|
+
throw new Error('Bridge transport closed while activating connection');
|
|
300
|
+
}
|
|
301
|
+
if (session.handshakeTimer)
|
|
302
|
+
clearTimeout(session.handshakeTimer);
|
|
303
|
+
session.handshakeTimer = undefined;
|
|
304
|
+
session.peer.maxBytes = Math.max(session.connection.limits.maxFrameBytes, session.connection.limits.maxSnapshotBytes) + WIRE_OVERHEAD_BYTES;
|
|
305
|
+
session.peer.send({
|
|
306
|
+
type: 'ready', protocolVersion: PROTOCOL_VERSION,
|
|
307
|
+
connectionId: session.connection.connectionId,
|
|
308
|
+
run: lease.ref,
|
|
309
|
+
limits: session.connection.limits,
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const connection = session.connection;
|
|
314
|
+
const id = value.id;
|
|
315
|
+
if (value.type === 'response') {
|
|
316
|
+
if (!bounded(value.requestId, 256))
|
|
317
|
+
throw new Error('Invalid Bridge response id');
|
|
318
|
+
const pending = session.pending.get(value.requestId);
|
|
319
|
+
if (!pending)
|
|
320
|
+
return;
|
|
321
|
+
session.pending.delete(value.requestId);
|
|
322
|
+
pending.detachAbort();
|
|
323
|
+
if (value.ok === true)
|
|
324
|
+
pending.resolve(value.value);
|
|
325
|
+
else
|
|
326
|
+
pending.reject(new Error(typeof value.error === 'string' ? value.error : 'Bridge handler failed'));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (value.type === 'snapshot') {
|
|
330
|
+
if (!bounded(id, 256) || !validChannelName(value.channel))
|
|
331
|
+
throw new Error('Invalid snapshot frame');
|
|
332
|
+
result(session.peer, id, await connection.channel(value.channel).setSnapshot(value.value));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (value.type === 'publish') {
|
|
336
|
+
if (!bounded(id, 256) || !validChannelName(value.channel) || !record(value.event)
|
|
337
|
+
|| (value.delivery !== undefined && value.delivery !== 'ephemeral' && value.delivery !== 'durable')) {
|
|
338
|
+
throw new Error('Invalid publish frame');
|
|
339
|
+
}
|
|
340
|
+
result(session.peer, id, await connection.channel(value.channel).publish(value.event, value.delivery === undefined ? undefined : { delivery: value.delivery }));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (value.type === 'handle') {
|
|
344
|
+
if (!bounded(id, 256) || !validChannelName(value.channel) || !validChannelName(value.method)) {
|
|
345
|
+
throw new Error('Invalid handler frame');
|
|
346
|
+
}
|
|
347
|
+
const key = `${value.channel}\0${value.method}`;
|
|
348
|
+
if (session.unregister.has(key))
|
|
349
|
+
throw new Error('Bridge handler is already registered');
|
|
350
|
+
const unregister = connection.channel(value.channel).handle(value.method, (payload, context) => (this.#requestClient(session, value.channel, value.method, payload, context)));
|
|
351
|
+
session.unregister.set(key, unregister);
|
|
352
|
+
result(session.peer, id, { registered: true });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (value.type === 'unhandle') {
|
|
356
|
+
if (!bounded(id, 256) || !validChannelName(value.channel) || !validChannelName(value.method)) {
|
|
357
|
+
throw new Error('Invalid unhandle frame');
|
|
358
|
+
}
|
|
359
|
+
const key = `${value.channel}\0${value.method}`;
|
|
360
|
+
session.unregister.get(key)?.();
|
|
361
|
+
session.unregister.delete(key);
|
|
362
|
+
result(session.peer, id, { registered: false });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
throw new Error('Unsupported Bridge transport frame');
|
|
366
|
+
}
|
|
367
|
+
#requestClient(session, channel, method, payload, context) {
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
const cancel = () => {
|
|
370
|
+
if (!session.pending.delete(context.requestId))
|
|
371
|
+
return;
|
|
372
|
+
try {
|
|
373
|
+
session.peer.send({ type: 'cancel', requestId: context.requestId });
|
|
374
|
+
}
|
|
375
|
+
catch { /* closed */ }
|
|
376
|
+
reject(context.signal.reason instanceof Error ? context.signal.reason : new Error('Bridge request cancelled'));
|
|
377
|
+
};
|
|
378
|
+
context.signal.addEventListener('abort', cancel, { once: true });
|
|
379
|
+
session.pending.set(context.requestId, {
|
|
380
|
+
resolve, reject,
|
|
381
|
+
detachAbort: () => context.signal.removeEventListener('abort', cancel),
|
|
382
|
+
});
|
|
383
|
+
try {
|
|
384
|
+
session.peer.send({
|
|
385
|
+
type: 'request', requestId: context.requestId, channel, method, payload,
|
|
386
|
+
deadlineAt: context.deadlineAt,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
session.pending.delete(context.requestId);
|
|
391
|
+
context.signal.removeEventListener('abort', cancel);
|
|
392
|
+
reject(error);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
#closeSession(session) {
|
|
397
|
+
if (!this.#sessions.delete(session))
|
|
398
|
+
return;
|
|
399
|
+
if (session.handshakeTimer)
|
|
400
|
+
clearTimeout(session.handshakeTimer);
|
|
401
|
+
session.handshakeTimer = undefined;
|
|
402
|
+
for (const unregister of session.unregister.values())
|
|
403
|
+
unregister();
|
|
404
|
+
session.unregister.clear();
|
|
405
|
+
for (const pending of session.pending.values()) {
|
|
406
|
+
pending.detachAbort();
|
|
407
|
+
pending.reject(new Error('Bridge transport closed'));
|
|
408
|
+
}
|
|
409
|
+
session.pending.clear();
|
|
410
|
+
session.connection?.close();
|
|
411
|
+
session.peer.close();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
export async function connectBridgeTransport({ socketPath, authToken, adapterId, candidate, generation, connect = net.createConnection, newRequestId = crypto.randomUUID, handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS, }) {
|
|
415
|
+
if (!path.isAbsolute(socketPath) || !bounded(authToken, 1024) || authToken.length < 32
|
|
416
|
+
|| !NAME_RE.test(adapterId) || !validCandidate(candidate)
|
|
417
|
+
|| (generation !== undefined && (!bounded(generation.id, 256) || generation.replace !== true))
|
|
418
|
+
|| !Number.isSafeInteger(handshakeTimeoutMs) || handshakeTimeoutMs <= 0) {
|
|
419
|
+
throw new TypeError('Invalid Bridge transport client options');
|
|
420
|
+
}
|
|
421
|
+
const socket = connect(socketPath);
|
|
422
|
+
const abort = new AbortController();
|
|
423
|
+
const pending = new Map();
|
|
424
|
+
const handlers = new Map();
|
|
425
|
+
const handlerAborts = new Map();
|
|
426
|
+
let peer;
|
|
427
|
+
const nextId = () => {
|
|
428
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
429
|
+
const id = newRequestId();
|
|
430
|
+
if (bounded(id, 256) && !pending.has(id))
|
|
431
|
+
return id;
|
|
432
|
+
}
|
|
433
|
+
throw new Error('Unable to allocate Bridge wire request id');
|
|
434
|
+
};
|
|
435
|
+
const request = (value) => new Promise((resolve, reject) => {
|
|
436
|
+
const id = nextId();
|
|
437
|
+
pending.set(id, { resolve, reject });
|
|
438
|
+
try {
|
|
439
|
+
peer.send({ ...value, id });
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
pending.delete(id);
|
|
443
|
+
reject(error);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
let handshakeTimer;
|
|
447
|
+
let onHandshakeError;
|
|
448
|
+
let onHandshakeClose;
|
|
449
|
+
const opened = new Promise((resolve, reject) => {
|
|
450
|
+
let settled = false;
|
|
451
|
+
const finish = (outcome) => {
|
|
452
|
+
if (settled)
|
|
453
|
+
return;
|
|
454
|
+
settled = true;
|
|
455
|
+
if (handshakeTimer)
|
|
456
|
+
clearTimeout(handshakeTimer);
|
|
457
|
+
handshakeTimer = undefined;
|
|
458
|
+
if ('error' in outcome)
|
|
459
|
+
reject(outcome.error);
|
|
460
|
+
else
|
|
461
|
+
resolve(outcome.value);
|
|
462
|
+
};
|
|
463
|
+
onHandshakeError = (error) => finish({ error });
|
|
464
|
+
onHandshakeClose = () => finish({ error: new Error('Bridge transport closed during handshake') });
|
|
465
|
+
socket.once('error', onHandshakeError);
|
|
466
|
+
socket.once('close', onHandshakeClose);
|
|
467
|
+
peer = wirePeer(socket, MAX_HANDSHAKE_BYTES, (raw) => {
|
|
468
|
+
const value = record(raw);
|
|
469
|
+
if (!value || typeof value.type !== 'string') {
|
|
470
|
+
peer.close();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (value.type === 'challenge' && value.protocolVersion === PROTOCOL_VERSION
|
|
474
|
+
&& bounded(value.nonce, 256)) {
|
|
475
|
+
peer.send({
|
|
476
|
+
type: 'hello', protocolVersion: PROTOCOL_VERSION,
|
|
477
|
+
proof: nonceProof(authToken, value.nonce),
|
|
478
|
+
adapterId, candidate,
|
|
479
|
+
...(generation === undefined ? {} : {
|
|
480
|
+
newGeneration: true,
|
|
481
|
+
generationId: generation.id,
|
|
482
|
+
}),
|
|
483
|
+
});
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (value.type === 'ready') {
|
|
487
|
+
finish({ value });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (value.type === 'result' && bounded(value.id, 256)) {
|
|
491
|
+
const waiter = pending.get(value.id);
|
|
492
|
+
if (!waiter)
|
|
493
|
+
return;
|
|
494
|
+
pending.delete(value.id);
|
|
495
|
+
if (value.ok === true)
|
|
496
|
+
waiter.resolve(value.value);
|
|
497
|
+
else
|
|
498
|
+
waiter.reject(new Error(typeof value.error === 'string' ? value.error : 'Bridge operation failed'));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (value.type === 'cancel' && bounded(value.requestId, 256)) {
|
|
502
|
+
handlerAborts.get(value.requestId)?.abort('server_cancelled');
|
|
503
|
+
handlerAborts.delete(value.requestId);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (value.type === 'request' && bounded(value.requestId, 256)
|
|
507
|
+
&& validChannelName(value.channel) && validChannelName(value.method)
|
|
508
|
+
&& typeof value.deadlineAt === 'number') {
|
|
509
|
+
const handler = handlers.get(`${value.channel}\0${value.method}`);
|
|
510
|
+
if (!handler) {
|
|
511
|
+
peer.send({ type: 'response', requestId: value.requestId, ok: false, error: 'handler unavailable' });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const controller = new AbortController();
|
|
515
|
+
handlerAborts.set(value.requestId, controller);
|
|
516
|
+
Promise.resolve(handler(value.payload, {
|
|
517
|
+
requestId: value.requestId,
|
|
518
|
+
deadlineAt: value.deadlineAt,
|
|
519
|
+
signal: controller.signal,
|
|
520
|
+
})).then((response) => {
|
|
521
|
+
try {
|
|
522
|
+
peer.send({ type: 'response', requestId: value.requestId, ok: true, value: response });
|
|
523
|
+
}
|
|
524
|
+
catch { /* request was cancelled or transport closed */ }
|
|
525
|
+
}, (error) => {
|
|
526
|
+
try {
|
|
527
|
+
peer.send({
|
|
528
|
+
type: 'response', requestId: value.requestId, ok: false, error: errorMessage(error),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
catch { /* request was cancelled or transport closed */ }
|
|
532
|
+
}).finally(() => handlerAborts.delete(value.requestId)).catch(() => { });
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
handshakeTimer = setTimeout(() => {
|
|
536
|
+
finish({ error: new Error('Bridge transport handshake timed out') });
|
|
537
|
+
peer.close();
|
|
538
|
+
}, handshakeTimeoutMs);
|
|
539
|
+
handshakeTimer.unref?.();
|
|
540
|
+
});
|
|
541
|
+
const close = () => {
|
|
542
|
+
if (abort.signal.aborted)
|
|
543
|
+
return;
|
|
544
|
+
abort.abort('transport_closed');
|
|
545
|
+
peer.close();
|
|
546
|
+
for (const waiter of pending.values())
|
|
547
|
+
waiter.reject(new Error('Bridge transport closed'));
|
|
548
|
+
pending.clear();
|
|
549
|
+
for (const controller of handlerAborts.values())
|
|
550
|
+
controller.abort('transport_closed');
|
|
551
|
+
handlerAborts.clear();
|
|
552
|
+
};
|
|
553
|
+
socket.once('close', close);
|
|
554
|
+
let handshake;
|
|
555
|
+
try {
|
|
556
|
+
handshake = await opened;
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
close();
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
socket.removeListener('error', onHandshakeError);
|
|
563
|
+
socket.removeListener('close', onHandshakeClose);
|
|
564
|
+
socket.on('error', close);
|
|
565
|
+
if (handshake.protocolVersion !== PROTOCOL_VERSION
|
|
566
|
+
|| !validRun(handshake.run, adapterId, candidate.paneId)
|
|
567
|
+
|| !bounded(handshake.connectionId, 256) || !validLimits(handshake.limits)) {
|
|
568
|
+
close();
|
|
569
|
+
throw new Error('Invalid Bridge transport ready frame');
|
|
570
|
+
}
|
|
571
|
+
const limits = handshake.limits;
|
|
572
|
+
peer.maxBytes = Math.max(limits.maxFrameBytes, limits.maxSnapshotBytes) + WIRE_OVERHEAD_BYTES;
|
|
573
|
+
const channels = new Map();
|
|
574
|
+
return Object.freeze({
|
|
575
|
+
run: structuredClone(handshake.run),
|
|
576
|
+
connectionId: handshake.connectionId,
|
|
577
|
+
limits: structuredClone(limits),
|
|
578
|
+
signal: abort.signal,
|
|
579
|
+
channel(name) {
|
|
580
|
+
if (!validChannelName(name) || abort.signal.aborted)
|
|
581
|
+
throw new Error('Invalid or closed Bridge channel');
|
|
582
|
+
const existing = channels.get(name);
|
|
583
|
+
if (existing)
|
|
584
|
+
return existing;
|
|
585
|
+
const channel = Object.freeze({
|
|
586
|
+
setSnapshot: (value) => request({ type: 'snapshot', channel: name, value }),
|
|
587
|
+
publish: (event, options) => request({
|
|
588
|
+
type: 'publish', channel: name, event,
|
|
589
|
+
...(options?.delivery === undefined ? {} : { delivery: options.delivery }),
|
|
590
|
+
}),
|
|
591
|
+
async handle(method, handler) {
|
|
592
|
+
if (!validChannelName(method) || typeof handler !== 'function') {
|
|
593
|
+
throw new Error('Invalid Bridge handler');
|
|
594
|
+
}
|
|
595
|
+
const key = `${name}\0${method}`;
|
|
596
|
+
if (handlers.has(key))
|
|
597
|
+
throw new Error('Bridge handler is already registered');
|
|
598
|
+
handlers.set(key, handler);
|
|
599
|
+
try {
|
|
600
|
+
await request({ type: 'handle', channel: name, method });
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
handlers.delete(key);
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
return async () => {
|
|
607
|
+
if (handlers.get(key) !== handler)
|
|
608
|
+
return;
|
|
609
|
+
handlers.delete(key);
|
|
610
|
+
await request({ type: 'unhandle', channel: name, method });
|
|
611
|
+
};
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
channels.set(name, channel);
|
|
615
|
+
return channel;
|
|
616
|
+
},
|
|
617
|
+
close,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const DEFAULT_BRIDGE_LIMITS = Object.freeze({
|
|
2
|
+
maxFrameBytes: 256 * 1024,
|
|
3
|
+
maxSnapshotBytes: 1024 * 1024,
|
|
4
|
+
maxQueuedEventsPerChannel: 256,
|
|
5
|
+
maxQueuedBytesPerChannel: 4 * 1024 * 1024,
|
|
6
|
+
maxDurableSpoolBytesPerAdapter: 16 * 1024 * 1024,
|
|
7
|
+
maxRequestsPerRun: 32,
|
|
8
|
+
maxRequestsPerAdapter: 128,
|
|
9
|
+
defaultRequestTimeoutMs: 30_000,
|
|
10
|
+
maxRequestTimeoutMs: 5 * 60_000,
|
|
11
|
+
sustainedEventsPerSecond: 200,
|
|
12
|
+
burstEvents: 500,
|
|
13
|
+
});
|