hyperbrane 0.1.2 → 0.2.1
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 +105 -23
- 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 +64 -323
- package/dist/agent/agentStorage.d.ts +1 -20
- package/dist/agent/agentStorage.js +0 -30
- package/dist/agent/api.d.ts +105 -152
- 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/dimensionAgentClient.d.ts +8 -0
- package/dist/agent/dimensionAgentClient.js +99 -0
- package/dist/agent/hostTranscript.d.ts +0 -6
- package/dist/agent/hostTranscript.js +3 -14
- package/dist/agent/session/backend.d.ts +18 -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 +3 -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 +30 -21
- 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 +24 -17
- 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 +32 -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 +42 -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 +22 -0
- package/dist/dimension/control.js +346 -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 +85 -227
- 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 +7 -9
- package/dist/index.js +7 -19
- package/dist/lib/control.d.ts +11 -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 +34 -18
- 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/lib/unixSocket.d.ts +2 -0
- package/dist/lib/unixSocket.js +28 -0
- package/dist/portal-gateway/api.d.ts +14 -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 +82 -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/restore.d.ts +9 -0
- package/dist/restore.js +10 -0
- package/dist/sandbox/api.d.ts +126 -187
- package/dist/sandbox/api.js +5 -7
- package/dist/sandbox/client.d.ts +3 -0
- package/dist/sandbox/client.js +7 -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/lib/sshKey.d.ts +6 -0
- package/dist/sandbox/lib/sshKey.js +75 -0
- package/dist/sandbox/providers/boat/boatService.d.ts +3 -0
- package/dist/sandbox/providers/boat/boatService.js +373 -0
- package/dist/sandbox/providers/exedev/exeDevService.d.ts +2 -0
- package/dist/sandbox/providers/exedev/exeDevService.js +129 -0
- package/dist/sandbox/providers/exedev/exeDevToken.d.ts +1 -0
- package/dist/sandbox/providers/exedev/exeDevToken.js +50 -0
- package/dist/sandbox/providers/local/localService.d.ts +2 -0
- package/dist/sandbox/providers/local/localService.js +197 -0
- package/dist/sandbox/providers/ssh/sshService.d.ts +2 -0
- package/dist/sandbox/providers/ssh/sshService.js +83 -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 +3 -0
- package/dist/sandbox/service.js +26 -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/dist/vendor-types/claude/agent-sdk/bridge.d.ts +20 -4
- package/dist/vendor-types/claude/agent-sdk/browser-sdk.d.ts +24 -0
- package/dist/vendor-types/claude/agent-sdk/core.d.ts +2 -0
- package/dist/vendor-types/claude/agent-sdk/sdk-tools.d.ts +75 -6
- package/dist/vendor-types/claude/agent-sdk/sdk.d.ts +569 -100
- package/package.json +13 -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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createPrivateKey, randomBytes } from "node:crypto";
|
|
2
|
+
import ssh2 from "ssh2";
|
|
3
|
+
const FORMATS = "an unencrypted private key in OpenSSH, PKCS#8 (Ed25519, RSA, ECDSA), PKCS#1 (RSA) or SEC1 (ECDSA) format";
|
|
4
|
+
const PEM_WIDTH = 70;
|
|
5
|
+
function parsed(privateKey) {
|
|
6
|
+
const result = ssh2.utils.parseKey(privateKey);
|
|
7
|
+
const key = Array.isArray(result) ? result[0] : result;
|
|
8
|
+
if (!key)
|
|
9
|
+
return new Error("no key found");
|
|
10
|
+
if (!(key instanceof Error) && !key.isPrivateKey())
|
|
11
|
+
return new Error("it's a public key");
|
|
12
|
+
return key;
|
|
13
|
+
}
|
|
14
|
+
function sshString(value) {
|
|
15
|
+
const bytes = typeof value === "string" ? Buffer.from(value) : value;
|
|
16
|
+
const length = Buffer.alloc(4);
|
|
17
|
+
length.writeUInt32BE(bytes.length);
|
|
18
|
+
return Buffer.concat([length, bytes]);
|
|
19
|
+
}
|
|
20
|
+
function uint32(value) {
|
|
21
|
+
const bytes = Buffer.alloc(4);
|
|
22
|
+
bytes.writeUInt32BE(value);
|
|
23
|
+
return bytes;
|
|
24
|
+
}
|
|
25
|
+
function openSshEd25519(key) {
|
|
26
|
+
const { d, x } = key.export({ format: "jwk" });
|
|
27
|
+
const seed = Buffer.from(d, "base64url");
|
|
28
|
+
const publicKey = Buffer.from(x, "base64url");
|
|
29
|
+
const check = randomBytes(4);
|
|
30
|
+
const unpadded = Buffer.concat([check, check, sshString("ssh-ed25519"), sshString(publicKey), sshString(Buffer.concat([seed, publicKey])), sshString("")]);
|
|
31
|
+
const padding = Buffer.from(Array.from({ length: (8 - (unpadded.length % 8)) % 8 }, (_, i) => i + 1));
|
|
32
|
+
const blob = Buffer.concat([
|
|
33
|
+
Buffer.from("openssh-key-v1\0"),
|
|
34
|
+
sshString("none"),
|
|
35
|
+
sshString("none"),
|
|
36
|
+
sshString(""),
|
|
37
|
+
uint32(1),
|
|
38
|
+
sshString(Buffer.concat([sshString("ssh-ed25519"), sshString(publicKey)])),
|
|
39
|
+
sshString(Buffer.concat([unpadded, padding])),
|
|
40
|
+
]);
|
|
41
|
+
const lines = blob.toString("base64").match(new RegExp(`.{1,${PEM_WIDTH}}`, "g"));
|
|
42
|
+
return `-----BEGIN OPENSSH PRIVATE KEY-----\n${lines.join("\n")}\n-----END OPENSSH PRIVATE KEY-----\n`;
|
|
43
|
+
}
|
|
44
|
+
function converted(privateKey) {
|
|
45
|
+
let key;
|
|
46
|
+
try {
|
|
47
|
+
key = createPrivateKey(privateKey);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
switch (key.asymmetricKeyType) {
|
|
53
|
+
case "rsa":
|
|
54
|
+
return key.export({ type: "pkcs1", format: "pem" });
|
|
55
|
+
case "ec":
|
|
56
|
+
return key.export({ type: "sec1", format: "pem" });
|
|
57
|
+
case "ed25519":
|
|
58
|
+
return openSshEd25519(key);
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function parseSshKey(privateKey, whose) {
|
|
64
|
+
const key = parsed(privateKey);
|
|
65
|
+
if (!(key instanceof Error))
|
|
66
|
+
return { privateKey, key };
|
|
67
|
+
const readable = converted(privateKey);
|
|
68
|
+
const convertedKey = readable === null ? null : parsed(readable);
|
|
69
|
+
if (readable !== null && convertedKey !== null && !(convertedKey instanceof Error))
|
|
70
|
+
return { privateKey: readable, key: convertedKey };
|
|
71
|
+
throw new Error(`${whose} SSH key can't be read (${key.message}): it takes ${FORMATS}`);
|
|
72
|
+
}
|
|
73
|
+
export function sshPrivateKey(privateKey, whose) {
|
|
74
|
+
return parseSshKey(privateKey, whose).privateKey;
|
|
75
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type SandboxAuthContext, type SandboxListing, type SandboxService } from "../../api.ts";
|
|
2
|
+
export declare function listBoatSandboxes(ctx: SandboxAuthContext): Promise<SandboxListing[]>;
|
|
3
|
+
export declare function createBoatService(ctx: SandboxAuthContext): SandboxService;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { posix } from "node:path";
|
|
3
|
+
import { BoatApi, Configuration, ResponseError, waitUntilReady } from "@boatdev/sdk";
|
|
4
|
+
import { SandboxNotFoundError, SandboxPathError, SandboxStoppedError, } from "../../api.js";
|
|
5
|
+
import { commandLine, quote } from "../../lib/shellCommand.js";
|
|
6
|
+
import { commandFileOperations } from "../../lib/remoteFileOperations.js";
|
|
7
|
+
import { parseSshKey } from "../../lib/sshKey.js";
|
|
8
|
+
import { createDescriptor, getDescriptor, openDescriptor } from "../../storage/descriptors.js";
|
|
9
|
+
import { createEndpoints } from "../../data-endpoints/dataEndpoints.js";
|
|
10
|
+
import { createSandboxStorage } from "../../storage/sandboxStorage.js";
|
|
11
|
+
import { sshConnect, sshFetch } from "../ssh/sshTransport.js";
|
|
12
|
+
const BOAT_API_URL = "https://boat.dev/api/v1";
|
|
13
|
+
const WORK_DIRECTORY = "/home/user";
|
|
14
|
+
const FILE_API_DIRECTORIES = [WORK_DIRECTORY, "/tmp"];
|
|
15
|
+
const REFUSED_PATH = /^Path must resolve under/;
|
|
16
|
+
const COMMAND_TIMEOUT_S = 600;
|
|
17
|
+
const READY_TIMEOUT_MS = 180_000;
|
|
18
|
+
const RUNNING_STATES = ["ready", "idle", "running"];
|
|
19
|
+
const STOPPED_CODES = ["machine_not_running", "sandbox_not_ready"];
|
|
20
|
+
const UNDELIVERED = ["localhost_relay_failed", "sandbox_direct_failed"];
|
|
21
|
+
const RESTORING = "sandbox_restoring";
|
|
22
|
+
const UNDELIVERED_RETRY_MS = [1000, 2000, 4000];
|
|
23
|
+
const LISTED_PER_PAGE = 200;
|
|
24
|
+
const UNLISTED_STATES = ["cancelled", "error"];
|
|
25
|
+
const DEFAULT_PORTS = {
|
|
26
|
+
from: 8900,
|
|
27
|
+
to: 8999,
|
|
28
|
+
};
|
|
29
|
+
class BoatError extends Error {
|
|
30
|
+
status;
|
|
31
|
+
code;
|
|
32
|
+
detail;
|
|
33
|
+
retryable;
|
|
34
|
+
name = "BoatError";
|
|
35
|
+
constructor(status, code, detail, retryable, message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.detail = detail;
|
|
40
|
+
this.retryable = retryable;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function apiKeyOf(ctx) {
|
|
44
|
+
const apiKey = ctx.apikey ?? process.env.BOAT_API_KEY;
|
|
45
|
+
if (!apiKey)
|
|
46
|
+
throw new Error("a Boat sandbox needs Boat's API key (apikey, or BOAT_API_KEY)");
|
|
47
|
+
return apiKey;
|
|
48
|
+
}
|
|
49
|
+
function apiOf(ctx) {
|
|
50
|
+
return new BoatApi(new Configuration({ basePath: BOAT_API_URL, accessToken: apiKeyOf(ctx) }));
|
|
51
|
+
}
|
|
52
|
+
function sandboxIdOf(ctx) {
|
|
53
|
+
if (!ctx.id)
|
|
54
|
+
throw new Error("the Boat sandbox has no id yet: it's set once it's created");
|
|
55
|
+
return ctx.id;
|
|
56
|
+
}
|
|
57
|
+
function where(ctx) {
|
|
58
|
+
return `Boat sandbox ${ctx.id ?? "(not created)"}`;
|
|
59
|
+
}
|
|
60
|
+
async function boatOnce(what, call) {
|
|
61
|
+
try {
|
|
62
|
+
return await call();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (!(error instanceof ResponseError))
|
|
66
|
+
throw error;
|
|
67
|
+
const text = await error.response.text().catch(() => "");
|
|
68
|
+
let body = {};
|
|
69
|
+
try {
|
|
70
|
+
body = JSON.parse(text);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
body = { message: text };
|
|
74
|
+
}
|
|
75
|
+
const code = body.code ?? "";
|
|
76
|
+
if (STOPPED_CODES.includes(code))
|
|
77
|
+
throw new SandboxStoppedError(`${what}: the Boat sandbox is stopped`);
|
|
78
|
+
const details = body.error?.details ?? {};
|
|
79
|
+
throw new BoatError(error.response.status, code, details.message ?? body.message ?? "", details.retryable, `${what} failed (${error.response.status}${code ? ` ${code}` : ""}): ${body.message ?? text}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function undelivered(error, reaches) {
|
|
83
|
+
if (reaches === "command")
|
|
84
|
+
return error.code === "sandbox_direct_failed" && error.detail === RESTORING;
|
|
85
|
+
if (reaches === "file")
|
|
86
|
+
return error.code === "localhost_relay_failed" || (error.code === "sandbox_direct_failed" && error.retryable === true);
|
|
87
|
+
return UNDELIVERED.includes(error.code);
|
|
88
|
+
}
|
|
89
|
+
async function boat(what, call, reaches = "api") {
|
|
90
|
+
for (const wait of [...UNDELIVERED_RETRY_MS, null]) {
|
|
91
|
+
try {
|
|
92
|
+
return await boatOnce(what, call);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (wait === null || !(error instanceof BoatError) || !undelivered(error, reaches))
|
|
96
|
+
throw error;
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`${what} failed`);
|
|
101
|
+
}
|
|
102
|
+
async function ensureRunning(ctx) {
|
|
103
|
+
const api = apiOf(ctx);
|
|
104
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
105
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
106
|
+
for (;;) {
|
|
107
|
+
const { sandbox } = await boat(`reading ${where(ctx)}`, () => api.get({ sandboxId }));
|
|
108
|
+
if (RUNNING_STATES.includes(sandbox.state))
|
|
109
|
+
return sandbox;
|
|
110
|
+
if (sandbox.state === "archived") {
|
|
111
|
+
await boat(`resuming ${where(ctx)}`, () => api.resume({ sandboxId }));
|
|
112
|
+
return waitUntilReady(api, sandboxId, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
113
|
+
}
|
|
114
|
+
if (sandbox.state === "error" || sandbox.state === "cancelled")
|
|
115
|
+
throw new Error(`${where(ctx)} is ${sandbox.state}${sandbox.error ? `: ${sandbox.error}` : ""}`);
|
|
116
|
+
if (Date.now() > deadline)
|
|
117
|
+
throw new Error(`${where(ctx)} stayed ${sandbox.state}`);
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function resuming(ctx, call) {
|
|
122
|
+
try {
|
|
123
|
+
return await call();
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!(error instanceof SandboxStoppedError))
|
|
127
|
+
throw error;
|
|
128
|
+
await ensureRunning(ctx);
|
|
129
|
+
return call();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function command(ctx, line, cwd) {
|
|
133
|
+
const api = apiOf(ctx);
|
|
134
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
135
|
+
const result = await boat(`a command in ${where(ctx)}`, () => api.command({ sandboxId, command: `exec sh -c ${quote(line)}`, cwd, timeoutSeconds: COMMAND_TIMEOUT_S }), "command");
|
|
136
|
+
if (!("exitCode" in result))
|
|
137
|
+
throw new Error(`a command in ${where(ctx)} was started in the background`);
|
|
138
|
+
if (result.timedOut)
|
|
139
|
+
throw new Error(`a command in ${where(ctx)} ran past ${COMMAND_TIMEOUT_S}s`);
|
|
140
|
+
if (result.stdoutTruncated || result.stderrTruncated)
|
|
141
|
+
throw new Error(`Boat cut the output of a command in ${where(ctx)} short`);
|
|
142
|
+
return { exitCode: result.exitCode ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
143
|
+
}
|
|
144
|
+
function reachable(ctx, path) {
|
|
145
|
+
if (FILE_API_DIRECTORIES.some((directory) => path === directory || path.startsWith(`${directory}/`)))
|
|
146
|
+
return path;
|
|
147
|
+
throw new SandboxPathError(`${path} is outside ${FILE_API_DIRECTORIES.join(" and ")}, the directories Boat's file API reaches in ${where(ctx)}`);
|
|
148
|
+
}
|
|
149
|
+
function refused(ctx, path, error) {
|
|
150
|
+
if (!(error instanceof BoatError) || !(error.code === "invalid_path" || REFUSED_PATH.test(error.detail)))
|
|
151
|
+
return error;
|
|
152
|
+
return new SandboxPathError(`Boat's file API refuses ${path} in ${where(ctx)}: ${error.detail}`);
|
|
153
|
+
}
|
|
154
|
+
async function upload(ctx, path, data) {
|
|
155
|
+
const api = apiOf(ctx);
|
|
156
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
157
|
+
const content = Buffer.from(data).toString("base64");
|
|
158
|
+
try {
|
|
159
|
+
await boat(`writing ${path} in ${where(ctx)}`, () => api.writeFile({ sandboxId, path: reachable(ctx, path), content, encoding: "base64" }), "file");
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw refused(ctx, path, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function runCommand(ctx, cmd, { cwd, env, stdin } = {}) {
|
|
166
|
+
const directory = cwd === undefined ? undefined : posix.resolve("/", cwd);
|
|
167
|
+
if (stdin === undefined)
|
|
168
|
+
return resuming(ctx, () => command(ctx, commandLine(cmd, { env }), directory));
|
|
169
|
+
return resuming(ctx, async () => {
|
|
170
|
+
const staged = `${WORK_DIRECTORY}/.hyperbrane-stdin-${randomUUID()}`;
|
|
171
|
+
await upload(ctx, staged, typeof stdin === "string" ? new TextEncoder().encode(stdin) : stdin);
|
|
172
|
+
return command(ctx, `(${commandLine(cmd, { env })}) < ${quote(staged)}; code=$?; rm -f ${quote(staged)}; exit "$code"`, directory);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function boatMachine(ctx) {
|
|
176
|
+
async function readBytes(path, start, end) {
|
|
177
|
+
const api = apiOf(ctx);
|
|
178
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
179
|
+
reachable(ctx, path);
|
|
180
|
+
try {
|
|
181
|
+
const { content } = await boat(`reading ${path} in ${where(ctx)}`, () => api.readFile({ sandboxId, path, encoding: "base64" }), "file");
|
|
182
|
+
const data = new Uint8Array(Buffer.from(content, "base64"));
|
|
183
|
+
return start === undefined ? data : data.subarray(start, end);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const failure = refused(ctx, path, error);
|
|
187
|
+
if (failure === error && error instanceof BoatError && (await absent(path)))
|
|
188
|
+
throw new SandboxNotFoundError(`Not found: "${path}"`);
|
|
189
|
+
throw failure;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function absent(path) {
|
|
193
|
+
const checked = await command(ctx, `test -e ${quote(path)}`).catch(() => null);
|
|
194
|
+
return checked?.exitCode === 1;
|
|
195
|
+
}
|
|
196
|
+
async function putFile(path, data, exclusive) {
|
|
197
|
+
reachable(ctx, path);
|
|
198
|
+
return resuming(ctx, async () => {
|
|
199
|
+
const staged = `${posix.dirname(path)}/.hyperbrane-${randomUUID()}.tmp`;
|
|
200
|
+
const made = await command(ctx, `mkdir -p ${quote(posix.dirname(path))}`);
|
|
201
|
+
if (made.exitCode !== 0)
|
|
202
|
+
throw new Error(`writing ${path} in ${where(ctx)} failed: ${made.stderr.trim()}`);
|
|
203
|
+
await upload(ctx, staged, data);
|
|
204
|
+
const placed = exclusive
|
|
205
|
+
? await command(ctx, `ln ${quote(staged)} ${quote(path)} 2> /dev/null; code=$?; rm -f ${quote(staged)}; exit "$code"`)
|
|
206
|
+
: await command(ctx, `mv -f ${quote(staged)} ${quote(path)}`);
|
|
207
|
+
if (exclusive)
|
|
208
|
+
return placed.exitCode === 0;
|
|
209
|
+
if (placed.exitCode !== 0)
|
|
210
|
+
throw new Error(`writing ${path} in ${where(ctx)} failed: ${placed.stderr.trim()}`);
|
|
211
|
+
return true;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
runCommand: (cmd, options) => runCommand(ctx, cmd, options),
|
|
216
|
+
readBytes,
|
|
217
|
+
putFile,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function sshTarget(ctx) {
|
|
221
|
+
if (!ctx.sshKey)
|
|
222
|
+
throw new Error(`${where(ctx)} has no shell: its context has no SSH key (sshKey)`);
|
|
223
|
+
const { privateKey, key } = parseSshKey(ctx.sshKey, `${where(ctx)}'s`);
|
|
224
|
+
await ensureRunning(ctx);
|
|
225
|
+
const api = apiOf(ctx);
|
|
226
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
227
|
+
const publicKey = `${key.type} ${key.getPublicSSH().toString("base64")}`;
|
|
228
|
+
const response = await boat(`adding an SSH key to ${where(ctx)}`, () => api.sshKey({ sandboxId, key: publicKey }));
|
|
229
|
+
const user = response.sshUser ?? "user";
|
|
230
|
+
const endpoint = /^(.+):(\d+)$/.exec(response.sshEndpoint ?? "");
|
|
231
|
+
if (endpoint)
|
|
232
|
+
return { host: endpoint[1], port: Number(endpoint[2]), user, privateKey };
|
|
233
|
+
if (response.machineIp)
|
|
234
|
+
return { host: response.machineIp, port: 22, user, privateKey };
|
|
235
|
+
throw new Error(`Boat gave no SSH endpoint for ${where(ctx)}`);
|
|
236
|
+
}
|
|
237
|
+
function groupsOf(ctx) {
|
|
238
|
+
const fileOps = commandFileOperations(boatMachine(ctx));
|
|
239
|
+
const cmdOps = {
|
|
240
|
+
runCommand: (cmd, options) => runCommand(ctx, cmd, options),
|
|
241
|
+
};
|
|
242
|
+
const netOps = {
|
|
243
|
+
fetch: async (url, init) => sshFetch(await sshTarget(ctx), url, init),
|
|
244
|
+
connect: async (path) => sshConnect(await sshTarget(ctx), path),
|
|
245
|
+
};
|
|
246
|
+
return { fileOps, cmdOps, netOps };
|
|
247
|
+
}
|
|
248
|
+
function storageOf(ctx) {
|
|
249
|
+
const { fileOps, cmdOps, netOps } = groupsOf(ctx);
|
|
250
|
+
return createSandboxStorage({ ...fileOps, ...cmdOps, ...netOps });
|
|
251
|
+
}
|
|
252
|
+
async function create(ctx, spec) {
|
|
253
|
+
const api = apiOf(ctx);
|
|
254
|
+
const created = await boat("creating a Boat sandbox", () => api.create({ idempotencyKey: randomUUID() }));
|
|
255
|
+
const sandboxId = created.sandbox.id;
|
|
256
|
+
try {
|
|
257
|
+
await boat(`naming Boat sandbox ${sandboxId}`, () => api.update({ sandboxId, name: spec.name }));
|
|
258
|
+
await waitUntilReady(api, sandboxId, { timeoutMs: READY_TIMEOUT_MS });
|
|
259
|
+
return await createDescriptor(ctx, storageOf({ ...ctx, id: sandboxId }), sandboxId, spec);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
await boat(`stopping Boat sandbox ${sandboxId}`, () => api.stop({ sandboxId })).catch(() => { });
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function open(ctx, storage) {
|
|
267
|
+
return openDescriptor(ctx, storage, sandboxIdOf(ctx));
|
|
268
|
+
}
|
|
269
|
+
async function state(ctx) {
|
|
270
|
+
const api = apiOf(ctx);
|
|
271
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
272
|
+
const { sandbox } = await boat(`reading ${where(ctx)}`, () => api.get({ sandboxId }));
|
|
273
|
+
return RUNNING_STATES.includes(sandbox.state) ? "running" : "stopped";
|
|
274
|
+
}
|
|
275
|
+
async function start(ctx) {
|
|
276
|
+
await ensureRunning(ctx);
|
|
277
|
+
}
|
|
278
|
+
async function destroy(ctx) {
|
|
279
|
+
const api = apiOf(ctx);
|
|
280
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
281
|
+
await boat(`stopping ${where(ctx)}`, () => api.stop({ sandboxId }));
|
|
282
|
+
}
|
|
283
|
+
function boatPorts(ctx, storage) {
|
|
284
|
+
async function publish(port, isPublic) {
|
|
285
|
+
const api = apiOf(ctx);
|
|
286
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
287
|
+
const response = await boat(`publishing port ${port} of ${where(ctx)}`, () => api.hostPort({ sandboxId, port, _public: isPublic }));
|
|
288
|
+
return response.url;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
ports: async () => (await storage.readDescriptor())?.dataEndpointOptions?.ports ?? DEFAULT_PORTS,
|
|
292
|
+
async expose(port) {
|
|
293
|
+
const url = await publish(port, true);
|
|
294
|
+
if (!url)
|
|
295
|
+
throw new Error(`Boat gave no URL for port ${port} of ${where(ctx)}`);
|
|
296
|
+
return url.replace(/\/$/, "");
|
|
297
|
+
},
|
|
298
|
+
async conceal(port) {
|
|
299
|
+
await publish(port, false);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function hasExpiration() {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
async function getExpiration(ctx) {
|
|
307
|
+
const api = apiOf(ctx);
|
|
308
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
309
|
+
const { sandbox } = await boat(`reading ${where(ctx)}`, () => api.get({ sandboxId }));
|
|
310
|
+
return sandbox.archiveAfter ? new Date(sandbox.archiveAfter) : null;
|
|
311
|
+
}
|
|
312
|
+
async function setExpiration(ctx, seconds) {
|
|
313
|
+
if (seconds !== null && !(Number.isInteger(seconds) && seconds > 0))
|
|
314
|
+
throw new Error(`not a number of seconds: ${seconds}`);
|
|
315
|
+
const api = apiOf(ctx);
|
|
316
|
+
const sandboxId = sandboxIdOf(ctx);
|
|
317
|
+
const { sandbox } = await boat(`setting when ${where(ctx)} stops`, () => api.update({ sandboxId, ttlSeconds: seconds }));
|
|
318
|
+
return sandbox.archiveAfter ? new Date(sandbox.archiveAfter) : null;
|
|
319
|
+
}
|
|
320
|
+
function hasShell(ctx) {
|
|
321
|
+
return ctx.sshKey !== undefined;
|
|
322
|
+
}
|
|
323
|
+
async function sshEndpoint(ctx) {
|
|
324
|
+
return sshTarget(ctx);
|
|
325
|
+
}
|
|
326
|
+
export async function listBoatSandboxes(ctx) {
|
|
327
|
+
const api = apiOf(ctx);
|
|
328
|
+
const listings = [];
|
|
329
|
+
let cursor;
|
|
330
|
+
do {
|
|
331
|
+
const page = await boat("listing Boat sandboxes", () => api.sandboxes({ limit: LISTED_PER_PAGE, cursor }));
|
|
332
|
+
for (const sandbox of page.sandboxes) {
|
|
333
|
+
if (UNLISTED_STATES.includes(sandbox.state))
|
|
334
|
+
continue;
|
|
335
|
+
listings.push({ id: sandbox.id, name: sandbox.name, state: RUNNING_STATES.includes(sandbox.state) ? "running" : "stopped" });
|
|
336
|
+
}
|
|
337
|
+
cursor = page.pageInfo?.hasMore ? (page.pageInfo.nextCursor ?? undefined) : undefined;
|
|
338
|
+
} while (cursor);
|
|
339
|
+
return listings;
|
|
340
|
+
}
|
|
341
|
+
export function createBoatService(ctx) {
|
|
342
|
+
apiKeyOf(ctx);
|
|
343
|
+
const { fileOps, cmdOps, netOps } = groupsOf(ctx);
|
|
344
|
+
const machine = { ...fileOps, ...cmdOps, ...netOps };
|
|
345
|
+
const storage = createSandboxStorage(machine);
|
|
346
|
+
const lifecycleOps = {
|
|
347
|
+
create: (spec) => create(ctx, spec),
|
|
348
|
+
open: () => resuming(ctx, () => open(ctx, storage)),
|
|
349
|
+
get: () => getDescriptor(storage, where(ctx)),
|
|
350
|
+
state: () => state(ctx),
|
|
351
|
+
start: () => start(ctx),
|
|
352
|
+
destroy: () => destroy(ctx),
|
|
353
|
+
};
|
|
354
|
+
const dataOps = createEndpoints(machine, storage, boatPorts(ctx, storage));
|
|
355
|
+
const expirationOps = {
|
|
356
|
+
hasExpiration,
|
|
357
|
+
getExpiration: () => getExpiration(ctx),
|
|
358
|
+
setExpiration: (seconds) => setExpiration(ctx, seconds),
|
|
359
|
+
};
|
|
360
|
+
const shellOps = {
|
|
361
|
+
hasShell: () => hasShell(ctx),
|
|
362
|
+
sshEndpoint: () => sshEndpoint(ctx),
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
...fileOps,
|
|
366
|
+
...lifecycleOps,
|
|
367
|
+
...cmdOps,
|
|
368
|
+
...dataOps,
|
|
369
|
+
...expirationOps,
|
|
370
|
+
...shellOps,
|
|
371
|
+
...netOps,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { userInfo } from "node:os";
|
|
2
|
+
import { commandFileOperations } from "../../lib/remoteFileOperations.js";
|
|
3
|
+
import { sshPrivateKey } from "../../lib/sshKey.js";
|
|
4
|
+
import { createDescriptor, getDescriptor, openDescriptor } from "../../storage/descriptors.js";
|
|
5
|
+
import { createEndpoints } from "../../data-endpoints/dataEndpoints.js";
|
|
6
|
+
import { createSandboxStorage } from "../../storage/sandboxStorage.js";
|
|
7
|
+
import { sshCommand, sshConnect, sshFetch, sshMachine } from "../ssh/sshTransport.js";
|
|
8
|
+
import { signExeDevToken } from "./exeDevToken.js";
|
|
9
|
+
const VM_NAME = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
10
|
+
const FORWARDED = {
|
|
11
|
+
from: 3000,
|
|
12
|
+
to: 9999,
|
|
13
|
+
};
|
|
14
|
+
const DEFAULT_PORTS = {
|
|
15
|
+
from: 9000,
|
|
16
|
+
to: 9099,
|
|
17
|
+
};
|
|
18
|
+
function vmOf(ctx) {
|
|
19
|
+
if (!ctx.id)
|
|
20
|
+
throw new Error("an exe.dev VM needs its name (id)");
|
|
21
|
+
if (!VM_NAME.test(ctx.id))
|
|
22
|
+
throw new Error(`not an exe.dev VM name: "${ctx.id}"`);
|
|
23
|
+
return ctx.id;
|
|
24
|
+
}
|
|
25
|
+
function target(ctx) {
|
|
26
|
+
if (!ctx.sshKey)
|
|
27
|
+
throw new Error(`exe.dev VM ${ctx.id ?? "(no name)"} needs the account's SSH key (sshKey)`);
|
|
28
|
+
const privateKey = sshPrivateKey(ctx.sshKey, "the exe.dev account's");
|
|
29
|
+
if (ctx.sshHost)
|
|
30
|
+
return { host: ctx.sshHost, port: ctx.sshPort ?? 22, user: ctx.sshUser ?? userInfo().username, privateKey };
|
|
31
|
+
return { host: "vm.exe.xyz", port: 22, user: `vm+${vmOf(ctx)}`, privateKey };
|
|
32
|
+
}
|
|
33
|
+
function where(ctx) {
|
|
34
|
+
return `exe.dev VM ${ctx.id ?? "(no name)"}`;
|
|
35
|
+
}
|
|
36
|
+
function groupsOf(ctx) {
|
|
37
|
+
const fileOps = commandFileOperations(sshMachine(() => target(ctx)));
|
|
38
|
+
const cmdOps = {
|
|
39
|
+
runCommand: (cmd, options) => sshCommand(target(ctx), cmd, options),
|
|
40
|
+
};
|
|
41
|
+
const netOps = {
|
|
42
|
+
fetch: (url, init) => sshFetch(target(ctx), url, init),
|
|
43
|
+
connect: (path) => sshConnect(target(ctx), path),
|
|
44
|
+
};
|
|
45
|
+
return { fileOps, cmdOps, netOps };
|
|
46
|
+
}
|
|
47
|
+
function storageOf(ctx) {
|
|
48
|
+
const { fileOps, cmdOps, netOps } = groupsOf(ctx);
|
|
49
|
+
return createSandboxStorage({ ...fileOps, ...cmdOps, ...netOps });
|
|
50
|
+
}
|
|
51
|
+
async function create(ctx, spec) {
|
|
52
|
+
const id = ctx.id ?? spec.name;
|
|
53
|
+
return createDescriptor(ctx, storageOf({ ...ctx, id }), id, spec);
|
|
54
|
+
}
|
|
55
|
+
async function open(ctx, storage) {
|
|
56
|
+
return openDescriptor(ctx, storage, vmOf(ctx));
|
|
57
|
+
}
|
|
58
|
+
async function state() {
|
|
59
|
+
return "running";
|
|
60
|
+
}
|
|
61
|
+
async function start() { }
|
|
62
|
+
async function destroy() { }
|
|
63
|
+
function hasExpiration() {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
async function getExpiration() {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
async function setExpiration() {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function hasShell(ctx) {
|
|
73
|
+
return ctx.id !== undefined && ctx.sshKey !== undefined;
|
|
74
|
+
}
|
|
75
|
+
async function sshEndpoint(ctx) {
|
|
76
|
+
return target(ctx);
|
|
77
|
+
}
|
|
78
|
+
function exeDevPorts(ctx, storage) {
|
|
79
|
+
return {
|
|
80
|
+
async ports() {
|
|
81
|
+
const ports = (await storage.readDescriptor())?.dataEndpointOptions?.ports ?? DEFAULT_PORTS;
|
|
82
|
+
if (ports.from < FORWARDED.from || ports.to > FORWARDED.to)
|
|
83
|
+
throw new Error(`exe.dev forwards ports ${FORWARDED.from}-${FORWARDED.to}, not ${ports.from}-${ports.to}`);
|
|
84
|
+
return ports;
|
|
85
|
+
},
|
|
86
|
+
async expose(port) {
|
|
87
|
+
return `https://${vmOf(ctx)}.exe.xyz:${port}`;
|
|
88
|
+
},
|
|
89
|
+
async conceal() { },
|
|
90
|
+
async credentials(_endpoint, ttlSeconds) {
|
|
91
|
+
if (!ctx.sshKey)
|
|
92
|
+
throw new Error(`${where(ctx)} needs the account's SSH key (sshKey) to sign exe.dev tokens`);
|
|
93
|
+
const exp = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
94
|
+
return { exeDev: { token: signExeDevToken(ctx.sshKey, `v0@${vmOf(ctx)}.exe.xyz`, { exp }) } };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function createExeDevService(ctx) {
|
|
99
|
+
const { fileOps, cmdOps, netOps } = groupsOf(ctx);
|
|
100
|
+
const machine = { ...fileOps, ...cmdOps, ...netOps };
|
|
101
|
+
const storage = createSandboxStorage(machine);
|
|
102
|
+
const lifecycleOps = {
|
|
103
|
+
create: (spec) => create(ctx, spec),
|
|
104
|
+
open: () => open(ctx, storage),
|
|
105
|
+
get: () => getDescriptor(storage, where(ctx)),
|
|
106
|
+
state,
|
|
107
|
+
start,
|
|
108
|
+
destroy,
|
|
109
|
+
};
|
|
110
|
+
const dataOps = createEndpoints(machine, storage, exeDevPorts(ctx, storage));
|
|
111
|
+
const expirationOps = {
|
|
112
|
+
hasExpiration,
|
|
113
|
+
getExpiration,
|
|
114
|
+
setExpiration,
|
|
115
|
+
};
|
|
116
|
+
const shellOps = {
|
|
117
|
+
hasShell: () => hasShell(ctx),
|
|
118
|
+
sshEndpoint: () => sshEndpoint(ctx),
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
...fileOps,
|
|
122
|
+
...lifecycleOps,
|
|
123
|
+
...cmdOps,
|
|
124
|
+
...dataOps,
|
|
125
|
+
...expirationOps,
|
|
126
|
+
...shellOps,
|
|
127
|
+
...netOps,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function signExeDevToken(privateKey: string, namespace: string, permissions: Record<string, unknown>): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parseSshKey } from "../../lib/sshKey.js";
|
|
3
|
+
function sshString(value) {
|
|
4
|
+
const bytes = typeof value === "string" ? Buffer.from(value) : value;
|
|
5
|
+
const length = Buffer.alloc(4);
|
|
6
|
+
length.writeUInt32BE(bytes.length);
|
|
7
|
+
return Buffer.concat([length, bytes]);
|
|
8
|
+
}
|
|
9
|
+
function privateKeyOf(privateKey) {
|
|
10
|
+
return parseSshKey(privateKey, "the exe.dev account's").key;
|
|
11
|
+
}
|
|
12
|
+
function ecdsaBlob(der) {
|
|
13
|
+
let offset = 2 + (der[1] & 0x80 ? der[1] & 0x7f : 0);
|
|
14
|
+
const integers = [];
|
|
15
|
+
for (let i = 0; i < 2; i++) {
|
|
16
|
+
const length = der[offset + 1];
|
|
17
|
+
integers.push(sshString(der.subarray(offset + 2, offset + 2 + length)));
|
|
18
|
+
offset += 2 + length;
|
|
19
|
+
}
|
|
20
|
+
return Buffer.concat(integers);
|
|
21
|
+
}
|
|
22
|
+
function signed(key, data) {
|
|
23
|
+
if (key.type === "ssh-ed25519")
|
|
24
|
+
return [key.type, key.sign(data)];
|
|
25
|
+
if (key.type === "ssh-rsa")
|
|
26
|
+
return ["rsa-sha2-512", key.sign(data, "sha512")];
|
|
27
|
+
if (key.type.startsWith("ecdsa-sha2-"))
|
|
28
|
+
return [key.type, ecdsaBlob(key.sign(data))];
|
|
29
|
+
throw new Error(`exe.dev tokens can't be signed with a ${key.type} key`);
|
|
30
|
+
}
|
|
31
|
+
function sshSignature(key, namespace, message) {
|
|
32
|
+
const magic = Buffer.from("SSHSIG");
|
|
33
|
+
const hash = createHash("sha512").update(message).digest();
|
|
34
|
+
const [algorithm, signature] = signed(key, Buffer.concat([magic, sshString(namespace), sshString(""), sshString("sha512"), sshString(hash)]));
|
|
35
|
+
const version = Buffer.alloc(4);
|
|
36
|
+
version.writeUInt32BE(1);
|
|
37
|
+
return Buffer.concat([
|
|
38
|
+
magic,
|
|
39
|
+
version,
|
|
40
|
+
sshString(key.getPublicSSH()),
|
|
41
|
+
sshString(namespace),
|
|
42
|
+
sshString(""),
|
|
43
|
+
sshString("sha512"),
|
|
44
|
+
sshString(Buffer.concat([sshString(algorithm), sshString(signature)])),
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
export function signExeDevToken(privateKey, namespace, permissions) {
|
|
48
|
+
const payload = Buffer.from(JSON.stringify(permissions));
|
|
49
|
+
return `exe0.${payload.toString("base64url")}.${sshSignature(privateKeyOf(privateKey), namespace, payload).toString("base64url")}`;
|
|
50
|
+
}
|