@alchemy.run/e2b 0.0.0 → 2.0.0-beta.86
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 +56 -4
- package/lib/ApiKey.d.ts +53 -0
- package/lib/ApiKey.d.ts.map +1 -0
- package/lib/ApiKey.js +107 -0
- package/lib/ApiKey.js.map +1 -0
- package/lib/AuthProvider.d.ts +60 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +293 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Bootstrap.d.ts +7 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +25 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +67 -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 +22 -0
- package/lib/ConnectSandboxHttp.d.ts.map +1 -0
- package/lib/ConnectSandboxHttp.js +30 -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 +139 -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 +22 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +99 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CreateSecret.d.ts +57 -0
- package/lib/CreateSecret.d.ts.map +1 -0
- package/lib/CreateSecret.js +3 -0
- package/lib/CreateSecret.js.map +1 -0
- package/lib/CreateSecretHttp.d.ts +20 -0
- package/lib/CreateSecretHttp.d.ts.map +1 -0
- package/lib/CreateSecretHttp.js +64 -0
- package/lib/CreateSecretHttp.js.map +1 -0
- package/lib/CreateSnapshot.d.ts +53 -0
- package/lib/CreateSnapshot.d.ts.map +1 -0
- package/lib/CreateSnapshot.js +3 -0
- package/lib/CreateSnapshot.js.map +1 -0
- package/lib/CreateSnapshotHttp.d.ts +20 -0
- package/lib/CreateSnapshotHttp.d.ts.map +1 -0
- package/lib/CreateSnapshotHttp.js +41 -0
- package/lib/CreateSnapshotHttp.js.map +1 -0
- package/lib/CreateTemplate.d.ts +96 -0
- package/lib/CreateTemplate.d.ts.map +1 -0
- package/lib/CreateTemplate.js +3 -0
- package/lib/CreateTemplate.js.map +1 -0
- package/lib/CreateTemplateHttp.d.ts +20 -0
- package/lib/CreateTemplateHttp.d.ts.map +1 -0
- package/lib/CreateTemplateHttp.js +99 -0
- package/lib/CreateTemplateHttp.js.map +1 -0
- package/lib/CreateVolume.d.ts +58 -0
- package/lib/CreateVolume.d.ts.map +1 -0
- package/lib/CreateVolume.js +3 -0
- package/lib/CreateVolume.js.map +1 -0
- package/lib/CreateVolumeHttp.d.ts +20 -0
- package/lib/CreateVolumeHttp.d.ts.map +1 -0
- package/lib/CreateVolumeHttp.js +51 -0
- package/lib/CreateVolumeHttp.js.map +1 -0
- package/lib/Credentials.d.ts +18 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +55 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/E2BBindings.d.ts +35 -0
- package/lib/E2BBindings.d.ts.map +1 -0
- package/lib/E2BBindings.js +127 -0
- package/lib/E2BBindings.js.map +1 -0
- package/lib/Errors.d.ts +61 -0
- package/lib/Errors.d.ts.map +1 -0
- package/lib/Errors.js +23 -0
- package/lib/Errors.js.map +1 -0
- package/lib/InferEnv.d.ts +26 -0
- package/lib/InferEnv.d.ts.map +1 -0
- package/lib/InferEnv.js +1 -0
- package/lib/InferEnv.js.map +1 -0
- package/lib/MountVolume.d.ts +86 -0
- package/lib/MountVolume.d.ts.map +1 -0
- package/lib/MountVolume.js +3 -0
- package/lib/MountVolume.js.map +1 -0
- package/lib/MountVolumeLive.d.ts +23 -0
- package/lib/MountVolumeLive.d.ts.map +1 -0
- package/lib/MountVolumeLive.js +41 -0
- package/lib/MountVolumeLive.js.map +1 -0
- package/lib/Providers.d.ts +14 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +23 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadTeam.d.ts +41 -0
- package/lib/ReadTeam.d.ts.map +1 -0
- package/lib/ReadTeam.js +3 -0
- package/lib/ReadTeam.js.map +1 -0
- package/lib/ReadTeamHttp.d.ts +20 -0
- package/lib/ReadTeamHttp.d.ts.map +1 -0
- package/lib/ReadTeamHttp.js +40 -0
- package/lib/ReadTeamHttp.js.map +1 -0
- package/lib/ReadVolume.d.ts +25 -0
- package/lib/ReadVolume.d.ts.map +1 -0
- package/lib/ReadVolume.js +3 -0
- package/lib/ReadVolume.js.map +1 -0
- package/lib/ReadVolumeHttp.d.ts +20 -0
- package/lib/ReadVolumeHttp.d.ts.map +1 -0
- package/lib/ReadVolumeHttp.js +27 -0
- package/lib/ReadVolumeHttp.js.map +1 -0
- package/lib/ReadWebhook.d.ts +32 -0
- package/lib/ReadWebhook.d.ts.map +1 -0
- package/lib/ReadWebhook.js +3 -0
- package/lib/ReadWebhook.js.map +1 -0
- package/lib/ReadWebhookHttp.d.ts +20 -0
- package/lib/ReadWebhookHttp.d.ts.map +1 -0
- package/lib/ReadWebhookHttp.js +31 -0
- package/lib/ReadWebhookHttp.js.map +1 -0
- package/lib/ReadWriteVolume.d.ts +26 -0
- package/lib/ReadWriteVolume.d.ts.map +1 -0
- package/lib/ReadWriteVolume.js +3 -0
- package/lib/ReadWriteVolume.js.map +1 -0
- package/lib/ReadWriteVolumeHttp.d.ts +20 -0
- package/lib/ReadWriteVolumeHttp.d.ts.map +1 -0
- package/lib/ReadWriteVolumeHttp.js +27 -0
- package/lib/ReadWriteVolumeHttp.js.map +1 -0
- package/lib/Sandbox.d.ts +295 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +351 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +226 -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 +87 -0
- package/lib/SandboxEvents.d.ts.map +1 -0
- package/lib/SandboxEvents.js +39 -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 +80 -0
- package/lib/SandboxEventsLive.js.map +1 -0
- package/lib/Secret.d.ts +105 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +161 -0
- package/lib/Secret.js.map +1 -0
- package/lib/Snapshot.d.ts +118 -0
- package/lib/Snapshot.d.ts.map +1 -0
- package/lib/Snapshot.js +158 -0
- package/lib/Snapshot.js.map +1 -0
- package/lib/Template.d.ts +313 -0
- package/lib/Template.d.ts.map +1 -0
- package/lib/Template.js +421 -0
- package/lib/Template.js.map +1 -0
- package/lib/Volume.d.ts +88 -0
- package/lib/Volume.d.ts.map +1 -0
- package/lib/Volume.js +136 -0
- package/lib/Volume.js.map +1 -0
- package/lib/VolumeFiles.d.ts +39 -0
- package/lib/VolumeFiles.d.ts.map +1 -0
- package/lib/VolumeFiles.js +1 -0
- package/lib/VolumeFiles.js.map +1 -0
- package/lib/Webhook.d.ts +83 -0
- package/lib/Webhook.d.ts.map +1 -0
- package/lib/Webhook.js +138 -0
- package/lib/Webhook.js.map +1 -0
- package/lib/WriteSecret.d.ts +34 -0
- package/lib/WriteSecret.d.ts.map +1 -0
- package/lib/WriteSecret.js +3 -0
- package/lib/WriteSecret.js.map +1 -0
- package/lib/WriteSecretHttp.d.ts +20 -0
- package/lib/WriteSecretHttp.d.ts.map +1 -0
- package/lib/WriteSecretHttp.js +37 -0
- package/lib/WriteSecretHttp.js.map +1 -0
- package/lib/WriteVolume.d.ts +25 -0
- package/lib/WriteVolume.d.ts.map +1 -0
- package/lib/WriteVolume.js +3 -0
- package/lib/WriteVolume.js.map +1 -0
- package/lib/WriteVolumeHttp.d.ts +20 -0
- package/lib/WriteVolumeHttp.d.ts.map +1 -0
- package/lib/WriteVolumeHttp.js +27 -0
- package/lib/WriteVolumeHttp.js.map +1 -0
- package/lib/index.d.ts +43 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +43 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/Declared.d.ts +29 -0
- package/lib/internal/Declared.d.ts.map +1 -0
- package/lib/internal/Declared.js +31 -0
- package/lib/internal/Declared.js.map +1 -0
- package/lib/internal/Login.d.ts +45 -0
- package/lib/internal/Login.d.ts.map +1 -0
- package/lib/internal/Login.js +157 -0
- package/lib/internal/Login.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 +49 -0
- package/lib/internal/Ownership.js.map +1 -0
- package/lib/internal/Program.d.ts +31 -0
- package/lib/internal/Program.d.ts.map +1 -0
- package/lib/internal/Program.js +60 -0
- package/lib/internal/Program.js.map +1 -0
- package/lib/internal/SandboxHandle.d.ts +25 -0
- package/lib/internal/SandboxHandle.d.ts.map +1 -0
- package/lib/internal/SandboxHandle.js +316 -0
- package/lib/internal/SandboxHandle.js.map +1 -0
- package/lib/internal/SnapshotBuild.d.ts +19 -0
- package/lib/internal/SnapshotBuild.d.ts.map +1 -0
- package/lib/internal/SnapshotBuild.js +49 -0
- package/lib/internal/SnapshotBuild.js.map +1 -0
- package/lib/internal/Tar.d.ts +10 -0
- package/lib/internal/Tar.d.ts.map +1 -0
- package/lib/internal/Tar.js +61 -0
- package/lib/internal/Tar.js.map +1 -0
- package/lib/internal/TemplateBuild.d.ts +142 -0
- package/lib/internal/TemplateBuild.d.ts.map +1 -0
- package/lib/internal/TemplateBuild.js +220 -0
- package/lib/internal/TemplateBuild.js.map +1 -0
- package/lib/internal/TemplatePlan.d.ts +57 -0
- package/lib/internal/TemplatePlan.d.ts.map +1 -0
- package/lib/internal/TemplatePlan.js +185 -0
- package/lib/internal/TemplatePlan.js.map +1 -0
- package/lib/internal/VolumeHttp.d.ts +5 -0
- package/lib/internal/VolumeHttp.d.ts.map +1 -0
- package/lib/internal/VolumeHttp.js +46 -0
- package/lib/internal/VolumeHttp.js.map +1 -0
- package/package.json +62 -3
- package/src/ApiKey.ts +148 -0
- package/src/AuthProvider.ts +437 -0
- package/src/Bootstrap.ts +51 -0
- package/src/ConnectSandbox.ts +71 -0
- package/src/ConnectSandboxHttp.ts +38 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateSandbox.ts +157 -0
- package/src/CreateSandboxHttp.ts +131 -0
- package/src/CreateSecret.ts +65 -0
- package/src/CreateSecretHttp.ts +94 -0
- package/src/CreateSnapshot.ts +64 -0
- package/src/CreateSnapshotHttp.ts +55 -0
- package/src/CreateTemplate.ts +135 -0
- package/src/CreateTemplateHttp.ts +152 -0
- package/src/CreateVolume.ts +65 -0
- package/src/CreateVolumeHttp.ts +80 -0
- package/src/Credentials.ts +81 -0
- package/src/E2BBindings.ts +160 -0
- package/src/Errors.ts +47 -0
- package/src/InferEnv.ts +27 -0
- package/src/MountVolume.ts +92 -0
- package/src/MountVolumeLive.ts +49 -0
- package/src/Providers.ts +48 -0
- package/src/ReadTeam.ts +55 -0
- package/src/ReadTeamHttp.ts +44 -0
- package/src/ReadVolume.ts +29 -0
- package/src/ReadVolumeHttp.ts +32 -0
- package/src/ReadWebhook.ts +41 -0
- package/src/ReadWebhookHttp.ts +42 -0
- package/src/ReadWriteVolume.ts +30 -0
- package/src/ReadWriteVolumeHttp.ts +32 -0
- package/src/Sandbox.ts +581 -0
- package/src/SandboxClient.ts +268 -0
- package/src/SandboxEvents.ts +101 -0
- package/src/SandboxEventsLive.ts +118 -0
- package/src/Secret.ts +229 -0
- package/src/Snapshot.ts +234 -0
- package/src/Template.ts +636 -0
- package/src/Volume.ts +173 -0
- package/src/VolumeFiles.ts +53 -0
- package/src/Webhook.ts +215 -0
- package/src/WriteSecret.ts +39 -0
- package/src/WriteSecretHttp.ts +43 -0
- package/src/WriteVolume.ts +29 -0
- package/src/WriteVolumeHttp.ts +32 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +42 -0
- package/src/internal/Declared.ts +80 -0
- package/src/internal/Login.ts +210 -0
- package/src/internal/Ownership.ts +67 -0
- package/src/internal/Program.ts +75 -0
- package/src/internal/SandboxHandle.ts +534 -0
- package/src/internal/SnapshotBuild.ts +74 -0
- package/src/internal/Tar.ts +74 -0
- package/src/internal/TemplateBuild.ts +383 -0
- package/src/internal/TemplatePlan.ts +230 -0
- package/src/internal/VolumeHttp.ts +88 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type * as tags from "@distilled.cloud/e2b/tags";
|
|
2
|
+
import type * as templates from "@distilled.cloud/e2b/templates";
|
|
3
|
+
import type * as unstable from "@distilled.cloud/e2b/unstable";
|
|
4
|
+
import * as Binding from "alchemy/Binding";
|
|
5
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
6
|
+
import type * as Effect from "effect/Effect";
|
|
7
|
+
import type * as PlatformError from "effect/PlatformError";
|
|
8
|
+
import type { TemplateBuildFailed } from "./Errors.ts";
|
|
9
|
+
import type { TemplateBuildSpec } from "./internal/TemplateBuild.ts";
|
|
10
|
+
import type { E2BError } from "./SandboxClient.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What a runtime build takes: E2B's own build inputs (a base `image` or
|
|
14
|
+
* `fromTemplate`, then `env`, `files` and `run` steps), with no Dockerfile.
|
|
15
|
+
* Files come from memory; there is no build-context directory where a
|
|
16
|
+
* Worker runs.
|
|
17
|
+
*/
|
|
18
|
+
export type RuntimeTemplateSpec = TemplateBuildSpec;
|
|
19
|
+
|
|
20
|
+
/** A built template. */
|
|
21
|
+
export interface TemplateInfo {
|
|
22
|
+
readonly templateId: string;
|
|
23
|
+
readonly buildId: string;
|
|
24
|
+
/** Fully qualified names, e.g. `my-team/my-template:default`. */
|
|
25
|
+
readonly names: ReadonlyArray<string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Errors a runtime build can fail with. */
|
|
29
|
+
export type TemplateBuildError = E2BError | TemplateBuildFailed | PlatformError.PlatformError;
|
|
30
|
+
|
|
31
|
+
/** E2B templates: build, look up, tag and delete them at runtime. */
|
|
32
|
+
export interface TemplatesClient {
|
|
33
|
+
/** Builds a template and waits until it is ready. */
|
|
34
|
+
build(spec: RuntimeTemplateSpec): Effect.Effect<TemplateInfo, TemplateBuildError, RuntimeContext>;
|
|
35
|
+
/**
|
|
36
|
+
* Builds a template unless a ready build of these exact inputs exists.
|
|
37
|
+
* Each build is tagged with its content hash, so a repeat call is one
|
|
38
|
+
* lookup.
|
|
39
|
+
*/
|
|
40
|
+
getOrBuild(
|
|
41
|
+
spec: RuntimeTemplateSpec,
|
|
42
|
+
): Effect.Effect<TemplateInfo, TemplateBuildError, RuntimeContext>;
|
|
43
|
+
/** A template by id or name (alias), or `undefined`. */
|
|
44
|
+
get(
|
|
45
|
+
nameOrId: string,
|
|
46
|
+
): Effect.Effect<templates.GetTemplateResponse | undefined, E2BError, RuntimeContext>;
|
|
47
|
+
/** The team's templates. */
|
|
48
|
+
list(
|
|
49
|
+
options?: Omit<templates.ListTemplatesRequest, "teamID" | "nextToken">,
|
|
50
|
+
): Effect.Effect<ReadonlyArray<templates.Template>, E2BError, RuntimeContext>;
|
|
51
|
+
/** E2B's public base templates. */
|
|
52
|
+
listDefaults(): Effect.Effect<templates.ListDefaultTemplatesResponse, E2BError, RuntimeContext>;
|
|
53
|
+
/** Templates matching a search. */
|
|
54
|
+
search(
|
|
55
|
+
options: unstable.SearchTemplatesRequest,
|
|
56
|
+
): Effect.Effect<unstable.TemplateSearchResponse, E2BError, RuntimeContext>;
|
|
57
|
+
/** Whether other teams can create sandboxes from the template. */
|
|
58
|
+
setPublic(templateId: string, isPublic: boolean): Effect.Effect<void, E2BError, RuntimeContext>;
|
|
59
|
+
/** A build's status, with its newest log lines. */
|
|
60
|
+
buildStatus(
|
|
61
|
+
templateId: string,
|
|
62
|
+
buildId: string,
|
|
63
|
+
options?: Omit<templates.GetTemplateBuildStatusRequest, "templateID" | "buildID">,
|
|
64
|
+
): Effect.Effect<templates.TemplateBuildInfo, E2BError, RuntimeContext>;
|
|
65
|
+
/** A build's logs. */
|
|
66
|
+
buildLogs(
|
|
67
|
+
templateId: string,
|
|
68
|
+
buildId: string,
|
|
69
|
+
options?: Omit<templates.ListTemplateBuildLogsRequest, "templateID" | "buildID">,
|
|
70
|
+
): Effect.Effect<templates.TemplateBuildLogsResponse, E2BError, RuntimeContext>;
|
|
71
|
+
/** Deletes a template. Deleting one that is gone succeeds. */
|
|
72
|
+
delete(templateId: string): Effect.Effect<void, E2BError, RuntimeContext>;
|
|
73
|
+
/** Tags on builds: `name:tag` names a build sandboxes are created from. */
|
|
74
|
+
readonly tags: {
|
|
75
|
+
/** Assigns tags to the build `target` (`name:tag` or `name`) points at. */
|
|
76
|
+
assign(
|
|
77
|
+
target: string,
|
|
78
|
+
tags: ReadonlyArray<string>,
|
|
79
|
+
): Effect.Effect<tags.AssignedTemplateTags, E2BError, RuntimeContext>;
|
|
80
|
+
/** Removes tags from a template by name. */
|
|
81
|
+
remove(
|
|
82
|
+
name: string,
|
|
83
|
+
tags: ReadonlyArray<string>,
|
|
84
|
+
): Effect.Effect<void, E2BError, RuntimeContext>;
|
|
85
|
+
list(
|
|
86
|
+
templateId: string,
|
|
87
|
+
): Effect.Effect<tags.ListTemplateTagsResponse, E2BError, RuntimeContext>;
|
|
88
|
+
exists(templateId: string, tag: string): Effect.Effect<boolean, E2BError, RuntimeContext>;
|
|
89
|
+
count(templateId: string): Effect.Effect<number, E2BError, RuntimeContext>;
|
|
90
|
+
groups(
|
|
91
|
+
templateId: string,
|
|
92
|
+
options?: Omit<tags.ListTemplateTagGroupsRequest, "templateID">,
|
|
93
|
+
): Effect.Effect<tags.TemplateTagGroupsResponse, E2BError, RuntimeContext>;
|
|
94
|
+
assignments(
|
|
95
|
+
templateId: string,
|
|
96
|
+
tag: string,
|
|
97
|
+
options?: Omit<tags.ListTemplateTagAssignmentsRequest, "templateID" | "tag">,
|
|
98
|
+
): Effect.Effect<tags.TemplateTagAssignmentsResponse, E2BError, RuntimeContext>;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build E2B templates at runtime from E2B's own build inputs: a template
|
|
104
|
+
* per repository, per customer image, per agent configuration.
|
|
105
|
+
* E2B builds remotely, so this works from a Worker.
|
|
106
|
+
*
|
|
107
|
+
* Templates built at runtime carry no metadata, so they are not deleted
|
|
108
|
+
* when the host is destroyed. Delete them with `delete` when their owner
|
|
109
|
+
* goes away.
|
|
110
|
+
*
|
|
111
|
+
* ### Building at runtime
|
|
112
|
+
* **Example:** A template per repository, built once
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const templates = yield* E2B.CreateTemplate();
|
|
115
|
+
* const sandboxes = yield* E2B.CreateSandbox();
|
|
116
|
+
* // in fetch:
|
|
117
|
+
* const template = yield* templates.getOrBuild({
|
|
118
|
+
* name: `repo-${slug}`,
|
|
119
|
+
* image: "node:22-bookworm",
|
|
120
|
+
* files: [{ path: "/home/user/app/package.json", contents: packageJson }],
|
|
121
|
+
* run: ["cd /home/user/app && npm install"],
|
|
122
|
+
* });
|
|
123
|
+
* const sandbox = yield* sandboxes.create({ templateID: template.templateId });
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @binding
|
|
127
|
+
* @product Template
|
|
128
|
+
*/
|
|
129
|
+
export interface CreateTemplate extends Binding.Service<
|
|
130
|
+
CreateTemplate,
|
|
131
|
+
"E2B.CreateTemplate",
|
|
132
|
+
() => Effect.Effect<TemplatesClient>
|
|
133
|
+
> {}
|
|
134
|
+
|
|
135
|
+
export const CreateTemplate = Binding.Service<CreateTemplate>("E2B.CreateTemplate");
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as tags from "@distilled.cloud/e2b/tags";
|
|
2
|
+
import * as templates from "@distilled.cloud/e2b/templates";
|
|
3
|
+
import * as unstable from "@distilled.cloud/e2b/unstable";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import {
|
|
8
|
+
CreateTemplate,
|
|
9
|
+
type RuntimeTemplateSpec,
|
|
10
|
+
type TemplateInfo,
|
|
11
|
+
type TemplatesClient,
|
|
12
|
+
} from "./CreateTemplate.ts";
|
|
13
|
+
import { bindE2B } from "./E2BBindings.ts";
|
|
14
|
+
import { planTemplateBuild, runTemplateBuild } from "./internal/TemplateBuild.ts";
|
|
15
|
+
|
|
16
|
+
/** The tag a build of these inputs carries. */
|
|
17
|
+
const hashTag = (hash: string) => `alchemy-${hash.slice(0, 16)}`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* HTTP implementation of {@link CreateTemplate}. Works on any host.
|
|
21
|
+
*
|
|
22
|
+
* ### Provide the layer
|
|
23
|
+
* **Example:** On a Worker
|
|
24
|
+
* ```typescript
|
|
25
|
+
* Effect.gen(function* () {
|
|
26
|
+
* const templates = yield* E2B.CreateTemplate();
|
|
27
|
+
* // ...
|
|
28
|
+
* }).pipe(Effect.provide(E2B.CreateTemplateHttp))
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @layer
|
|
32
|
+
* @product Template
|
|
33
|
+
* @provides E2B.CreateTemplate
|
|
34
|
+
*/
|
|
35
|
+
export const CreateTemplateHttp = Layer.succeed(
|
|
36
|
+
CreateTemplate,
|
|
37
|
+
Effect.fn("E2B.CreateTemplate")(function* () {
|
|
38
|
+
const auth = yield* bindE2B;
|
|
39
|
+
|
|
40
|
+
const get = (nameOrId: string) =>
|
|
41
|
+
auth
|
|
42
|
+
.authorize(
|
|
43
|
+
templates
|
|
44
|
+
.getTemplate({ templateID: nameOrId })
|
|
45
|
+
.pipe(
|
|
46
|
+
Effect.catchTag("NotFound", () =>
|
|
47
|
+
templates
|
|
48
|
+
.getTemplateByAlias({ alias: nameOrId })
|
|
49
|
+
.pipe(
|
|
50
|
+
Effect.flatMap((alias) =>
|
|
51
|
+
templates.getTemplate({ templateID: alias.templateID }),
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
.pipe(
|
|
58
|
+
Effect.map((template): templates.GetTemplateResponse | undefined => template),
|
|
59
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const build = Effect.fn("E2B.CreateTemplate.build")(function* (spec: RuntimeTemplateSpec) {
|
|
63
|
+
const plan = yield* planTemplateBuild(spec);
|
|
64
|
+
const built = yield* auth.authorize(
|
|
65
|
+
// E2B builds the `default` tag only when no tags are given, so keep it
|
|
66
|
+
// next to the hash tag unless the caller chose its own.
|
|
67
|
+
runTemplateBuild(
|
|
68
|
+
{ ...spec, tags: [...(spec.tags ?? ["default"]), hashTag(plan.hash)] },
|
|
69
|
+
plan,
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
return {
|
|
73
|
+
templateId: built.templateId,
|
|
74
|
+
buildId: built.buildId,
|
|
75
|
+
names: built.names,
|
|
76
|
+
} satisfies TemplateInfo;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const client: TemplatesClient = {
|
|
80
|
+
build,
|
|
81
|
+
getOrBuild: Effect.fn("E2B.CreateTemplate.getOrBuild")(function* (spec) {
|
|
82
|
+
const plan = yield* planTemplateBuild(spec);
|
|
83
|
+
const existing = yield* get(spec.name);
|
|
84
|
+
if (existing !== undefined) {
|
|
85
|
+
const tagged = yield* auth.authorize(
|
|
86
|
+
tags.getTemplateTagExists({ templateID: existing.templateID, tag: hashTag(plan.hash) }),
|
|
87
|
+
);
|
|
88
|
+
const ready =
|
|
89
|
+
existing.builds.find((b) => b.buildID === existing.buildID)?.status === "ready";
|
|
90
|
+
if (tagged.exists && ready) {
|
|
91
|
+
return {
|
|
92
|
+
templateId: existing.templateID,
|
|
93
|
+
buildId: existing.buildID,
|
|
94
|
+
names: existing.names,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return yield* build(spec);
|
|
99
|
+
}),
|
|
100
|
+
get,
|
|
101
|
+
list: (options) =>
|
|
102
|
+
auth
|
|
103
|
+
.authorize(Stream.runCollect(templates.listTemplates.items({ ...options })))
|
|
104
|
+
.pipe(Effect.map((all) => Array.from(all))),
|
|
105
|
+
listDefaults: () => auth.authorize(templates.listDefaultTemplates({})),
|
|
106
|
+
search: (options) => auth.authorize(unstable.searchTemplates(options)),
|
|
107
|
+
setPublic: (templateId, isPublic) =>
|
|
108
|
+
auth
|
|
109
|
+
.authorize(templates.updateTemplate({ templateID: templateId, public: isPublic }))
|
|
110
|
+
.pipe(Effect.asVoid),
|
|
111
|
+
buildStatus: (templateId, buildId, options) =>
|
|
112
|
+
auth.authorize(
|
|
113
|
+
templates.getTemplateBuildStatus({
|
|
114
|
+
...options,
|
|
115
|
+
templateID: templateId,
|
|
116
|
+
buildID: buildId,
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
buildLogs: (templateId, buildId, options) =>
|
|
120
|
+
auth.authorize(
|
|
121
|
+
templates.listTemplateBuildLogs({ ...options, templateID: templateId, buildID: buildId }),
|
|
122
|
+
),
|
|
123
|
+
delete: (templateId) =>
|
|
124
|
+
auth.authorize(templates.deleteTemplate({ templateID: templateId })).pipe(
|
|
125
|
+
Effect.asVoid,
|
|
126
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
127
|
+
),
|
|
128
|
+
tags: {
|
|
129
|
+
assign: (target, assigned) =>
|
|
130
|
+
auth.authorize(tags.createTemplateTag({ target, tags: [...assigned] })),
|
|
131
|
+
remove: (name, removed) =>
|
|
132
|
+
auth.authorize(tags.deleteTemplateTags({ name, tags: [...removed] })).pipe(Effect.asVoid),
|
|
133
|
+
list: (templateId) => auth.authorize(tags.listTemplateTags({ templateID: templateId })),
|
|
134
|
+
exists: (templateId, tag) =>
|
|
135
|
+
auth
|
|
136
|
+
.authorize(tags.getTemplateTagExists({ templateID: templateId, tag }))
|
|
137
|
+
.pipe(Effect.map((res) => res.exists)),
|
|
138
|
+
count: (templateId) =>
|
|
139
|
+
auth
|
|
140
|
+
.authorize(tags.getTemplateTagCount({ templateID: templateId }))
|
|
141
|
+
.pipe(Effect.map((res) => res.total)),
|
|
142
|
+
groups: (templateId, options) =>
|
|
143
|
+
auth.authorize(tags.listTemplateTagGroups({ ...options, templateID: templateId })),
|
|
144
|
+
assignments: (templateId, tag, options) =>
|
|
145
|
+
auth.authorize(
|
|
146
|
+
tags.listTemplateTagAssignments({ ...options, templateID: templateId, tag }),
|
|
147
|
+
),
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
return client;
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as Binding from "alchemy/Binding";
|
|
2
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
3
|
+
import type * as Effect from "effect/Effect";
|
|
4
|
+
import type { E2BError } from "./SandboxClient.ts";
|
|
5
|
+
import type { ReadWriteVolumeClient } from "./VolumeFiles.ts";
|
|
6
|
+
|
|
7
|
+
/** A volume created or found at runtime, with its files. */
|
|
8
|
+
export interface VolumeHandle {
|
|
9
|
+
/** E2B's volume id. */
|
|
10
|
+
readonly volumeId: string;
|
|
11
|
+
/** The volume's name. Mount it with `mounts: [{ volume, path }]`. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** The volume's files, readable and writable without a sandbox. */
|
|
14
|
+
readonly files: ReadWriteVolumeClient;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** E2B volumes by name: create, look up, list and delete them. */
|
|
18
|
+
export interface VolumesClient {
|
|
19
|
+
/** Creates a volume. Fails with `Conflict` when the name is taken. */
|
|
20
|
+
create(name: string): Effect.Effect<VolumeHandle, E2BError, RuntimeContext>;
|
|
21
|
+
/** The volume with this name or id, or `undefined`. */
|
|
22
|
+
get(nameOrId: string): Effect.Effect<VolumeHandle | undefined, E2BError, RuntimeContext>;
|
|
23
|
+
/** The volume with this name, created if missing. Safe to race. */
|
|
24
|
+
getOrCreate(name: string): Effect.Effect<VolumeHandle, E2BError, RuntimeContext>;
|
|
25
|
+
/** Every volume in the team. */
|
|
26
|
+
list(): Effect.Effect<
|
|
27
|
+
ReadonlyArray<{ readonly volumeId: string; readonly name: string }>,
|
|
28
|
+
E2BError,
|
|
29
|
+
RuntimeContext
|
|
30
|
+
>;
|
|
31
|
+
/** Deletes a volume and its files. Deleting one that is gone succeeds. */
|
|
32
|
+
delete(volumeId: string): Effect.Effect<void, E2BError, RuntimeContext>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create and manage E2B volumes at runtime, by name: a home directory per
|
|
37
|
+
* user, a workspace per session. Mount one into a sandbox by its `name`, and
|
|
38
|
+
* read or write its files with `files` without a sandbox.
|
|
39
|
+
*
|
|
40
|
+
* E2B volumes carry no metadata, so the volumes a host creates at runtime
|
|
41
|
+
* are not deleted when the host is destroyed. Delete them with `delete` when
|
|
42
|
+
* their owner goes away.
|
|
43
|
+
*
|
|
44
|
+
* ### Per-user volumes
|
|
45
|
+
* **Example:** A home volume per user, mounted into their sandbox
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const volumes = yield* E2B.CreateVolume();
|
|
48
|
+
* const sandboxes = yield* E2B.CreateSandbox(Agent);
|
|
49
|
+
* // in fetch:
|
|
50
|
+
* const home = yield* volumes.getOrCreate(`user-${user}`);
|
|
51
|
+
* const sandbox = yield* sandboxes.create({
|
|
52
|
+
* mounts: [{ volume: home, path: "/home/user/work" }],
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @binding
|
|
57
|
+
* @product Volume
|
|
58
|
+
*/
|
|
59
|
+
export interface CreateVolume extends Binding.Service<
|
|
60
|
+
CreateVolume,
|
|
61
|
+
"E2B.CreateVolume",
|
|
62
|
+
() => Effect.Effect<VolumesClient>
|
|
63
|
+
> {}
|
|
64
|
+
|
|
65
|
+
export const CreateVolume = Binding.Service<CreateVolume>("E2B.CreateVolume");
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as volumes from "@distilled.cloud/e2b/volumes";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import { CreateVolume, type VolumeHandle, type VolumesClient } from "./CreateVolume.ts";
|
|
5
|
+
import { bindE2B, type E2BAuth } from "./E2BBindings.ts";
|
|
6
|
+
import { makeVolumeFiles } from "./internal/VolumeHttp.ts";
|
|
7
|
+
|
|
8
|
+
const handleOf = (auth: E2BAuth, volume: { volumeID: string; name: string }): VolumeHandle => ({
|
|
9
|
+
volumeId: volume.volumeID,
|
|
10
|
+
name: volume.name,
|
|
11
|
+
files: makeVolumeFiles(auth, Effect.succeed(volume.volumeID)),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* HTTP implementation of {@link CreateVolume}. Works on any host.
|
|
16
|
+
*
|
|
17
|
+
* ### Provide the layer
|
|
18
|
+
* **Example:** On a Worker
|
|
19
|
+
* ```typescript
|
|
20
|
+
* Effect.gen(function* () {
|
|
21
|
+
* const volumes = yield* E2B.CreateVolume();
|
|
22
|
+
* // ...
|
|
23
|
+
* }).pipe(Effect.provide(E2B.CreateVolumeHttp))
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @layer
|
|
27
|
+
* @product Volume
|
|
28
|
+
* @provides E2B.CreateVolume
|
|
29
|
+
*/
|
|
30
|
+
export const CreateVolumeHttp = Layer.succeed(
|
|
31
|
+
CreateVolume,
|
|
32
|
+
Effect.fn("E2B.CreateVolume")(function* () {
|
|
33
|
+
const auth = yield* bindE2B;
|
|
34
|
+
const findByName = (name: string) =>
|
|
35
|
+
auth
|
|
36
|
+
.authorize(volumes.listVolumes({}))
|
|
37
|
+
.pipe(Effect.map((all) => all.find((volume) => volume.name === name)));
|
|
38
|
+
const get = (nameOrId: string) =>
|
|
39
|
+
auth.authorize(volumes.getVolume({ volumeID: nameOrId })).pipe(
|
|
40
|
+
Effect.map((volume): { volumeID: string; name: string } | undefined => volume),
|
|
41
|
+
Effect.catchTag("NotFound", () => findByName(nameOrId)),
|
|
42
|
+
Effect.map((volume) => (volume === undefined ? undefined : handleOf(auth, volume))),
|
|
43
|
+
);
|
|
44
|
+
const client: VolumesClient = {
|
|
45
|
+
create: (name) =>
|
|
46
|
+
auth
|
|
47
|
+
.authorize(volumes.createVolume({ name }))
|
|
48
|
+
.pipe(Effect.map((volume) => handleOf(auth, volume))),
|
|
49
|
+
get,
|
|
50
|
+
getOrCreate: (name) =>
|
|
51
|
+
Effect.flatMap(get(name), (existing) =>
|
|
52
|
+
existing !== undefined
|
|
53
|
+
? Effect.succeed(existing)
|
|
54
|
+
: auth.authorize(volumes.createVolume({ name })).pipe(
|
|
55
|
+
Effect.map((volume) => handleOf(auth, volume)),
|
|
56
|
+
// A concurrent create won the name: use theirs.
|
|
57
|
+
Effect.catchTag("Conflict", (error) =>
|
|
58
|
+
Effect.flatMap(get(name), (raced) =>
|
|
59
|
+
raced === undefined ? Effect.fail(error) : Effect.succeed(raced),
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
list: () =>
|
|
65
|
+
auth
|
|
66
|
+
.authorize(volumes.listVolumes({}))
|
|
67
|
+
.pipe(
|
|
68
|
+
Effect.map((all) =>
|
|
69
|
+
all.map((volume) => ({ volumeId: volume.volumeID, name: volume.name })),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
delete: (volumeId) =>
|
|
73
|
+
auth.authorize(volumes.deleteVolume({ volumeID: volumeId })).pipe(
|
|
74
|
+
Effect.asVoid,
|
|
75
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
76
|
+
),
|
|
77
|
+
};
|
|
78
|
+
return client;
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/core/errors";
|
|
2
|
+
import * as auth from "@distilled.cloud/e2b/auth";
|
|
3
|
+
import {
|
|
4
|
+
apiBaseUrlFor,
|
|
5
|
+
Credentials,
|
|
6
|
+
credentials,
|
|
7
|
+
type Config,
|
|
8
|
+
} from "@distilled.cloud/e2b/Credentials";
|
|
9
|
+
import { lazyCredentials } from "alchemy/Auth/Resolve";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
12
|
+
import * as Layer from "effect/Layer";
|
|
13
|
+
import { E2B_AUTH_PROVIDER_NAME, type E2BResolvedCredentials } from "./AuthProvider.ts";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
Credentials,
|
|
17
|
+
CredentialsFromEnv,
|
|
18
|
+
credentials,
|
|
19
|
+
DEFAULT_DOMAIN,
|
|
20
|
+
type Config as CredentialsConfig,
|
|
21
|
+
} from "@distilled.cloud/e2b/Credentials";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fills in the team id for an access token that came without one: E2B
|
|
25
|
+
* lists the token's teams, and the default team is the one it acts on.
|
|
26
|
+
*/
|
|
27
|
+
export const withTeamId = Effect.fn(function* (config: Config) {
|
|
28
|
+
if (config.accessToken === undefined || (config.teamId ?? "") !== "") return config;
|
|
29
|
+
const teams = yield* auth.listTeams({}).pipe(
|
|
30
|
+
Effect.provide(Layer.mergeAll(credentials(config), FetchHttpClient.layer)),
|
|
31
|
+
Effect.mapError(
|
|
32
|
+
(cause) =>
|
|
33
|
+
new ConfigError({
|
|
34
|
+
message: `Failed to look up the E2B team for the access token: ${cause.message}`,
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
const team = teams.find((t) => t.isDefault) ?? (teams.length === 1 ? teams[0] : undefined);
|
|
39
|
+
if (team === undefined) {
|
|
40
|
+
return yield* new ConfigError({
|
|
41
|
+
message:
|
|
42
|
+
teams.length === 0
|
|
43
|
+
? "The E2B access token belongs to no team"
|
|
44
|
+
: `The E2B access token belongs to ${teams.length} teams and none is the default: set teamId (E2B_TEAM_ID)`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return { ...config, teamId: team.teamID };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a `Credentials` layer that resolves E2B credentials via the current
|
|
52
|
+
* Alchemy profile, or directly from environment variables in CI. An access
|
|
53
|
+
* token without a team id gets its default team's id. A browser login's
|
|
54
|
+
* token is re-resolved (and refreshed) before it expires.
|
|
55
|
+
*/
|
|
56
|
+
export const fromAuthProvider = () =>
|
|
57
|
+
Layer.effect(
|
|
58
|
+
Credentials,
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
const resolve = yield* lazyCredentials<E2BResolvedCredentials, ConfigError>()(
|
|
61
|
+
E2B_AUTH_PROVIDER_NAME,
|
|
62
|
+
(creds): Config => ({
|
|
63
|
+
...(creds.apiKey === undefined ? {} : { apiKey: creds.apiKey }),
|
|
64
|
+
...(creds.accessToken === undefined ? {} : { accessToken: creds.accessToken }),
|
|
65
|
+
...(creds.teamId === undefined ? {} : { teamId: creds.teamId }),
|
|
66
|
+
domain: creds.domain,
|
|
67
|
+
apiBaseUrl: apiBaseUrlFor(creds.domain),
|
|
68
|
+
}),
|
|
69
|
+
{ expiresAt: (creds) => creds.expires },
|
|
70
|
+
);
|
|
71
|
+
// The team lookup runs once; a failed one is retried on the next call.
|
|
72
|
+
let teamId: string | undefined;
|
|
73
|
+
return Effect.gen(function* () {
|
|
74
|
+
const config = yield* resolve;
|
|
75
|
+
if (teamId !== undefined && (config.teamId ?? "") === "") return { ...config, teamId };
|
|
76
|
+
const withTeam = yield* withTeamId(config);
|
|
77
|
+
teamId = withTeam.teamId;
|
|
78
|
+
return withTeam;
|
|
79
|
+
});
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How E2B bindings reach E2B from any host.
|
|
3
|
+
*
|
|
4
|
+
* E2B authenticates with a team API key and has no identity federation or
|
|
5
|
+
* key scopes, so every host — a Cloudflare Worker, a Lambda, a container, an
|
|
6
|
+
* E2B sandbox — gets its own API key, minted at deploy as an `E2B.ApiKey` in
|
|
7
|
+
* the host's namespace and deleted with the host. The deployer's credentials
|
|
8
|
+
* never reach a host. Minting takes the deployer's access token. The binding
|
|
9
|
+
* captures the key (and the E2B domain) as named runtime Outputs; inside the
|
|
10
|
+
* deployed host they are read back from the host's env, still `Redacted`.
|
|
11
|
+
* Every runtime call provides `Credentials` from those accessors, so nothing
|
|
12
|
+
* reads `process.env`.
|
|
13
|
+
*
|
|
14
|
+
* Bindings that create sandboxes or secrets at run time tag them with the
|
|
15
|
+
* host's `alchemy::stack` / `alchemy::stage` / `alchemy::id`, computed at
|
|
16
|
+
* deploy and bound into the host (`bindOwnerTags`). Destroying the stack
|
|
17
|
+
* does not delete them.
|
|
18
|
+
*/
|
|
19
|
+
import { ConfigError } from "@distilled.cloud/core/errors";
|
|
20
|
+
import {
|
|
21
|
+
apiBaseUrlFor,
|
|
22
|
+
Credentials,
|
|
23
|
+
CredentialsFromEnv,
|
|
24
|
+
DEFAULT_DOMAIN,
|
|
25
|
+
} from "@distilled.cloud/e2b/Credentials";
|
|
26
|
+
import { orDieCredentialsUnavailable } from "alchemy/Auth/Resolve";
|
|
27
|
+
import * as Binding from "alchemy/Binding";
|
|
28
|
+
import * as Namespace from "alchemy/Namespace";
|
|
29
|
+
import * as Output from "alchemy/Output";
|
|
30
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
31
|
+
import { Stack } from "alchemy/Stack";
|
|
32
|
+
import { Stage } from "alchemy/Stage";
|
|
33
|
+
import * as Effect from "effect/Effect";
|
|
34
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
35
|
+
import type * as HttpClient from "effect/http/HttpClient";
|
|
36
|
+
import * as Layer from "effect/Layer";
|
|
37
|
+
import * as Option from "effect/Option";
|
|
38
|
+
import * as Redacted from "effect/Redacted";
|
|
39
|
+
import { ApiKey } from "./ApiKey.ts";
|
|
40
|
+
import { withTeamId } from "./Credentials.ts";
|
|
41
|
+
|
|
42
|
+
/** A plain value as an Output with no requirements (`asOutput` leaves them `any`). */
|
|
43
|
+
const output = <A>(value: A) => Output.asOutput(value) as Output.Output<A, never>;
|
|
44
|
+
|
|
45
|
+
/** The `alchemy::*` metadata on objects a host creates at run time. */
|
|
46
|
+
export type OwnerTags = Readonly<Record<string, string>>;
|
|
47
|
+
|
|
48
|
+
/** E2B's `metadata` filter: `key=value&…`. */
|
|
49
|
+
export const metadataQuery = (metadata: Readonly<Record<string, string>>) =>
|
|
50
|
+
new URLSearchParams(Object.entries(metadata)).toString();
|
|
51
|
+
|
|
52
|
+
export interface E2BAuth {
|
|
53
|
+
/** Runs a distilled call with the bound credentials and an HTTP client. */
|
|
54
|
+
readonly authorize: <A, E>(
|
|
55
|
+
effect: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
|
|
56
|
+
) => Effect.Effect<A, E, RuntimeContext>;
|
|
57
|
+
/** The E2B domain sandboxes are addressed under. */
|
|
58
|
+
readonly domain: Effect.Effect<string>;
|
|
59
|
+
/** The team the host's key belongs to. */
|
|
60
|
+
readonly teamId: Effect.Effect<string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The deploying stack's credentials: the ambient profile-backed ones, else the env. */
|
|
64
|
+
const deployCredentials = Effect.gen(function* () {
|
|
65
|
+
const ambient = yield* Effect.serviceOption(Credentials);
|
|
66
|
+
const resolve = Option.isSome(ambient)
|
|
67
|
+
? ambient.value
|
|
68
|
+
: yield* Credentials.pipe(Effect.provide(CredentialsFromEnv));
|
|
69
|
+
return yield* resolve;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const requireAccessToken = (config: { readonly accessToken?: Redacted.Redacted<string> }) =>
|
|
73
|
+
config.accessToken === undefined
|
|
74
|
+
? Effect.fail(
|
|
75
|
+
new ConfigError({
|
|
76
|
+
message:
|
|
77
|
+
"E2B bindings mint an API key for each host, which takes an E2B access token: " +
|
|
78
|
+
"add accessToken to the E2B profile (alchemy profile edit --add E2B) or set E2B_ACCESS_TOKEN",
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
: Effect.void;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Mints the current host's API key and binds it, the domain and the
|
|
85
|
+
* ownership stamp onto the host, and returns runtime accessors for them.
|
|
86
|
+
* Call it in a binding's construction; every binding on a host shares the
|
|
87
|
+
* one key.
|
|
88
|
+
*/
|
|
89
|
+
export const bindE2B = Effect.gen(function* () {
|
|
90
|
+
// Yielding the resource class erases its `Providers` requirement; the
|
|
91
|
+
// stack satisfies it at plan time.
|
|
92
|
+
const makeKey = yield* ApiKey;
|
|
93
|
+
let apiKey: Output.Output<Redacted.Redacted<string>, never> = output(Redacted.make(""));
|
|
94
|
+
let domain = DEFAULT_DOMAIN;
|
|
95
|
+
let teamId = "";
|
|
96
|
+
if (!globalThis.__ALCHEMY_RUNTIME__) {
|
|
97
|
+
const config = yield* deployCredentials.pipe(orDieCredentialsUnavailable("E2B"));
|
|
98
|
+
yield* requireAccessToken(config).pipe(orDieCredentialsUnavailable("E2B"));
|
|
99
|
+
domain = config.domain;
|
|
100
|
+
teamId = (yield* withTeamId(config).pipe(orDieCredentialsUnavailable("E2B"))).teamId ?? "";
|
|
101
|
+
const host = yield* Binding.Host;
|
|
102
|
+
if (host !== undefined) {
|
|
103
|
+
const hostKey = yield* Namespace.push(host.LogicalId, makeKey("E2BHostKey", {}));
|
|
104
|
+
apiKey = hostKey.key as Output.Output<Redacted.Redacted<string>, never>;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const key = yield* Output.named(apiKey, "E2B_API_KEY").asEffect();
|
|
108
|
+
const boundDomain = yield* Output.named(output(domain), "E2B_DOMAIN").asEffect();
|
|
109
|
+
const boundTeamId = yield* Output.named(output(teamId), "E2B_TEAM_ID").asEffect();
|
|
110
|
+
|
|
111
|
+
const domainOf = Effect.map(boundDomain, (value) => value || DEFAULT_DOMAIN);
|
|
112
|
+
const credentials = Effect.gen(function* () {
|
|
113
|
+
const resolvedDomain = yield* domainOf;
|
|
114
|
+
return {
|
|
115
|
+
apiKey: yield* key,
|
|
116
|
+
domain: resolvedDomain,
|
|
117
|
+
apiBaseUrl: apiBaseUrlFor(resolvedDomain),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
const services = Layer.mergeAll(Layer.succeed(Credentials, credentials), FetchHttpClient.layer);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
authorize: (effect) => effect.pipe(Effect.provide(services)),
|
|
124
|
+
domain: domainOf,
|
|
125
|
+
teamId: boundTeamId,
|
|
126
|
+
} satisfies E2BAuth;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const parseOwnerTags = (json: string): OwnerTags => {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(json || "{}");
|
|
132
|
+
} catch {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Binds the host's `alchemy::stack` / `alchemy::stage` / `alchemy::id` into
|
|
139
|
+
* it, and returns them at run time for `create` to put on what it makes.
|
|
140
|
+
* Without a host (a script) there are none.
|
|
141
|
+
*/
|
|
142
|
+
export const bindOwnerTags = Effect.gen(function* () {
|
|
143
|
+
let tags = "{}";
|
|
144
|
+
if (!globalThis.__ALCHEMY_RUNTIME__) {
|
|
145
|
+
const host = yield* Binding.Host;
|
|
146
|
+
// The values `createInternalTags(host.LogicalId)` gives, read without
|
|
147
|
+
// adding `Stack` / `Stage` to the binding's requirements.
|
|
148
|
+
const stack = yield* Effect.serviceOption(Stack);
|
|
149
|
+
const stage = yield* Effect.serviceOption(Stage);
|
|
150
|
+
if (host !== undefined && Option.isSome(stack) && Option.isSome(stage)) {
|
|
151
|
+
tags = JSON.stringify({
|
|
152
|
+
"alchemy::stack": stack.value.name,
|
|
153
|
+
"alchemy::stage": stage.value,
|
|
154
|
+
"alchemy::id": host.LogicalId,
|
|
155
|
+
} satisfies OwnerTags);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const bound = yield* Output.named(output(tags), "E2B_OWNER_TAGS").asEffect();
|
|
159
|
+
return Effect.map(bound, parseOwnerTags);
|
|
160
|
+
});
|