@alchemy.run/boat 0.0.0 → 2.0.0-beta.87
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/LICENSE +201 -0
- package/README.md +66 -4
- package/lib/AiProxy.d.ts +89 -0
- package/lib/AiProxy.d.ts.map +1 -0
- package/lib/AiProxy.js +125 -0
- package/lib/AiProxy.js.map +1 -0
- package/lib/ApiKey.d.ts +105 -0
- package/lib/ApiKey.d.ts.map +1 -0
- package/lib/ApiKey.js +134 -0
- package/lib/ApiKey.js.map +1 -0
- package/lib/AuthProvider.d.ts +71 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +322 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/BoatBindings.d.ts +52 -0
- package/lib/BoatBindings.d.ts.map +1 -0
- package/lib/BoatBindings.js +104 -0
- package/lib/BoatBindings.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +28 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +56 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +3 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/ConnectSandboxHttp.d.ts +21 -0
- package/lib/ConnectSandboxHttp.d.ts.map +1 -0
- package/lib/ConnectSandboxHttp.js +46 -0
- package/lib/ConnectSandboxHttp.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateSandbox.d.ts +134 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +3 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/CreateSandboxHttp.d.ts +21 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +93 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CredentialProfile.d.ts +157 -0
- package/lib/CredentialProfile.d.ts.map +1 -0
- package/lib/CredentialProfile.js +245 -0
- package/lib/CredentialProfile.js.map +1 -0
- package/lib/Credentials.d.ts +15 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +23 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Environment.d.ts +219 -0
- package/lib/Environment.d.ts.map +1 -0
- package/lib/Environment.js +273 -0
- package/lib/Environment.js.map +1 -0
- package/lib/Errors.d.ts +50 -0
- package/lib/Errors.d.ts.map +1 -0
- package/lib/Errors.js +17 -0
- package/lib/Errors.js.map +1 -0
- package/lib/Providers.d.ts +33 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +47 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Sandbox.d.ts +205 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +368 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +159 -0
- package/lib/SandboxClient.d.ts.map +1 -0
- package/lib/SandboxClient.js +1 -0
- package/lib/SandboxClient.js.map +1 -0
- package/lib/SandboxEvents.d.ts +111 -0
- package/lib/SandboxEvents.d.ts.map +1 -0
- package/lib/SandboxEvents.js +60 -0
- package/lib/SandboxEvents.js.map +1 -0
- package/lib/SandboxEventsLive.d.ts +13 -0
- package/lib/SandboxEventsLive.d.ts.map +1 -0
- package/lib/SandboxEventsLive.js +63 -0
- package/lib/SandboxEventsLive.js.map +1 -0
- package/lib/Snapshot.d.ts +123 -0
- package/lib/Snapshot.d.ts.map +1 -0
- package/lib/Snapshot.js +173 -0
- package/lib/Snapshot.js.map +1 -0
- package/lib/Webhook.d.ts +56 -0
- package/lib/Webhook.d.ts.map +1 -0
- package/lib/Webhook.js +98 -0
- package/lib/Webhook.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/Declared.d.ts +34 -0
- package/lib/internal/Declared.d.ts.map +1 -0
- package/lib/internal/Declared.js +38 -0
- package/lib/internal/Declared.js.map +1 -0
- package/lib/internal/Org.d.ts +22 -0
- package/lib/internal/Org.d.ts.map +1 -0
- package/lib/internal/Org.js +23 -0
- package/lib/internal/Org.js.map +1 -0
- package/lib/internal/Ownership.d.ts +29 -0
- package/lib/internal/Ownership.d.ts.map +1 -0
- package/lib/internal/Ownership.js +50 -0
- package/lib/internal/Ownership.js.map +1 -0
- package/lib/internal/Program.d.ts +69 -0
- package/lib/internal/Program.d.ts.map +1 -0
- package/lib/internal/Program.js +178 -0
- package/lib/internal/Program.js.map +1 -0
- package/lib/internal/Ref.d.ts +14 -0
- package/lib/internal/Ref.d.ts.map +1 -0
- package/lib/internal/Ref.js +7 -0
- package/lib/internal/Ref.js.map +1 -0
- package/lib/internal/SandboxHandle.d.ts +17 -0
- package/lib/internal/SandboxHandle.d.ts.map +1 -0
- package/lib/internal/SandboxHandle.js +238 -0
- package/lib/internal/SandboxHandle.js.map +1 -0
- package/lib/internal/SavedSnapshot.d.ts +10 -0
- package/lib/internal/SavedSnapshot.d.ts.map +1 -0
- package/lib/internal/SavedSnapshot.js +22 -0
- package/lib/internal/SavedSnapshot.js.map +1 -0
- package/lib/internal/WebhookDelivery.d.ts +16 -0
- package/lib/internal/WebhookDelivery.d.ts.map +1 -0
- package/lib/internal/WebhookDelivery.js +109 -0
- package/lib/internal/WebhookDelivery.js.map +1 -0
- package/package.json +67 -3
- package/src/AiProxy.ts +171 -0
- package/src/ApiKey.ts +250 -0
- package/src/AuthProvider.ts +472 -0
- package/src/BoatBindings.ts +136 -0
- package/src/Bootstrap.ts +55 -0
- package/src/ConnectSandbox.ts +60 -0
- package/src/ConnectSandboxHttp.ts +59 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateSandbox.ts +143 -0
- package/src/CreateSandboxHttp.ts +115 -0
- package/src/CredentialProfile.ts +350 -0
- package/src/Credentials.ts +33 -0
- package/src/Environment.ts +416 -0
- package/src/Errors.ts +34 -0
- package/src/Providers.ts +71 -0
- package/src/Sandbox.ts +647 -0
- package/src/SandboxClient.ts +200 -0
- package/src/SandboxEvents.ts +125 -0
- package/src/SandboxEventsLive.ts +88 -0
- package/src/Snapshot.ts +264 -0
- package/src/Webhook.ts +163 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +18 -0
- package/src/internal/Declared.ts +87 -0
- package/src/internal/Org.ts +31 -0
- package/src/internal/Ownership.ts +75 -0
- package/src/internal/Program.ts +250 -0
- package/src/internal/Ref.ts +15 -0
- package/src/internal/SandboxHandle.ts +407 -0
- package/src/internal/SavedSnapshot.ts +34 -0
- package/src/internal/WebhookDelivery.ts +179 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as Binding from "alchemy/Binding";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
import type { Sandbox } from "./Sandbox.ts";
|
|
4
|
+
import type { ConnectedSandbox } from "./SandboxClient.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Work with one {@link Sandbox} declared in the stack, from a Worker,
|
|
8
|
+
* Function or any other host: run commands, read and write files, call its
|
|
9
|
+
* ports, stop, resume or snapshot it. The handle has the same operations
|
|
10
|
+
* as the sandboxes `CreateSandbox` returns, except `fork` and `delete`; its
|
|
11
|
+
* `sandboxId` is an Effect, read at runtime.
|
|
12
|
+
*
|
|
13
|
+
* A declared sandbox stops after its `autoStop`. Calls into it then fail
|
|
14
|
+
* with `SandboxNotRunning` until you `resume()` it.
|
|
15
|
+
*
|
|
16
|
+
* The host gets its own Boat API key, minted at deploy and revoked when the
|
|
17
|
+
* host is destroyed. The key reaches only this sandbox.
|
|
18
|
+
*
|
|
19
|
+
* ### Running commands in a dev box
|
|
20
|
+
* **Example:** Pull and test on request
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const devBox = yield* Boat.ConnectSandbox(DevBox);
|
|
23
|
+
* return {
|
|
24
|
+
* fetch: Effect.gen(function* () {
|
|
25
|
+
* yield* devBox.resume({ autoStop: "10 minutes" });
|
|
26
|
+
* const test = yield* devBox.exec("git pull && npm test", { cwd: "/home/user/repo" });
|
|
27
|
+
* return yield* HttpServerResponse.json(test);
|
|
28
|
+
* }),
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* ### Calling a program in the sandbox
|
|
33
|
+
* **Example:** Call port 3000
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const port = yield* devBox.getTcpPort(3000);
|
|
36
|
+
* const response = yield* port.fetch(HttpClientRequest.get("/health"));
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* **Example:** Proxy the Worker's request to port 3000
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const port = yield* devBox.getTcpPort(3000);
|
|
42
|
+
* return yield* port.fetch(yield* HttpServerRequest);
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* ### Stopping it early
|
|
46
|
+
* **Example:** Stop when the work is done
|
|
47
|
+
* ```typescript
|
|
48
|
+
* yield* devBox.stop();
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @binding
|
|
52
|
+
* @product Sandbox
|
|
53
|
+
*/
|
|
54
|
+
export interface ConnectSandbox extends Binding.Service<
|
|
55
|
+
ConnectSandbox,
|
|
56
|
+
"Boat.ConnectSandbox",
|
|
57
|
+
(sandbox: Sandbox) => Effect.Effect<ConnectedSandbox>
|
|
58
|
+
> {}
|
|
59
|
+
|
|
60
|
+
export const ConnectSandbox = Binding.Service<ConnectSandbox>("Boat.ConnectSandbox");
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type * as Output from "alchemy/Output";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import type { ApiKeyAction } from "./ApiKey.ts";
|
|
5
|
+
import { bindBoat } from "./BoatBindings.ts";
|
|
6
|
+
import { ConnectSandbox } from "./ConnectSandbox.ts";
|
|
7
|
+
import { makeConnectedSandbox } from "./internal/SandboxHandle.ts";
|
|
8
|
+
import type { Sandbox } from "./Sandbox.ts";
|
|
9
|
+
|
|
10
|
+
/** What a `ConnectSandbox` host may do, on its one sandbox. */
|
|
11
|
+
const CONNECT_SANDBOX_ACTIONS: ApiKeyAction[] = [
|
|
12
|
+
"sandbox.read",
|
|
13
|
+
"sandbox.update",
|
|
14
|
+
"sandbox.stop",
|
|
15
|
+
"sandbox.resume",
|
|
16
|
+
"exec",
|
|
17
|
+
"file.read",
|
|
18
|
+
"file.write",
|
|
19
|
+
"host",
|
|
20
|
+
"snapshot.read",
|
|
21
|
+
"snapshot.write",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** `DevBox` → `DEV_BOX`, for the env var that carries its key. */
|
|
25
|
+
const envSuffix = (logicalId: string) =>
|
|
26
|
+
logicalId
|
|
27
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
28
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
29
|
+
.toUpperCase();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* HTTP implementation of {@link ConnectSandbox}: Boat's REST API with a key
|
|
33
|
+
* that reaches only the bound sandbox. Works on any host.
|
|
34
|
+
*
|
|
35
|
+
* ### Provide the layer
|
|
36
|
+
* **Example:** On a Cloudflare Worker
|
|
37
|
+
* ```typescript
|
|
38
|
+
* Effect.gen(function* () {
|
|
39
|
+
* const devBox = yield* Boat.ConnectSandbox(DevBox);
|
|
40
|
+
* // ...
|
|
41
|
+
* }).pipe(Effect.provide(Boat.ConnectSandboxHttp))
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @layer
|
|
45
|
+
* @product Sandbox
|
|
46
|
+
* @provides Boat.ConnectSandbox
|
|
47
|
+
*/
|
|
48
|
+
export const ConnectSandboxHttp = Layer.succeed(
|
|
49
|
+
ConnectSandbox,
|
|
50
|
+
Effect.fn("Boat.ConnectSandbox")(function* (sandbox: Sandbox) {
|
|
51
|
+
const auth = yield* bindBoat(
|
|
52
|
+
`${sandbox.LogicalId}Key`,
|
|
53
|
+
`BOAT_${envSuffix(sandbox.LogicalId)}_KEY`,
|
|
54
|
+
{ actions: CONNECT_SANDBOX_ACTIONS, sandboxIds: [sandbox.sandboxId] },
|
|
55
|
+
);
|
|
56
|
+
const sandboxId = yield* (sandbox.sandboxId as Output.Output<string, never>).asEffect();
|
|
57
|
+
return makeConnectedSandbox(auth, sandboxId);
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type * as boat from "@distilled.cloud/boat-dev/boat";
|
|
2
|
+
import * as Binding from "alchemy/Binding";
|
|
3
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
4
|
+
import type * as Duration from "effect/Duration";
|
|
5
|
+
import type * as Effect from "effect/Effect";
|
|
6
|
+
import type { BoatError, BoatSandbox } from "./SandboxClient.ts";
|
|
7
|
+
import type { Snapshot } from "./Snapshot.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Boat's `createSandbox` parameters, with `autoStop` in place of
|
|
11
|
+
* `ttlSeconds`. `from` defaults to the bound snapshot; `labels` are merged
|
|
12
|
+
* under the host's ownership labels.
|
|
13
|
+
*/
|
|
14
|
+
export type CreateSandboxOptions = Omit<boat.CreateSandboxRequest, "count" | "ttlSeconds"> & {
|
|
15
|
+
/**
|
|
16
|
+
* How long the sandbox runs before Boat stops it, saving its disk;
|
|
17
|
+
* `false` never stops it.
|
|
18
|
+
* @default "1 hour"
|
|
19
|
+
*/
|
|
20
|
+
readonly autoStop?: Duration.Input | false;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Boat's `listSandboxes` filters, with `labels` as a record. */
|
|
24
|
+
export interface ListSandboxesOptions {
|
|
25
|
+
/** Only sandboxes with all of these labels. */
|
|
26
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
27
|
+
/** Only sandboxes in these states, e.g. `["ready", "idle", "running"]`. */
|
|
28
|
+
readonly state?: ReadonlyArray<boat.SandboxState>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Boat sandboxes: create, connect to, list and delete them. */
|
|
32
|
+
export interface SandboxesClient {
|
|
33
|
+
/** Creates a sandbox, from the bound snapshot unless `from` says otherwise. */
|
|
34
|
+
create(options?: CreateSandboxOptions): Effect.Effect<BoatSandbox, BoatError, RuntimeContext>;
|
|
35
|
+
/** A handle to an existing sandbox. */
|
|
36
|
+
connect(sandboxId: string): Effect.Effect<BoatSandbox, BoatError, RuntimeContext>;
|
|
37
|
+
/** A sandbox's details. */
|
|
38
|
+
get(sandboxId: string): Effect.Effect<boat.Sandbox, BoatError, RuntimeContext>;
|
|
39
|
+
/** Sandboxes, filtered by labels or state. */
|
|
40
|
+
list(
|
|
41
|
+
options?: ListSandboxesOptions,
|
|
42
|
+
): Effect.Effect<ReadonlyArray<boat.Sandbox>, BoatError, RuntimeContext>;
|
|
43
|
+
/**
|
|
44
|
+
* Deletes a sandbox this host created, with its disk. Deleting one that
|
|
45
|
+
* is gone succeeds.
|
|
46
|
+
*/
|
|
47
|
+
delete(sandboxId: string): Effect.Effect<void, BoatError, RuntimeContext>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create and manage Boat sandboxes from a Worker, Function or any other
|
|
52
|
+
* host.
|
|
53
|
+
*
|
|
54
|
+
* `Boat.CreateSandbox(snapshot)` creates sandboxes from a snapshot declared
|
|
55
|
+
* in the stack; `Boat.CreateSandbox()` creates them from scratch, or from
|
|
56
|
+
* the `from` each call passes. Both return the same client, whose calls take
|
|
57
|
+
* Boat's own parameters.
|
|
58
|
+
*
|
|
59
|
+
* Sandboxes created at run time are labelled with the stack, stage and
|
|
60
|
+
* host (`alchemy.stack`, `alchemy.stage`, `alchemy.id`) and are not deleted
|
|
61
|
+
* when the stack is destroyed. List them by those labels to clean them up:
|
|
62
|
+
* `sandboxes.list({ labels: { "alchemy.stack": "my-app", "alchemy.stage": "prod" } })`.
|
|
63
|
+
* Labels you pass to `create` merge underneath; they never replace the
|
|
64
|
+
* `alchemy.*` ones.
|
|
65
|
+
*
|
|
66
|
+
* The host gets its own Boat API key, minted at deploy and revoked when the
|
|
67
|
+
* host is destroyed. It may create sandboxes and work in them, and delete
|
|
68
|
+
* only the sandboxes it created; it can't touch the account's other
|
|
69
|
+
* sandboxes, environments, keys or billing.
|
|
70
|
+
*
|
|
71
|
+
* ### Creating sandboxes
|
|
72
|
+
* **Example:** One sandbox per request, deleted when the request ends
|
|
73
|
+
* ```typescript
|
|
74
|
+
* const sandboxes = yield* Boat.CreateSandbox();
|
|
75
|
+
* // in fetch:
|
|
76
|
+
* const sandbox = yield* Effect.acquireRelease(
|
|
77
|
+
* sandboxes.create({ type: "small", noEnv: true, autoStop: "10 minutes" }),
|
|
78
|
+
* (sandbox) => Effect.ignore(sandbox.delete()),
|
|
79
|
+
* );
|
|
80
|
+
* const result = yield* sandbox.exec("node --version");
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* **Example:** From a snapshot declared in the stack
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const sandboxes = yield* Boat.CreateSandbox(Golden);
|
|
86
|
+
* const sandbox = yield* sandboxes.create();
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* **Example:** A long-lived sandbox per task, found by label
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const [existing] = yield* sandboxes.list({ labels: { task } });
|
|
92
|
+
* const sandbox = existing
|
|
93
|
+
* ? yield* sandboxes.connect(existing.id)
|
|
94
|
+
* : yield* sandboxes.create({ labels: { task }, autoStop: "30 minutes" });
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* ### Running commands
|
|
98
|
+
* `exec` runs to completion and returns `{ exitCode, stdout, stderr }`; a
|
|
99
|
+
* non-zero exit is returned, not failed. A string runs in a shell, an
|
|
100
|
+
* array runs its arguments quoted.
|
|
101
|
+
*
|
|
102
|
+
* **Example:** A shell line, an argument vector, stdin and a timeout
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const test = yield* sandbox.exec("npm test", { cwd: "/home/user/app" });
|
|
105
|
+
* const version = yield* sandbox.exec(["node", "--version"]);
|
|
106
|
+
* const sorted = yield* sandbox.exec("sort", { stdin: "b\na\n" });
|
|
107
|
+
* const slow = yield* sandbox.exec("make", { timeout: "2 minutes" }); // Boat.ExecTimedOut
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* **Example:** Start a server in the background and stream its output
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const server = yield* sandbox.start("npm run dev", { cwd: "/home/user/app" });
|
|
113
|
+
* yield* server.output.pipe(
|
|
114
|
+
* Stream.take(5),
|
|
115
|
+
* Stream.runForEach((chunk) => Effect.log(new TextDecoder().decode(chunk.data))),
|
|
116
|
+
* );
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* ### Calling a port
|
|
120
|
+
* **Example:** Proxy the Worker's request to port 3000
|
|
121
|
+
* ```typescript
|
|
122
|
+
* const port = yield* sandbox.getTcpPort(3000);
|
|
123
|
+
* return yield* port.fetch(yield* HttpServerRequest);
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* ### Forking and snapshots
|
|
127
|
+
* **Example:** Save a sandbox and fork it
|
|
128
|
+
* ```typescript
|
|
129
|
+
* yield* sandbox.stop(); // stopping snapshots it
|
|
130
|
+
* const copy = yield* sandbox.fork({ autoStop: "10 minutes" });
|
|
131
|
+
* yield* sandbox.resume();
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* @binding
|
|
135
|
+
* @product Sandbox
|
|
136
|
+
*/
|
|
137
|
+
export interface CreateSandbox extends Binding.Service<
|
|
138
|
+
CreateSandbox,
|
|
139
|
+
"Boat.CreateSandbox",
|
|
140
|
+
(base?: Snapshot) => Effect.Effect<SandboxesClient>
|
|
141
|
+
> {}
|
|
142
|
+
|
|
143
|
+
export const CreateSandbox = Binding.Service<CreateSandbox>("Boat.CreateSandbox");
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as boat from "@distilled.cloud/boat-dev/boat";
|
|
2
|
+
import type * as Output from "alchemy/Output";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Stream from "effect/Stream";
|
|
6
|
+
import type { ApiKeyAction } from "./ApiKey.ts";
|
|
7
|
+
import { bindBoat, bindOwnerLabels } from "./BoatBindings.ts";
|
|
8
|
+
import { CreateSandbox, type CreateSandboxOptions, type SandboxesClient } from "./CreateSandbox.ts";
|
|
9
|
+
import { BoatUnexpectedResponse } from "./Errors.ts";
|
|
10
|
+
import { labelFilter } from "./internal/Ownership.ts";
|
|
11
|
+
import { makeSandboxHandle, ttlOf } from "./internal/SandboxHandle.ts";
|
|
12
|
+
import type { Snapshot } from "./Snapshot.ts";
|
|
13
|
+
|
|
14
|
+
/** What a `CreateSandbox` host may do: work in sandboxes, and delete only its own. */
|
|
15
|
+
const CREATE_SANDBOX_ACTIONS: ApiKeyAction[] = [
|
|
16
|
+
"sandbox.create",
|
|
17
|
+
"sandbox.read",
|
|
18
|
+
"sandbox.update",
|
|
19
|
+
"sandbox.stop",
|
|
20
|
+
"sandbox.resume",
|
|
21
|
+
"sandbox.fork",
|
|
22
|
+
"sandbox.delete-own",
|
|
23
|
+
"exec",
|
|
24
|
+
"file.read",
|
|
25
|
+
"file.write",
|
|
26
|
+
"host",
|
|
27
|
+
"snapshot.read",
|
|
28
|
+
"snapshot.write",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* HTTP implementation of {@link CreateSandbox}: Boat's REST API with the
|
|
33
|
+
* host's own key. Works on any host.
|
|
34
|
+
*
|
|
35
|
+
* ### Provide the layer
|
|
36
|
+
* **Example:** On a Cloudflare Worker
|
|
37
|
+
* ```typescript
|
|
38
|
+
* Effect.gen(function* () {
|
|
39
|
+
* const sandboxes = yield* Boat.CreateSandbox();
|
|
40
|
+
* // ...
|
|
41
|
+
* }).pipe(Effect.provide(Boat.CreateSandboxHttp))
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @layer
|
|
45
|
+
* @product Sandbox
|
|
46
|
+
* @provides Boat.CreateSandbox
|
|
47
|
+
*/
|
|
48
|
+
export const CreateSandboxHttp = Layer.succeed(
|
|
49
|
+
CreateSandbox,
|
|
50
|
+
Effect.fn("Boat.CreateSandbox")(function* (base?: Snapshot) {
|
|
51
|
+
const auth = yield* bindBoat("BoatSandboxesKey", "BOAT_SANDBOXES_KEY", {
|
|
52
|
+
actions: CREATE_SANDBOX_ACTIONS,
|
|
53
|
+
});
|
|
54
|
+
const owner = yield* bindOwnerLabels;
|
|
55
|
+
const baseName: Effect.Effect<string> | undefined =
|
|
56
|
+
base === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: yield* (base.name as Output.Output<string, never>).asEffect();
|
|
59
|
+
|
|
60
|
+
const handleOf = (sandbox: boat.Sandbox) => makeSandboxHandle(auth, sandbox, owner);
|
|
61
|
+
|
|
62
|
+
const client: SandboxesClient = {
|
|
63
|
+
create: Effect.fn("Boat.CreateSandbox.create")(function* (
|
|
64
|
+
createOptions: CreateSandboxOptions = {},
|
|
65
|
+
) {
|
|
66
|
+
const { autoStop, ...options } = createOptions;
|
|
67
|
+
const from = options.from ?? (baseName === undefined ? undefined : yield* baseName);
|
|
68
|
+
const created = yield* auth.authorize(
|
|
69
|
+
boat.createSandbox({
|
|
70
|
+
...options,
|
|
71
|
+
...(autoStop === undefined ? {} : { ttlSeconds: ttlOf(autoStop) }),
|
|
72
|
+
...(from === undefined ? {} : { from }),
|
|
73
|
+
labels: { ...options.labels, ...(yield* owner) },
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
return handleOf(created.sandbox);
|
|
77
|
+
}),
|
|
78
|
+
connect: Effect.fn("Boat.CreateSandbox.connect")(function* (sandboxId) {
|
|
79
|
+
const got = yield* auth.authorize(boat.getSandbox({ sandboxId }));
|
|
80
|
+
if (got.sandbox === undefined) {
|
|
81
|
+
return yield* new BoatUnexpectedResponse({
|
|
82
|
+
message: `Boat returned no sandbox for ${sandboxId}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return handleOf(got.sandbox);
|
|
86
|
+
}),
|
|
87
|
+
get: Effect.fn("Boat.CreateSandbox.get")(function* (sandboxId) {
|
|
88
|
+
const got = yield* auth.authorize(boat.getSandbox({ sandboxId }));
|
|
89
|
+
if (got.sandbox === undefined) {
|
|
90
|
+
return yield* new BoatUnexpectedResponse({
|
|
91
|
+
message: `Boat returned no sandbox for ${sandboxId}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return got.sandbox;
|
|
95
|
+
}),
|
|
96
|
+
list: Effect.fn("Boat.CreateSandbox.list")(function* (options) {
|
|
97
|
+
const all = yield* auth.authorize(
|
|
98
|
+
Stream.runCollect(
|
|
99
|
+
boat.listSandboxes.items({
|
|
100
|
+
...(options?.labels === undefined ? {} : { label: labelFilter(options.labels) }),
|
|
101
|
+
...(options?.state === undefined ? {} : { state: options.state.join(",") }),
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
return Array.from(all);
|
|
106
|
+
}),
|
|
107
|
+
delete: (sandboxId) =>
|
|
108
|
+
auth.authorize(boat.deleteSandbox({ sandboxId, xAsciiConfirmDelete: sandboxId })).pipe(
|
|
109
|
+
Effect.asVoid,
|
|
110
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
return client;
|
|
114
|
+
}),
|
|
115
|
+
);
|