hyperbrane 0.1.2 → 0.2.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 +67 -25
- package/dist/agent/agentClient.d.ts +5 -0
- package/dist/agent/agentClient.js +4 -0
- package/dist/agent/agentService.d.ts +9 -37
- package/dist/agent/agentService.js +62 -322
- package/dist/agent/agentStorage.d.ts +1 -20
- package/dist/agent/agentStorage.js +0 -30
- package/dist/agent/api.d.ts +80 -151
- package/dist/agent/api.js +1 -8
- package/dist/agent/client.d.ts +3 -1
- package/dist/agent/client.js +2 -3
- package/dist/agent/control.d.ts +23 -0
- package/dist/agent/control.js +479 -0
- package/dist/agent/hostTranscript.d.ts +0 -6
- package/dist/agent/hostTranscript.js +3 -14
- package/dist/agent/session/backend.d.ts +17 -32
- package/dist/agent/session/claude/home.d.ts +0 -1
- package/dist/agent/session/claude/home.js +0 -1
- package/dist/agent/session/claude/provision.d.ts +10 -0
- package/dist/agent/session/claude/provision.js +17 -0
- package/dist/agent/session/claude/transcript.d.ts +2 -7
- package/dist/agent/session/claude/transcript.js +68 -14
- package/dist/agent/session/client.d.ts +1 -10
- package/dist/agent/session/client.js +0 -3
- package/dist/agent/session/codex/provision.d.ts +14 -0
- package/dist/agent/session/codex/provision.js +25 -0
- package/dist/agent/session/fileTranscript.d.ts +6 -49
- package/dist/agent/session/fileTranscript.js +26 -82
- package/dist/agent/session/install.d.ts +5 -0
- package/dist/agent/session/install.js +35 -0
- package/dist/agent/session/sessionStorage.d.ts +4 -35
- package/dist/agent/session/sessionStorage.js +10 -16
- package/dist/agent/session/sharedConnection.d.ts +0 -8
- package/dist/agent/session/sharedConnection.js +0 -7
- package/dist/agent/session/storedTranscripts.d.ts +1 -6
- package/dist/agent/session/storedTranscripts.js +0 -3
- package/dist/agent/session/worker/jsonRpc.d.ts +2 -13
- package/dist/agent/session/worker/jsonRpc.js +2 -11
- package/dist/agent/session/worker/protocol.d.ts +26 -20
- package/dist/agent/session/worker/protocol.js +29 -18
- package/dist/agent/session/worker/workerParams.d.ts +0 -5
- package/dist/agent/session/worker/workerParams.js +14 -14
- package/dist/agent/session/workerConnection.d.ts +2 -13
- package/dist/agent/session/workerConnection.js +7 -12
- package/dist/agent/session/workerGatewayConnection.d.ts +18 -0
- package/dist/agent/session/workerGatewayConnection.js +86 -0
- package/dist/agent/vendor-types/manifest.json +87 -0
- package/dist/agent/workerBundle.d.ts +4 -19
- package/dist/agent/workerBundle.generated.js +1 -1
- package/dist/agent/workerBundle.js +3 -30
- package/dist/agent/workerReplacement.d.ts +3 -0
- package/dist/agent/workerReplacement.js +49 -0
- package/dist/cli/agent.js +31 -16
- package/dist/cli/command.d.ts +10 -11
- package/dist/cli/command.js +0 -4
- package/dist/cli/dimension.js +6 -6
- package/dist/cli/gateway.js +3 -6
- package/dist/cli/main.js +14 -31
- package/dist/cli/streamPrinter.d.ts +2 -10
- package/dist/cli/streamPrinter.js +16 -46
- package/dist/cli/table.d.ts +0 -3
- package/dist/cli/table.js +0 -4
- package/dist/dimension/api.d.ts +39 -27
- package/dist/dimension/api.js +28 -47
- package/dist/dimension/client.d.ts +5 -0
- package/dist/dimension/client.js +4 -0
- package/dist/dimension/control.d.ts +21 -0
- package/dist/dimension/control.js +328 -0
- package/dist/dimension/image.d.ts +0 -5
- package/dist/dimension/image.js +0 -8
- package/dist/dimension/imageFiles.generated.js +1 -1
- package/dist/dimension/paths.d.ts +5 -4
- package/dist/dimension/paths.js +15 -6
- package/dist/dimension/service.d.ts +8 -24
- package/dist/dimension/service.js +60 -228
- package/dist/dimension/transport-worker.d.ts +1 -0
- package/dist/dimension/transport-worker.js +151 -0
- package/dist/dimension/transportBundle.d.ts +2 -0
- package/dist/dimension/transportBundle.generated.d.ts +1 -0
- package/dist/dimension/transportBundle.generated.js +2 -0
- package/dist/dimension/transportBundle.js +4 -0
- package/dist/dimension/transportClient.d.ts +10 -0
- package/dist/dimension/transportClient.js +115 -0
- package/dist/dimension/transportProtocol.d.ts +53 -0
- package/dist/dimension/transportProtocol.js +8 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.js +5 -19
- package/dist/lib/control.d.ts +8 -0
- package/dist/lib/control.js +62 -0
- package/dist/lib/docker.d.ts +0 -22
- package/dist/lib/docker.js +0 -10
- package/dist/lib/image.d.ts +0 -3
- package/dist/lib/image.js +0 -2
- package/dist/lib/jsonDirectory.d.ts +0 -13
- package/dist/lib/jsonDirectory.js +10 -15
- package/dist/lib/scriptBundle.d.ts +6 -0
- package/dist/lib/scriptBundle.js +28 -0
- package/dist/lib/sh.d.ts +2 -0
- package/dist/lib/sh.js +8 -0
- package/dist/lib/sleep.d.ts +0 -1
- package/dist/lib/sleep.js +0 -1
- package/dist/lib/streamFetch.d.ts +15 -0
- package/dist/lib/streamFetch.js +150 -0
- package/dist/lib/tar.d.ts +0 -1
- package/dist/lib/tar.js +5 -11
- package/dist/portal-gateway/api.d.ts +13 -44
- package/dist/portal-gateway/api.js +1 -3
- package/dist/portal-gateway/control.d.ts +42 -0
- package/dist/portal-gateway/control.js +657 -0
- package/dist/portal-gateway/gatewayClient.d.ts +5 -0
- package/dist/portal-gateway/gatewayClient.js +4 -0
- package/dist/portal-gateway/gatewayService.d.ts +8 -38
- package/dist/portal-gateway/gatewayService.js +68 -254
- package/dist/portal-gateway/image.d.ts +0 -1
- package/dist/portal-gateway/image.js +0 -2
- package/dist/portal-gateway/imageFiles.generated.js +2 -2
- package/dist/portal-gateway/portalClient.d.ts +5 -0
- package/dist/portal-gateway/portalClient.js +4 -0
- package/dist/portal-gateway/portalService.d.ts +5 -47
- package/dist/portal-gateway/portalService.js +19 -349
- package/dist/portal-gateway/transport-worker.d.ts +1 -0
- package/dist/portal-gateway/transport-worker.js +205 -0
- package/dist/portal-gateway/transportBundle.d.ts +2 -0
- package/dist/portal-gateway/transportBundle.generated.d.ts +1 -0
- package/dist/portal-gateway/transportBundle.generated.js +2 -0
- package/dist/portal-gateway/transportBundle.js +4 -0
- package/dist/portal-gateway/upstream.d.ts +0 -81
- package/dist/portal-gateway/upstream.js +0 -1
- package/dist/sandbox/api.d.ts +121 -187
- package/dist/sandbox/api.js +5 -7
- package/dist/sandbox/client.d.ts +2 -0
- package/dist/sandbox/client.js +4 -0
- package/dist/sandbox/data-endpoints/dataEndpoints.d.ts +13 -0
- package/dist/sandbox/data-endpoints/dataEndpoints.js +100 -0
- package/dist/sandbox/data-endpoints/firewalledPorts.d.ts +4 -0
- package/dist/sandbox/data-endpoints/firewalledPorts.js +28 -0
- package/dist/sandbox/lib/httpOverStream.d.ts +3 -0
- package/dist/sandbox/lib/httpOverStream.js +21 -0
- package/dist/sandbox/lib/remoteFileOperations.d.ts +10 -0
- package/dist/sandbox/lib/remoteFileOperations.js +70 -0
- package/dist/sandbox/lib/shellCommand.d.ts +3 -0
- package/dist/sandbox/lib/shellCommand.js +7 -0
- package/dist/sandbox/providers/boat/boatService.d.ts +2 -0
- package/dist/sandbox/providers/boat/boatService.js +321 -0
- package/dist/sandbox/providers/exedev/exeDevService.d.ts +2 -0
- package/dist/sandbox/providers/exedev/exeDevService.js +127 -0
- package/dist/sandbox/providers/exedev/exeDevToken.d.ts +1 -0
- package/dist/sandbox/providers/exedev/exeDevToken.js +54 -0
- package/dist/sandbox/providers/local/localService.d.ts +2 -0
- package/dist/sandbox/providers/local/localService.js +219 -0
- package/dist/sandbox/providers/ssh/sshService.d.ts +2 -0
- package/dist/sandbox/providers/ssh/sshService.js +82 -0
- package/dist/sandbox/providers/ssh/sshTransport.d.ts +12 -0
- package/dist/sandbox/providers/ssh/sshTransport.js +216 -0
- package/dist/sandbox/service.d.ts +2 -0
- package/dist/sandbox/service.js +18 -0
- package/dist/sandbox/storage/descriptors.d.ts +6 -0
- package/dist/sandbox/storage/descriptors.js +25 -0
- package/dist/sandbox/storage/sandboxStorage.d.ts +11 -0
- package/dist/sandbox/storage/sandboxStorage.js +42 -0
- package/dist/upstreams/anthropic.d.ts +0 -5
- package/dist/upstreams/anthropic.js +1 -8
- package/dist/upstreams/index.d.ts +2 -6
- package/dist/upstreams/index.js +0 -4
- package/dist/upstreams/openai.d.ts +0 -5
- package/dist/upstreams/openai.js +1 -13
- package/package.json +12 -4
- package/dist/agent/gatewaySignIns.d.ts +0 -5
- package/dist/agent/gatewaySignIns.js +0 -11
- package/dist/agent/session/codex/home.d.ts +0 -2
- package/dist/agent/session/codex/home.js +0 -4
- package/dist/agent/session/codex/messages.d.ts +0 -30
- package/dist/agent/session/codex/messages.js +0 -135
- package/dist/agent/session/codex/transcript.d.ts +0 -12
- package/dist/agent/session/codex/transcript.js +0 -22
- package/dist/agent/session/sdkMessages.d.ts +0 -15
- package/dist/agent/session/sdkMessages.js +0 -28
- package/dist/agent/session/workerSseConnection.d.ts +0 -31
- package/dist/agent/session/workerSseConnection.js +0 -76
- package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.d.ts +0 -10
- package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.d.ts +0 -2
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.d.ts +0 -22
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/ImageDetail.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/ImageDetail.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.d.ts +0 -5
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.d.ts +0 -11
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.d.ts +0 -23
- package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/MessagePhase.d.ts +0 -7
- package/dist/agent/vendor-types/codex/app-server/MessagePhase.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.d.ts +0 -4
- package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/SleepItem.d.ts +0 -7
- package/dist/agent/vendor-types/codex/app-server/SleepItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/WebSearchItem.d.ts +0 -14
- package/dist/agent/vendor-types/codex/app-server/WebSearchItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.d.ts +0 -3
- package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.d.ts +0 -4
- package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.d.ts +0 -4
- package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.d.ts +0 -5
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.d.ts +0 -19
- package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.d.ts +0 -10
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.d.ts +0 -6
- package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.d.ts +0 -4
- package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.d.ts +0 -8
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.d.ts +0 -7
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.d.ts +0 -3
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.d.ts +0 -6
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.d.ts +0 -5
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.d.ts +0 -6
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.d.ts +0 -8
- package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/TextElement.d.ts +0 -11
- package/dist/agent/vendor-types/codex/app-server/v2/TextElement.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.d.ts +0 -215
- package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/UserInput.d.ts +0 -35
- package/dist/agent/vendor-types/codex/app-server/v2/UserInput.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.d.ts +0 -14
- package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.js +0 -2
- package/dist/cli/services.d.ts +0 -19
- package/dist/cli/services.js +0 -34
- package/dist/dimension/http.d.ts +0 -7
- package/dist/dimension/http.js +0 -77
- package/dist/dimension/storage.d.ts +0 -39
- package/dist/dimension/storage.js +0 -79
- package/dist/portal-gateway/fakes.d.ts +0 -3
- package/dist/portal-gateway/fakes.js +0 -11
- package/dist/portal-gateway/gatewayStorage.d.ts +0 -66
- package/dist/portal-gateway/gatewayStorage.js +0 -125
- package/dist/portal-gateway/portalStorage.d.ts +0 -34
- package/dist/portal-gateway/portalStorage.js +0 -44
- package/dist/sandbox/boatService.d.ts +0 -52
- package/dist/sandbox/boatService.js +0 -343
- package/dist/sandbox/containerService.d.ts +0 -15
- package/dist/sandbox/containerService.js +0 -184
- package/dist/sandbox/endpointProxy.d.ts +0 -90
- package/dist/sandbox/endpointProxy.js +0 -716
- package/dist/sandbox/exeDevAuth.d.ts +0 -16
- package/dist/sandbox/exeDevAuth.js +0 -49
- package/dist/sandbox/localService.d.ts +0 -19
- package/dist/sandbox/localService.js +0 -200
- package/dist/sandbox/sseConnection.d.ts +0 -41
- package/dist/sandbox/sseConnection.js +0 -166
- package/dist/sandbox/sshService.d.ts +0 -30
- package/dist/sandbox/sshService.js +0 -680
|
@@ -1,50 +1,8 @@
|
|
|
1
|
-
import { type DockerClient } from "../lib/docker.ts";
|
|
2
|
-
import type { ManagedImage } from "../lib/image.ts";
|
|
3
1
|
import type { Sandbox } from "../sandbox/api.ts";
|
|
4
|
-
import {
|
|
5
|
-
import type { GatewayStorage } from "./gatewayStorage.ts";
|
|
6
|
-
import type { PortalStorage } from "./portalStorage.ts";
|
|
2
|
+
import type { PortalClient } from "./api.ts";
|
|
7
3
|
/** @sealed */
|
|
8
|
-
export interface PortalService {
|
|
9
|
-
createPortal(portal: Portal): Promise<Portal>;
|
|
10
|
-
destroyPortal(portalName: string): Promise<void>;
|
|
11
|
-
listPortals(): Promise<Portal[]>;
|
|
12
|
-
/**
|
|
13
|
-
* Re-wires the ready portals (optionally only those of one gateway or dimension) whose containers
|
|
14
|
-
* are running and whose link is gone: a container restart takes its network namespace, and the
|
|
15
|
-
* portal's veth, with it. Portals still wired cost one look at each gateway's interfaces, so this
|
|
16
|
-
* is cheap enough to run before every command. Failures are logged per portal rather than thrown.
|
|
17
|
-
*/
|
|
18
|
-
restorePortals(filter?: {
|
|
19
|
-
gatewayName?: string;
|
|
20
|
-
dimensionName?: string;
|
|
21
|
-
}): Promise<void>;
|
|
4
|
+
export interface PortalService extends PortalClient {
|
|
22
5
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
dimensionIp: string;
|
|
27
|
-
gatewayIp: string;
|
|
28
|
-
metric: number;
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Interface names come from the portal name (so a portal can always be found again) and addresses
|
|
32
|
-
* from its claimed subnet. The gateway only intercepts interfaces named "hvg*".
|
|
33
|
-
*/
|
|
34
|
-
export declare function portalLink(portalName: string, subnet: number): PortalLink;
|
|
35
|
-
/** The OS trust bundle; the gateways' CAs end up in it. */
|
|
36
|
-
export declare const OS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
|
|
37
|
-
/**
|
|
38
|
-
* `host` is the machine the docker daemon runs on, rooted at "/" (see createGatewayService).
|
|
39
|
-
* `image` is the gateway image: it carries the wiring script run by the privileged helper.
|
|
40
|
-
*/
|
|
41
|
-
export declare function createPortalService(storage: PortalStorage, gateways: GatewayStorage, docker: DockerClient, image: ManagedImage, host: Sandbox): PortalService;
|
|
42
|
-
/** @sealed */
|
|
43
|
-
export interface PortalWatcher {
|
|
44
|
-
/** Restores every portal, then keeps restoring those of each hyperbrane container that starts. */
|
|
45
|
-
start(): void;
|
|
46
|
-
stop(): Promise<void>;
|
|
47
|
-
}
|
|
48
|
-
/** Run one per host: the portals of a container that restarts are gone until something re-wires them. */
|
|
49
|
-
export declare function createPortalWatcher(portals: PortalService, docker: DockerClient): PortalWatcher;
|
|
50
|
-
export {};
|
|
6
|
+
export declare function createPortalService(host: Sandbox, { dataDirectory }?: {
|
|
7
|
+
dataDirectory?: string;
|
|
8
|
+
}): PortalService;
|
|
@@ -1,359 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const RESTORE_RECHECK_MS = 1_000;
|
|
8
|
-
/** Each portal is a point-to-point /30 out of 10.77.0.0/16. */
|
|
9
|
-
const SUBNET_COUNT = 1 << 14;
|
|
10
|
-
/** A dimension's default routes are ordered by subnet: its portal with the lowest one is preferred. */
|
|
11
|
-
const ROUTE_METRIC_BASE = 100;
|
|
12
|
-
/**
|
|
13
|
-
* Interface names come from the portal name (so a portal can always be found again) and addresses
|
|
14
|
-
* from its claimed subnet. The gateway only intercepts interfaces named "hvg*".
|
|
15
|
-
*/
|
|
16
|
-
export function portalLink(portalName, subnet) {
|
|
17
|
-
const hash = createHash("sha256").update(portalName).digest("hex").slice(0, 8);
|
|
18
|
-
const base = `10.77.${subnet >> 6}`;
|
|
19
|
-
const offset = (subnet & 0x3f) << 2;
|
|
20
|
-
return {
|
|
21
|
-
dimensionIf: `hvd${hash}`,
|
|
22
|
-
gatewayIf: `hvg${hash}`,
|
|
23
|
-
gatewayIp: `${base}.${offset + 1}`,
|
|
24
|
-
dimensionIp: `${base}.${offset + 2}`,
|
|
25
|
-
metric: ROUTE_METRIC_BASE + subnet,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/** The OS trust bundle; the gateways' CAs end up in it. */
|
|
29
|
-
export const OS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
|
|
30
|
-
// Run as root in the dimension: installs GATEWAY_CA as the gateway's certificate in the OS trust
|
|
31
|
-
// store, or removes it when empty. Nothing but its gateways' CAs ever signs what a dimension sees
|
|
32
|
-
// (every TCP connection is intercepted), so without the ca-certificates package the bundle can be
|
|
33
|
-
// just those CAs. Node ignores the OS store, so while any gateway is trusted NODE_EXTRA_CA_CERTS
|
|
34
|
-
// points it at the bundle, for login shells (profile.d) and PAM sessions (/etc/environment); a
|
|
35
|
-
// running container's own environment can't be changed. DNS needs no setup: the gateway answers
|
|
36
|
-
// UDP 53 whatever address it was sent to.
|
|
37
|
-
const TRUST_GATEWAY_SCRIPT = `set -eu
|
|
38
|
-
# Portals of one dimension are wired concurrently (e.g. restored together after a restart), and
|
|
39
|
-
# they share the trust store and /etc/environment.
|
|
40
|
-
exec 9>/run/hyperbrane-trust.lock
|
|
41
|
-
flock 9
|
|
42
|
-
bundle=${OS_CA_BUNDLE}
|
|
43
|
-
mkdir -p /usr/local/share/ca-certificates /etc/ssl/certs /etc/profile.d
|
|
44
|
-
cert="/usr/local/share/ca-certificates/hyperbrane-gateway-$GATEWAY.crt"
|
|
45
|
-
if [ -n "$GATEWAY_CA" ]; then printf '%s\\n' "$GATEWAY_CA" > "$cert"; else rm -f "$cert"; fi
|
|
46
|
-
if command -v update-ca-certificates >/dev/null; then
|
|
47
|
-
update-ca-certificates --fresh >/dev/null
|
|
48
|
-
else
|
|
49
|
-
cat /usr/local/share/ca-certificates/hyperbrane-gateway-*.crt > "$bundle" 2>/dev/null || : > "$bundle"
|
|
50
|
-
if command -v openssl >/dev/null; then openssl rehash /etc/ssl/certs; fi
|
|
51
|
-
fi
|
|
52
|
-
{ grep -v '^NODE_EXTRA_CA_CERTS=' /etc/environment 2>/dev/null || true; } > /etc/environment.hyperbrane
|
|
53
|
-
if ls /usr/local/share/ca-certificates/hyperbrane-gateway-*.crt >/dev/null 2>&1; then
|
|
54
|
-
echo "NODE_EXTRA_CA_CERTS=$bundle" >> /etc/environment.hyperbrane
|
|
55
|
-
echo "export NODE_EXTRA_CA_CERTS=$bundle" > /etc/profile.d/hyperbrane-gateway.sh
|
|
56
|
-
else
|
|
57
|
-
rm -f /etc/profile.d/hyperbrane-gateway.sh
|
|
58
|
-
fi
|
|
59
|
-
mv /etc/environment.hyperbrane /etc/environment
|
|
60
|
-
`;
|
|
61
|
-
/**
|
|
62
|
-
* `host` is the machine the docker daemon runs on, rooted at "/" (see createGatewayService).
|
|
63
|
-
* `image` is the gateway image: it carries the wiring script run by the privileged helper.
|
|
64
|
-
*/
|
|
65
|
-
export function createPortalService(storage, gateways, docker, image, host) {
|
|
66
|
-
// Re-running re-wires, so it also repairs a portal whose veth went away.
|
|
1
|
+
import { createDockerClient } from "../lib/docker.js";
|
|
2
|
+
import { gatewayControl } from "./control.js";
|
|
3
|
+
import { assertValidName } from "./gatewayService.js";
|
|
4
|
+
import { createGatewayImage } from "./image.js";
|
|
5
|
+
export function createPortalService(host, { dataDirectory } = {}) {
|
|
6
|
+
const control = gatewayControl(host, () => createGatewayImage(createDockerClient(host)).ensure(), { dataDirectory });
|
|
67
7
|
async function createPortal(portal) {
|
|
68
8
|
assertValidName("portal", portal.name);
|
|
69
9
|
assertValidName("dimension", portal.dimensionName);
|
|
70
10
|
assertValidName("gateway", portal.gatewayName);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
throw new GatewayNotFoundError(`gateway ${portal.gatewayName} is not ready`);
|
|
74
|
-
// Checked again when wiring; failing here first leaves no record behind.
|
|
75
|
-
await runningPid(gatewayContainerName(portal.gatewayName), "hyperbrane.gateway", portal.gatewayName);
|
|
76
|
-
await runningPid(dimensionContainerName(portal.dimensionName), "hyperbrane.dimension", portal.dimensionName);
|
|
77
|
-
let record = await storage.insertPortal({ ...portal, state: "creating" });
|
|
78
|
-
if (record.state === "destroying")
|
|
79
|
-
throw new GatewayConflictError(`portal ${portal.name} is being destroyed`);
|
|
80
|
-
if (record.dimensionName !== portal.dimensionName || record.gatewayName !== portal.gatewayName) {
|
|
81
|
-
throw new GatewayConflictError(`portal ${portal.name} already exists with a different spec`);
|
|
82
|
-
}
|
|
83
|
-
if (record.subnet === undefined) {
|
|
84
|
-
const subnet = await storage.claimSubnet(portal.name);
|
|
85
|
-
if (subnet >= SUBNET_COUNT) {
|
|
86
|
-
await storage.releaseSubnet(subnet);
|
|
87
|
-
throw new GatewayConflictError("no portal subnets left");
|
|
88
|
-
}
|
|
89
|
-
record = { ...record, subnet };
|
|
90
|
-
await storage.savePortal(record);
|
|
91
|
-
}
|
|
92
|
-
await connect(record);
|
|
93
|
-
if (record.state !== "ready")
|
|
94
|
-
await storage.savePortal({ ...record, state: "ready" });
|
|
95
|
-
return publicPortal(record);
|
|
96
|
-
}
|
|
97
|
-
async function connect(record, pids) {
|
|
98
|
-
const { gateway, dimension } = pids ?? (await runningPids(record));
|
|
99
|
-
await wire(record.name, portalLink(record.name, record.subnet), dimension, gateway);
|
|
100
|
-
await trustGateway(dimensionContainerName(record.dimensionName), record.gatewayName, true);
|
|
101
|
-
}
|
|
102
|
-
async function runningPids(record) {
|
|
103
|
-
return {
|
|
104
|
-
gateway: await runningPid(gatewayContainerName(record.gatewayName), "hyperbrane.gateway", record.gatewayName),
|
|
105
|
-
dimension: await runningPid(dimensionContainerName(record.dimensionName), "hyperbrane.dimension", record.dimensionName),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
async function runningPid(name, label, owner) {
|
|
109
|
-
const kind = label.replace("hyperbrane.", "");
|
|
110
|
-
const container = await docker.inspectContainer(name);
|
|
111
|
-
if (!container || container.Config?.Labels?.[label] !== owner) {
|
|
112
|
-
throw new GatewayNotFoundError(`${kind} ${owner} does not exist`);
|
|
113
|
-
}
|
|
114
|
-
if (!container.State?.Running || !container.State.Pid) {
|
|
115
|
-
throw new GatewayConflictError(`${kind} ${owner} is not running`);
|
|
116
|
-
}
|
|
117
|
-
return container.State.Pid;
|
|
118
|
-
}
|
|
119
|
-
// Moving a veth between two containers' namespaces takes the host's view of both, so it runs in a
|
|
120
|
-
// throwaway privileged container; neither the dimension nor the gateway gets that power.
|
|
121
|
-
async function wire(portalName, link, dimensionPid, gatewayPid) {
|
|
122
|
-
const helper = `hybwire-${crypto.randomUUID().slice(0, 8)}`;
|
|
123
|
-
await docker.createContainer(helper, {
|
|
124
|
-
Image: await image.ensure(),
|
|
125
|
-
Cmd: ["sleep", "infinity"],
|
|
126
|
-
Labels: { hyperbrane: "true", "hyperbrane.helper": "wire-portal" },
|
|
127
|
-
HostConfig: {
|
|
128
|
-
NetworkMode: "host",
|
|
129
|
-
PidMode: "host",
|
|
130
|
-
Privileged: true,
|
|
131
|
-
Binds: ["/run:/hostrun"],
|
|
132
|
-
Init: true,
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
try {
|
|
136
|
-
await docker.startContainer(helper);
|
|
137
|
-
const result = await docker.exec(helper, {
|
|
138
|
-
cmd: ["hyb-wire-portal"],
|
|
139
|
-
user: "root",
|
|
140
|
-
env: {
|
|
141
|
-
PORTAL: portalName,
|
|
142
|
-
DIMENSION_PID: String(dimensionPid),
|
|
143
|
-
GATEWAY_PID: String(gatewayPid),
|
|
144
|
-
DIMENSION_IF: link.dimensionIf,
|
|
145
|
-
GATEWAY_IF: link.gatewayIf,
|
|
146
|
-
DIMENSION_IP: link.dimensionIp,
|
|
147
|
-
GATEWAY_IP: link.gatewayIp,
|
|
148
|
-
ROUTE_METRIC: String(link.metric),
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
if (result.exitCode !== 0)
|
|
152
|
-
throw new Error(`wiring portal ${portalName} failed: ${result.stderr.trim()}`);
|
|
153
|
-
}
|
|
154
|
-
finally {
|
|
155
|
-
await docker.removeContainer(helper);
|
|
156
|
-
}
|
|
11
|
+
await control.createPortal(portal);
|
|
12
|
+
return { name: portal.name, dimensionName: portal.dimensionName, gatewayName: portal.gatewayName };
|
|
157
13
|
}
|
|
158
|
-
async function trustGateway(dimensionName, gatewayName, trusted) {
|
|
159
|
-
let ca = "";
|
|
160
|
-
if (trusted) {
|
|
161
|
-
const stateDirectory = await gateways.ensureStateDirectory(gatewayName);
|
|
162
|
-
ca = new TextDecoder().decode(await host.readFile(gatewayCaCertificatePath(stateDirectory))).trim();
|
|
163
|
-
}
|
|
164
|
-
const result = await docker.exec(dimensionName, {
|
|
165
|
-
cmd: ["sh", "-c", TRUST_GATEWAY_SCRIPT],
|
|
166
|
-
user: "root",
|
|
167
|
-
env: { GATEWAY: gatewayName, GATEWAY_CA: ca },
|
|
168
|
-
});
|
|
169
|
-
if (result.exitCode !== 0) {
|
|
170
|
-
throw new Error(`updating gateway ${gatewayName} trust in ${dimensionName} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// Deleting the gateway end removes the pair, and the dimension's route through it.
|
|
174
|
-
// Marked first: a restore already under way re-checks the record after wiring and undoes its work,
|
|
175
|
-
// and the link is removed after the mark, so whichever runs last, the link is gone.
|
|
176
14
|
async function destroyPortal(portalName) {
|
|
177
15
|
assertValidName("portal", portalName);
|
|
178
|
-
|
|
179
|
-
if (!record)
|
|
180
|
-
return;
|
|
181
|
-
if (record.state !== "destroying")
|
|
182
|
-
await storage.savePortal({ ...record, state: "destroying" });
|
|
183
|
-
// A restore under way finishes first (seeing the mark, it undoes what it wired), so nothing it
|
|
184
|
-
// does lands after this returns; one that starts from now on sees the mark and does nothing.
|
|
185
|
-
while (restoring.has(portalName))
|
|
186
|
-
await restoring.get(portalName).catch(() => { });
|
|
187
|
-
if (record.subnet !== undefined) {
|
|
188
|
-
await ignoringStopped(() => unwire(record.gatewayName, portalLink(portalName, record.subnet)));
|
|
189
|
-
await storage.releaseSubnet(record.subnet);
|
|
190
|
-
}
|
|
191
|
-
await untrustUnlessShared(record);
|
|
192
|
-
await storage.removePortal(portalName);
|
|
193
|
-
}
|
|
194
|
-
/** Takes the gateway's CA out of the dimension, unless another of its portals goes to that gateway. */
|
|
195
|
-
async function untrustUnlessShared(record) {
|
|
196
|
-
const sameGateway = (await storage.listPortals()).some((other) => other.name !== record.name &&
|
|
197
|
-
other.dimensionName === record.dimensionName &&
|
|
198
|
-
other.gatewayName === record.gatewayName);
|
|
199
|
-
if (!sameGateway) {
|
|
200
|
-
await ignoringStopped(() => trustGateway(dimensionContainerName(record.dimensionName), record.gatewayName, false));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async function unwire(gatewayName, link) {
|
|
204
|
-
const result = await docker.exec(gatewayContainerName(gatewayName), {
|
|
205
|
-
cmd: ["sh", "-c", `ip link del ${link.gatewayIf} 2>/dev/null || true`],
|
|
206
|
-
user: "root",
|
|
207
|
-
});
|
|
208
|
-
if (result.exitCode !== 0)
|
|
209
|
-
throw new Error(`removing ${link.gatewayIf} failed: ${result.stderr.trim()}`);
|
|
210
|
-
}
|
|
211
|
-
async function listPortals() {
|
|
212
|
-
return (await storage.listPortals()).map(publicPortal);
|
|
213
|
-
}
|
|
214
|
-
async function restorePortals({ gatewayName, dimensionName } = {}) {
|
|
215
|
-
const records = (await storage.listPortals()).filter((record) => record.state === "ready" &&
|
|
216
|
-
(gatewayName === undefined || record.gatewayName === gatewayName) &&
|
|
217
|
-
(dimensionName === undefined || record.dimensionName === dimensionName));
|
|
218
|
-
// A veth pair only ever goes as a whole, so a gateway end still there means the portal is intact.
|
|
219
|
-
const interfaces = new Map();
|
|
220
|
-
const gatewayInterfaces = (name) => {
|
|
221
|
-
if (!interfaces.has(name))
|
|
222
|
-
interfaces.set(name, listInterfaces(gatewayContainerName(name)));
|
|
223
|
-
return interfaces.get(name);
|
|
224
|
-
};
|
|
225
|
-
await Promise.all(records.map(async (record) => {
|
|
226
|
-
if ((await gatewayInterfaces(record.gatewayName))?.has(portalLink(record.name, record.subnet).gatewayIf))
|
|
227
|
-
return;
|
|
228
|
-
await restoreOnce(record);
|
|
229
|
-
}));
|
|
230
|
-
}
|
|
231
|
-
// One restore of a portal at a time, however many ask (a restart's own and the watcher's): two
|
|
232
|
-
// wiring the same link at once fail each other, and the retry could land after a destroy. Each
|
|
233
|
-
// waits for the one before rather than joining it: that one may have given up on a container that
|
|
234
|
-
// has since restarted, so once it's done the link is looked at again.
|
|
235
|
-
const restoring = new Map();
|
|
236
|
-
async function restoreOnce(record) {
|
|
237
|
-
while (restoring.has(record.name)) {
|
|
238
|
-
await restoring.get(record.name).catch(() => { });
|
|
239
|
-
const link = portalLink(record.name, record.subnet);
|
|
240
|
-
if ((await listInterfaces(gatewayContainerName(record.gatewayName)))?.has(link.gatewayIf))
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
const restored = restore(record).finally(() => restoring.delete(record.name));
|
|
244
|
-
restoring.set(record.name, restored);
|
|
245
|
-
return restored;
|
|
16
|
+
await control.destroyPortal(portalName);
|
|
246
17
|
}
|
|
247
|
-
async function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return;
|
|
252
|
-
let pids;
|
|
253
|
-
try {
|
|
254
|
-
pids = await runningPids(record);
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
// A stopped or removed container gets wired when it starts again, or never.
|
|
258
|
-
if (error instanceof GatewayConflictError || error instanceof GatewayNotFoundError)
|
|
259
|
-
return;
|
|
260
|
-
throw error;
|
|
261
|
-
}
|
|
262
|
-
for (let attempt = 1;; attempt++) {
|
|
263
|
-
try {
|
|
264
|
-
await connect(record, pids);
|
|
265
|
-
// Destroyed meanwhile: neither the link nor the trust just made may outlive it (see destroyPortal).
|
|
266
|
-
if ((await storage.readPortal(record.name))?.state !== "ready") {
|
|
267
|
-
await ignoringStopped(() => unwire(record.gatewayName, portalLink(record.name, record.subnet)));
|
|
268
|
-
await untrustUnlessShared(record);
|
|
269
|
-
}
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
catch (error) {
|
|
273
|
-
// A container that stopped or restarted meanwhile (killing what ran in it) is restored by
|
|
274
|
-
// its own start event. Docker kills a restarting container's processes before its state
|
|
275
|
-
// changes, so give it a moment before deciding it's still the same container.
|
|
276
|
-
await sleep(RESTORE_RECHECK_MS);
|
|
277
|
-
const now = await runningPids(record).catch(() => null);
|
|
278
|
-
if (now?.gateway !== pids.gateway || now?.dimension !== pids.dimension)
|
|
279
|
-
return;
|
|
280
|
-
if (attempt === 2)
|
|
281
|
-
return console.warn(`restoring portal ${record.name} failed:`, error);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
18
|
+
async function restorePortals(filter = {}) {
|
|
19
|
+
const failed = await control.restorePortals(filter);
|
|
20
|
+
if (failed)
|
|
21
|
+
console.warn(failed);
|
|
284
22
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return new Set(result.stdout.split("\n").map((line) => line.split(/[@\s]/)[0]).filter(Boolean));
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
if (error instanceof DockerError && (error.status === 404 || error.status === 409))
|
|
295
|
-
return null;
|
|
296
|
-
throw error;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return { createPortal, destroyPortal, listPortals, restorePortals };
|
|
300
|
-
}
|
|
301
|
-
/** Runs `action` unless its container isn't running, in which case there's nothing to undo. */
|
|
302
|
-
async function ignoringStopped(action) {
|
|
303
|
-
try {
|
|
304
|
-
await action();
|
|
305
|
-
}
|
|
306
|
-
catch (error) {
|
|
307
|
-
if (!(error instanceof DockerError && (error.status === 404 || error.status === 409)))
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
function publicPortal({ name, dimensionName, gatewayName }) {
|
|
312
|
-
return { name, dimensionName, gatewayName };
|
|
313
|
-
}
|
|
314
|
-
const WATCH_RETRY_MS = 1_000;
|
|
315
|
-
/** Run one per host: the portals of a container that restarts are gone until something re-wires them. */
|
|
316
|
-
export function createPortalWatcher(portals, docker) {
|
|
317
|
-
let controller;
|
|
318
|
-
let watching;
|
|
319
|
-
function restore(filter) {
|
|
320
|
-
portals.restorePortals(filter).catch((error) => console.warn("restoring portals failed:", error));
|
|
321
|
-
}
|
|
322
|
-
async function watch(signal) {
|
|
323
|
-
while (!signal.aborted) {
|
|
324
|
-
try {
|
|
325
|
-
const events = docker
|
|
326
|
-
.events({ type: ["container"], event: ["start"], label: ["hyperbrane"] }, signal)[Symbol.asyncIterator]();
|
|
327
|
-
// Subscribe first, then restore: a container starting in between is covered either way.
|
|
328
|
-
let next = events.next();
|
|
329
|
-
restore();
|
|
330
|
-
for (let event = await next; !event.done; event = await next) {
|
|
331
|
-
next = events.next();
|
|
332
|
-
const labels = event.value.Actor?.Attributes ?? {};
|
|
333
|
-
if (labels["hyperbrane.gateway"])
|
|
334
|
-
restore({ gatewayName: labels["hyperbrane.gateway"] });
|
|
335
|
-
if (labels["hyperbrane.dimension"])
|
|
336
|
-
restore({ dimensionName: labels["hyperbrane.dimension"] });
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
catch (error) {
|
|
340
|
-
console.warn("watching docker events failed:", error);
|
|
341
|
-
}
|
|
342
|
-
if (!signal.aborted)
|
|
343
|
-
await sleep(WATCH_RETRY_MS);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
function start() {
|
|
347
|
-
if (controller)
|
|
348
|
-
return;
|
|
349
|
-
controller = new AbortController();
|
|
350
|
-
watching = watch(controller.signal);
|
|
351
|
-
}
|
|
352
|
-
async function stop() {
|
|
353
|
-
controller?.abort();
|
|
354
|
-
await watching;
|
|
355
|
-
controller = undefined;
|
|
356
|
-
watching = undefined;
|
|
357
|
-
}
|
|
358
|
-
return { start, stop };
|
|
23
|
+
return {
|
|
24
|
+
createPortal: (portal) => createPortal(portal),
|
|
25
|
+
destroyPortal: (portalName) => destroyPortal(portalName),
|
|
26
|
+
listPortals: () => control.listPortals(),
|
|
27
|
+
restorePortals,
|
|
28
|
+
};
|
|
359
29
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { jwtVerify } from "jose";
|
|
5
|
+
import { dimensionLayout, isDimensionName } from "../dimension/api.js";
|
|
6
|
+
import { createLocalService } from "../sandbox/providers/local/localService.js";
|
|
7
|
+
const TRANSPORT_PROTOCOL = 1;
|
|
8
|
+
const TOKEN_PARAM = "hyb_token";
|
|
9
|
+
const HEARTBEAT_MS = 15_000;
|
|
10
|
+
const connections = new Map();
|
|
11
|
+
async function wired(dimensionName) {
|
|
12
|
+
const files = await readdir(join(data, "portals")).catch(() => []);
|
|
13
|
+
for (const file of files.filter((name) => name.endsWith(".json"))) {
|
|
14
|
+
const portal = await readFile(join(data, "portals", file), "utf8").then((text) => JSON.parse(text), () => null);
|
|
15
|
+
if (portal?.gatewayName === gateway && portal.dimensionName === dimensionName && portal.state === "ready")
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
async function authorized(url) {
|
|
21
|
+
const token = url.searchParams.get(TOKEN_PARAM);
|
|
22
|
+
if (!token)
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
await jwtVerify(token, key, { algorithms: ["HS256"], requiredClaims: ["exp"] });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function cors(request, response) {
|
|
33
|
+
const origin = request.headers.get("origin");
|
|
34
|
+
if (!origin)
|
|
35
|
+
return response;
|
|
36
|
+
response.headers.set("access-control-allow-origin", origin);
|
|
37
|
+
response.headers.append("vary", "Origin");
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
async function preflight(request) {
|
|
41
|
+
const headers = new Headers({
|
|
42
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
43
|
+
"access-control-max-age": "600",
|
|
44
|
+
});
|
|
45
|
+
const asked = request.headers.get("access-control-request-headers");
|
|
46
|
+
if (asked)
|
|
47
|
+
headers.set("access-control-allow-headers", asked);
|
|
48
|
+
return cors(request, new Response(null, { status: 204, headers }));
|
|
49
|
+
}
|
|
50
|
+
async function answer(handler, request, server) {
|
|
51
|
+
server.timeout(request, 0);
|
|
52
|
+
if (!(await authorized(new URL(request.url))))
|
|
53
|
+
return Response.json({ error: "a valid token is required" }, { status: 401 });
|
|
54
|
+
try {
|
|
55
|
+
return await handler(request, server);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const { name, message } = error instanceof Error ? error : new Error(String(error));
|
|
59
|
+
return Response.json({ name, message }, { status: 500 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function guarded(handler) {
|
|
63
|
+
return async (request, server) => {
|
|
64
|
+
const response = await answer(handler, request, server);
|
|
65
|
+
return response && cors(request, response);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function relay(connection, send, end) {
|
|
69
|
+
connection.onData(send);
|
|
70
|
+
void connection.closed.then(end);
|
|
71
|
+
}
|
|
72
|
+
const encoder = new TextEncoder();
|
|
73
|
+
function multipartFrames() {
|
|
74
|
+
const boundary = randomBytes(24).toString("hex");
|
|
75
|
+
const part = (type, data) => Buffer.concat([encoder.encode(`\r\ncontent-type: ${type}\r\n\r\n`), data, encoder.encode(`\r\n--${boundary}`)]);
|
|
76
|
+
return {
|
|
77
|
+
contentType: `multipart/mixed; boundary="${boundary}"`,
|
|
78
|
+
open: (id) => Buffer.concat([encoder.encode(`--${boundary}`), part("application/json", encoder.encode(JSON.stringify({ connection: id })))]),
|
|
79
|
+
data: (chunk) => part("application/octet-stream", chunk),
|
|
80
|
+
heartbeat: part("application/octet-stream", new Uint8Array()),
|
|
81
|
+
end: encoder.encode("--\r\n"),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function lineFrames() {
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
let pending = "";
|
|
87
|
+
return {
|
|
88
|
+
contentType: "text/event-stream",
|
|
89
|
+
open: (id) => encoder.encode(`event: open\ndata: ${JSON.stringify({ connection: id })}\n\n`),
|
|
90
|
+
data(chunk) {
|
|
91
|
+
const lines = (pending + decoder.decode(chunk, { stream: true })).split("\n");
|
|
92
|
+
pending = lines.pop();
|
|
93
|
+
return encoder.encode(lines.map((line) => `data: ${line.replace(/\r$/, "")}\n\n`).join(""));
|
|
94
|
+
},
|
|
95
|
+
heartbeat: encoder.encode(": ping\n\n"),
|
|
96
|
+
end: encoder.encode("event: close\ndata: {}\n\n"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function stream(connection, frames) {
|
|
100
|
+
const id = randomUUID();
|
|
101
|
+
let heartbeat;
|
|
102
|
+
let open = true;
|
|
103
|
+
const finish = () => {
|
|
104
|
+
open = false;
|
|
105
|
+
clearInterval(heartbeat);
|
|
106
|
+
connections.delete(id);
|
|
107
|
+
connection.close();
|
|
108
|
+
};
|
|
109
|
+
connections.set(id, connection);
|
|
110
|
+
const body = new ReadableStream({
|
|
111
|
+
start(controller) {
|
|
112
|
+
const send = (bytes) => {
|
|
113
|
+
if (open && bytes.length > 0)
|
|
114
|
+
controller.enqueue(bytes);
|
|
115
|
+
};
|
|
116
|
+
send(frames.open(id));
|
|
117
|
+
heartbeat = setInterval(() => send(frames.heartbeat), HEARTBEAT_MS);
|
|
118
|
+
relay(connection, (data) => send(frames.data(data)), () => {
|
|
119
|
+
if (!open)
|
|
120
|
+
return;
|
|
121
|
+
send(frames.end);
|
|
122
|
+
finish();
|
|
123
|
+
controller.close();
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
cancel: finish,
|
|
127
|
+
});
|
|
128
|
+
return new Response(body, { headers: { "content-type": frames.contentType, "cache-control": "no-cache, no-transform", "x-accel-buffering": "no" } });
|
|
129
|
+
}
|
|
130
|
+
async function write(request) {
|
|
131
|
+
const connection = connections.get(request.params.id);
|
|
132
|
+
if (!connection)
|
|
133
|
+
return Response.json({ error: "no such connection" }, { status: 404 });
|
|
134
|
+
connection.write(new Uint8Array(await request.arrayBuffer()));
|
|
135
|
+
return new Response(null, { status: 204 });
|
|
136
|
+
}
|
|
137
|
+
async function connect(request, server) {
|
|
138
|
+
const params = new URL(request.url).searchParams;
|
|
139
|
+
const dimensionName = params.get("dimension") ?? "";
|
|
140
|
+
const agentName = params.get("agent");
|
|
141
|
+
if (!isDimensionName(dimensionName) || (agentName !== null && !isDimensionName(agentName))) {
|
|
142
|
+
return Response.json({ error: "dimension and agent are names" }, { status: 400 });
|
|
143
|
+
}
|
|
144
|
+
if (!(await wired(dimensionName)))
|
|
145
|
+
return Response.json({ error: `dimension ${dimensionName} has no portal to gateway ${gateway}` }, { status: 403 });
|
|
146
|
+
const socket = agentName === null ? dimensionLayout.transportSocketPath(data, dimensionName) : join(dimensionLayout.runDirectory(data, dimensionName), `${agentName}.sock`);
|
|
147
|
+
const connection = await machine.connect(socket).catch((error) => error);
|
|
148
|
+
if (connection instanceof Error)
|
|
149
|
+
return Response.json({ error: connection.message }, { status: 502 });
|
|
150
|
+
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket")
|
|
151
|
+
return stream(connection, agentName === null ? multipartFrames() : lineFrames());
|
|
152
|
+
if (server.upgrade(request, { data: connection }))
|
|
153
|
+
return undefined;
|
|
154
|
+
connection.close();
|
|
155
|
+
return Response.json({ error: "the websocket upgrade failed" }, { status: 400 });
|
|
156
|
+
}
|
|
157
|
+
function serve(port) {
|
|
158
|
+
return Bun.serve({
|
|
159
|
+
port,
|
|
160
|
+
hostname: "0.0.0.0",
|
|
161
|
+
routes: {
|
|
162
|
+
"/connect": {
|
|
163
|
+
GET: guarded(connect),
|
|
164
|
+
OPTIONS: preflight,
|
|
165
|
+
},
|
|
166
|
+
"/connect/:id": {
|
|
167
|
+
POST: guarded(write),
|
|
168
|
+
OPTIONS: preflight,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
websocket: {
|
|
172
|
+
open(socket) {
|
|
173
|
+
relay(socket.data, (data) => socket.send(data), () => socket.close());
|
|
174
|
+
},
|
|
175
|
+
message(socket, message) {
|
|
176
|
+
socket.data.write(typeof message === "string" ? Buffer.from(message) : message);
|
|
177
|
+
},
|
|
178
|
+
close(socket) {
|
|
179
|
+
socket.data.close();
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function required(name) {
|
|
185
|
+
const value = process.env[name];
|
|
186
|
+
if (!value)
|
|
187
|
+
throw new Error(`${name} is not set`);
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
process.umask(0o077);
|
|
191
|
+
const dir = required("HYB_TRANSPORT_DIR");
|
|
192
|
+
const build = required("HYB_TRANSPORT_BUILD");
|
|
193
|
+
const port = Number(required("HYB_TRANSPORT_PORT"));
|
|
194
|
+
const gateway = required("HYB_GATEWAY");
|
|
195
|
+
const data = required("HYB_DATA");
|
|
196
|
+
const key = new TextEncoder().encode(await readFile(join(dir, "secret"), "utf8"));
|
|
197
|
+
const machine = createLocalService({ kind: "local" });
|
|
198
|
+
const server = serve(port);
|
|
199
|
+
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
200
|
+
process.once(signal, () => {
|
|
201
|
+
server.stop(true);
|
|
202
|
+
process.exit(0);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
await writeFile(join(dir, "ready"), `${process.pid} ${build} ${TRANSPORT_PROTOCOL}\n`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TRANSPORT_SOURCE: string;
|