@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
package/src/Secret.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as secrets from "@distilled.cloud/e2b/secrets";
|
|
2
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import { isResolved } from "alchemy/Diff";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { Resource } from "alchemy/Resource";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Redacted from "effect/Redacted";
|
|
8
|
+
import * as Stream from "effect/Stream";
|
|
9
|
+
import { E2BUnexpectedResponse } from "./Errors.ts";
|
|
10
|
+
import {
|
|
11
|
+
e2bName,
|
|
12
|
+
hasAnyAlchemyMetadata,
|
|
13
|
+
hasOwnershipMetadata,
|
|
14
|
+
ownershipMetadata,
|
|
15
|
+
userMetadata,
|
|
16
|
+
} from "./internal/Ownership.ts";
|
|
17
|
+
import type { Providers } from "./Providers.ts";
|
|
18
|
+
|
|
19
|
+
export interface SecretProps {
|
|
20
|
+
/**
|
|
21
|
+
* Secret name, unique within the team. E2B lowercases it. If omitted, a
|
|
22
|
+
* unique name is generated from the stack, stage and logical ID. Changing
|
|
23
|
+
* it replaces the secret.
|
|
24
|
+
*/
|
|
25
|
+
name?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The secret value. Write-only: E2B never returns it. Changing it stores a
|
|
28
|
+
* new version, which every sandbox resolves on its next request.
|
|
29
|
+
*/
|
|
30
|
+
value: Redacted.Redacted<string>;
|
|
31
|
+
/** Your own metadata (at most 32 entries). */
|
|
32
|
+
metadata?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SecretAttributes {
|
|
36
|
+
/** E2B's secret id (`sec_…`). */
|
|
37
|
+
secretId: string;
|
|
38
|
+
/** The secret's canonical (lowercased) name. */
|
|
39
|
+
name: string;
|
|
40
|
+
/** The version sandboxes resolve. */
|
|
41
|
+
version: number;
|
|
42
|
+
/** Your metadata, without Alchemy's ownership stamp. */
|
|
43
|
+
metadata: Record<string, string>;
|
|
44
|
+
/**
|
|
45
|
+
* The placeholder E2B resolves to the secret's value at sandbox egress:
|
|
46
|
+
* `${e2b.secrets.<name>}`. Put it in a sandbox's network header rules; the
|
|
47
|
+
* value itself never enters the sandbox.
|
|
48
|
+
*/
|
|
49
|
+
ref: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Secret extends Resource<
|
|
53
|
+
"E2B.Secret",
|
|
54
|
+
SecretProps,
|
|
55
|
+
SecretAttributes,
|
|
56
|
+
never,
|
|
57
|
+
Providers,
|
|
58
|
+
{ name: string }
|
|
59
|
+
> {}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* An E2B secret: a write-only value that E2B injects into a sandbox's
|
|
63
|
+
* outbound requests, so code inside the sandbox never sees it.
|
|
64
|
+
*
|
|
65
|
+
* Reference a secret in a sandbox's network rules with its `ref`
|
|
66
|
+
* placeholder (`${e2b.secrets.<name>}`). E2B resolves the placeholder at
|
|
67
|
+
* egress, on the host, for requests to the domains you list.
|
|
68
|
+
*
|
|
69
|
+
* Alchemy stamps its stack, stage and logical ID into the secret's
|
|
70
|
+
* metadata to recognise the secrets it owns.
|
|
71
|
+
*
|
|
72
|
+
* ### Creating a Secret
|
|
73
|
+
* **Example:** A GitHub token from the stage's config
|
|
74
|
+
* ```typescript
|
|
75
|
+
* export const GitHubToken = E2B.Secret("GitHubToken", {
|
|
76
|
+
* value: Config.Redacted("GITHUB_TOKEN"),
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* ### Injecting a Secret at egress
|
|
81
|
+
* **Example:** Authenticate GitHub API calls from inside a sandbox
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const github = yield* GitHubToken;
|
|
84
|
+
* const sandbox = yield* sandboxes.create({
|
|
85
|
+
* network: {
|
|
86
|
+
* allowOut: ["api.github.com"],
|
|
87
|
+
* rules: {
|
|
88
|
+
* "api.github.com": [{ transform: { headers: { Authorization: `Bearer ${github.ref}` } } }],
|
|
89
|
+
* },
|
|
90
|
+
* },
|
|
91
|
+
* });
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* ### Rotating a Secret
|
|
95
|
+
* Changing `value` stores a new version in place. Sandboxes resolve the new
|
|
96
|
+
* version on their next request.
|
|
97
|
+
*
|
|
98
|
+
* **Example:** Rotate at runtime with `WriteSecret`
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const token = yield* E2B.WriteSecret(GitHubToken);
|
|
101
|
+
* yield* token.put(Redacted.make(freshToken));
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* ### Referencing a secret the stack doesn't manage
|
|
105
|
+
* `E2B.Secret.lookup` finds an existing secret by name. Nothing is created,
|
|
106
|
+
* adopted or deleted.
|
|
107
|
+
*
|
|
108
|
+
* **Example:** Look one up by name
|
|
109
|
+
* ```typescript
|
|
110
|
+
* const github = E2B.Secret.lookup({ name: "github-token" });
|
|
111
|
+
* const token = yield* E2B.WriteSecret(github);
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @resource
|
|
115
|
+
* @product Secret
|
|
116
|
+
*/
|
|
117
|
+
export const Secret = Resource<Secret>("E2B.Secret");
|
|
118
|
+
|
|
119
|
+
/** E2B's egress placeholder for a secret name. */
|
|
120
|
+
export const secretRef = (name: string): string => `\${e2b.secrets.${name}}`;
|
|
121
|
+
|
|
122
|
+
export const toSecretAttributes = (secret: secrets.Secret): SecretAttributes => ({
|
|
123
|
+
secretId: secret.secretID,
|
|
124
|
+
name: secret.name,
|
|
125
|
+
version: secret.currentVersion,
|
|
126
|
+
metadata: userMetadata(secret.metadata as Record<string, string>),
|
|
127
|
+
ref: secretRef(secret.name),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/** A secret by id or canonical name (E2B accepts either). */
|
|
131
|
+
export const observeSecret = (idOrName: string) =>
|
|
132
|
+
secrets.getSecret({ secretID: idOrName }).pipe(
|
|
133
|
+
Effect.map((secret): secrets.Secret | undefined => secret),
|
|
134
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const sameMetadata = (a: Record<string, string>, b: Record<string, string | undefined>) => {
|
|
138
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
139
|
+
for (const key of keys) if (a[key] !== b[key]) return false;
|
|
140
|
+
return true;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const SecretProvider = () =>
|
|
144
|
+
Provider.effect(
|
|
145
|
+
Secret,
|
|
146
|
+
Effect.gen(function* () {
|
|
147
|
+
return {
|
|
148
|
+
stables: ["secretId", "name", "ref"],
|
|
149
|
+
diff: Effect.fn(function* ({ id, olds, news, output }) {
|
|
150
|
+
if (!isResolved(news)) return undefined;
|
|
151
|
+
if (news.name === undefined && olds?.name === undefined) return undefined;
|
|
152
|
+
const name = output?.name ?? (yield* e2bName(id, olds?.name));
|
|
153
|
+
if (news.name?.toLowerCase() !== name) return { action: "replace" } as const;
|
|
154
|
+
return undefined;
|
|
155
|
+
}),
|
|
156
|
+
lookup: Effect.fn(function* ({ name }) {
|
|
157
|
+
const secret = yield* observeSecret(name.toLowerCase());
|
|
158
|
+
return secret === undefined ? undefined : toSecretAttributes(secret);
|
|
159
|
+
}),
|
|
160
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
161
|
+
const name = output?.name ?? (yield* e2bName(id, olds?.name));
|
|
162
|
+
const secret = yield* observeSecret(output?.secretId ?? name);
|
|
163
|
+
if (secret === undefined) return undefined;
|
|
164
|
+
const attrs = toSecretAttributes(secret);
|
|
165
|
+
const ours =
|
|
166
|
+
output !== undefined ||
|
|
167
|
+
(yield* hasOwnershipMetadata(id, secret.metadata as Record<string, string>));
|
|
168
|
+
return ours ? attrs : Unowned(attrs);
|
|
169
|
+
}),
|
|
170
|
+
reconcile: Effect.fn(function* ({ id, news, olds, output, session }) {
|
|
171
|
+
const name = output?.name ?? (yield* e2bName(id, news.name)).toLowerCase();
|
|
172
|
+
const metadata = { ...news.metadata, ...(yield* ownershipMetadata(id)) };
|
|
173
|
+
|
|
174
|
+
// Observe
|
|
175
|
+
let secret = yield* observeSecret(output?.secretId ?? name);
|
|
176
|
+
|
|
177
|
+
// Ensure — a Conflict is a peer creating the same name: read it back
|
|
178
|
+
// and fall through to the sync, which writes our value.
|
|
179
|
+
if (secret === undefined) {
|
|
180
|
+
yield* session.note(`Creating secret ${name}...`);
|
|
181
|
+
const created = yield* secrets.createSecret({ name, value: news.value, metadata }).pipe(
|
|
182
|
+
Effect.map((s): secrets.Secret | undefined => s),
|
|
183
|
+
Effect.catchTag("Conflict", () => Effect.undefined),
|
|
184
|
+
);
|
|
185
|
+
if (created !== undefined) return toSecretAttributes(created);
|
|
186
|
+
secret = yield* observeSecret(name);
|
|
187
|
+
if (secret === undefined) {
|
|
188
|
+
return yield* new E2BUnexpectedResponse({
|
|
189
|
+
message: `E2B secret ${name} vanished after create`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// A secret someone else created under this name: write our value.
|
|
193
|
+
olds = undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Sync — values are write-only, so `olds` is the only signal that the
|
|
197
|
+
// stored value already matches. Without it (adoption, a lost write),
|
|
198
|
+
// store ours. Metadata is observed.
|
|
199
|
+
const valueChanged =
|
|
200
|
+
olds === undefined || Redacted.value(olds.value) !== Redacted.value(news.value);
|
|
201
|
+
const metadataChanged = !sameMetadata(
|
|
202
|
+
metadata,
|
|
203
|
+
secret.metadata as Record<string, string | undefined>,
|
|
204
|
+
);
|
|
205
|
+
if (valueChanged || metadataChanged) {
|
|
206
|
+
secret = yield* secrets.updateSecret({
|
|
207
|
+
secretID: secret.secretID,
|
|
208
|
+
value: news.value,
|
|
209
|
+
metadata,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return toSecretAttributes(secret);
|
|
213
|
+
}),
|
|
214
|
+
delete: Effect.fn(function* ({ output }) {
|
|
215
|
+
yield* secrets
|
|
216
|
+
.deleteSecret({ secretID: output.secretId })
|
|
217
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
218
|
+
}),
|
|
219
|
+
list: Effect.fn(function* () {
|
|
220
|
+
const all = Array.from(
|
|
221
|
+
yield* Stream.runCollect(secrets.listSecrets.items({ limit: 100 })),
|
|
222
|
+
);
|
|
223
|
+
return all
|
|
224
|
+
.filter((secret) => hasAnyAlchemyMetadata(secret.metadata as Record<string, string>))
|
|
225
|
+
.map(toSecretAttributes);
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
}),
|
|
229
|
+
);
|
package/src/Snapshot.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as templates from "@distilled.cloud/e2b/templates";
|
|
3
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
4
|
+
import { isResolved } from "alchemy/Diff";
|
|
5
|
+
import * as Provider from "alchemy/Provider";
|
|
6
|
+
import { Resource } from "alchemy/Resource";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Stream from "effect/Stream";
|
|
9
|
+
import { declarable } from "./internal/Declared.ts";
|
|
10
|
+
import { e2bName, isGeneratedName } from "./internal/Ownership.ts";
|
|
11
|
+
import { takeSnapshot } from "./internal/SnapshotBuild.ts";
|
|
12
|
+
import type { Providers } from "./Providers.ts";
|
|
13
|
+
import type { SandboxTemplate } from "./Sandbox.ts";
|
|
14
|
+
|
|
15
|
+
export interface SnapshotProps {
|
|
16
|
+
/**
|
|
17
|
+
* The `E2B.Template` (or `E2B.Snapshot`) the snapshot starts from.
|
|
18
|
+
* Rebuilding it takes the snapshot again.
|
|
19
|
+
*/
|
|
20
|
+
template: SandboxTemplate;
|
|
21
|
+
/**
|
|
22
|
+
* Shell commands run in order (`bash -l -c`) before the snapshot is taken:
|
|
23
|
+
* clone a repository, install dependencies, warm a cache. A non-zero exit
|
|
24
|
+
* fails the deploy with `SnapshotSetupFailed`.
|
|
25
|
+
*/
|
|
26
|
+
run: string[];
|
|
27
|
+
/** Environment variables for the setup commands. */
|
|
28
|
+
envs?: Record<string, string>;
|
|
29
|
+
/** Working directory for the setup commands. @default the sandbox user's home */
|
|
30
|
+
cwd?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Capture memory as well as the filesystem, so sandboxes resume with the
|
|
33
|
+
* setup's processes still running. `false` captures the filesystem only
|
|
34
|
+
* and sandboxes cold-boot from it.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
memory?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Snapshot name. If omitted, a unique name is generated from the stack,
|
|
40
|
+
* stage and logical ID. Changing it replaces the snapshot.
|
|
41
|
+
*/
|
|
42
|
+
name?: string;
|
|
43
|
+
/**
|
|
44
|
+
* How long the setup sandbox may run, in seconds.
|
|
45
|
+
* @default 900
|
|
46
|
+
*/
|
|
47
|
+
timeout?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SnapshotAttributes {
|
|
51
|
+
/** The id sandboxes are created from, e.g. `my-team/my-snapshot:default`. */
|
|
52
|
+
snapshotId: string;
|
|
53
|
+
/** The snapshot's name. */
|
|
54
|
+
name: string;
|
|
55
|
+
/** E2B's template id for the snapshot (a snapshot is stored as a template). */
|
|
56
|
+
templateId: string;
|
|
57
|
+
/** Every name the snapshot answers to. */
|
|
58
|
+
names: string[];
|
|
59
|
+
/** Content hash of the base and setup. A deploy re-snapshots only when it changes. */
|
|
60
|
+
setupHash: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface Snapshot extends Resource<
|
|
64
|
+
"E2B.Snapshot",
|
|
65
|
+
SnapshotProps,
|
|
66
|
+
SnapshotAttributes,
|
|
67
|
+
never,
|
|
68
|
+
Providers,
|
|
69
|
+
{ name: string }
|
|
70
|
+
> {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A warm base to fork sandboxes from: a template booted, set up with your
|
|
74
|
+
* commands, and snapshotted — memory included, so sandboxes created from it
|
|
75
|
+
* start exactly where the setup left off.
|
|
76
|
+
*
|
|
77
|
+
* Use a snapshot for work that is too slow to repeat per sandbox and does
|
|
78
|
+
* not belong in an image: cloning a repository, installing its
|
|
79
|
+
* dependencies, starting a language server. A deploy re-runs the setup only
|
|
80
|
+
* when the template or the commands change. E2B stores a snapshot as a
|
|
81
|
+
* template, so it is created from with `E2B.CreateSandbox(snapshot)` and
|
|
82
|
+
* deleted with its template.
|
|
83
|
+
*
|
|
84
|
+
* ### Creating a Snapshot
|
|
85
|
+
* **Example:** A repository with its dependencies installed
|
|
86
|
+
* ```typescript
|
|
87
|
+
* export const Agent = E2B.Template("Agent", { image: "node:22-bookworm" });
|
|
88
|
+
*
|
|
89
|
+
* export const Warm = E2B.Snapshot("Warm", {
|
|
90
|
+
* template: Agent,
|
|
91
|
+
* run: [
|
|
92
|
+
* "git clone https://github.com/alchemy-run/alchemy /home/user/alchemy",
|
|
93
|
+
* "cd /home/user/alchemy && pnpm install",
|
|
94
|
+
* ],
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* ### Forking sandboxes from a Snapshot
|
|
99
|
+
* **Example:** Every sandbox starts from the warm base
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const sandboxes = yield* E2B.CreateSandbox(Warm);
|
|
102
|
+
* const sandbox = yield* sandboxes.create({ timeout: 600 });
|
|
103
|
+
* yield* sandbox.exec("cd alchemy && pnpm test");
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* ### Referencing a snapshot the stack doesn't manage
|
|
107
|
+
* `E2B.Snapshot.lookup` finds an existing snapshot by name. Nothing is created,
|
|
108
|
+
* adopted or deleted.
|
|
109
|
+
*
|
|
110
|
+
* **Example:** Look one up by name
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const warm = E2B.Snapshot.lookup({ name: "warm-repo" });
|
|
113
|
+
* const sandboxes = yield* E2B.CreateSandbox(warm);
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @resource
|
|
117
|
+
* @product Snapshot
|
|
118
|
+
*/
|
|
119
|
+
export const Snapshot = declarable<Snapshot>()(Resource<Snapshot>("E2B.Snapshot"));
|
|
120
|
+
|
|
121
|
+
const setupHashOf = (props: SnapshotProps) =>
|
|
122
|
+
crypto
|
|
123
|
+
.createHash("sha256")
|
|
124
|
+
.update(
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
template: props.template.templateId,
|
|
127
|
+
build: props.template.buildId ?? props.template.setupHash,
|
|
128
|
+
run: props.run,
|
|
129
|
+
envs: props.envs,
|
|
130
|
+
cwd: props.cwd,
|
|
131
|
+
memory: props.memory ?? true,
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
.digest("hex");
|
|
135
|
+
|
|
136
|
+
/** The snapshot template, by alias. */
|
|
137
|
+
const observe = (name: string) =>
|
|
138
|
+
templates.getTemplateByAlias({ alias: name }).pipe(
|
|
139
|
+
Effect.flatMap((alias) => templates.getTemplate({ templateID: alias.templateID })),
|
|
140
|
+
Effect.map((template): templates.GetTemplateResponse | undefined => template),
|
|
141
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
export const SnapshotProvider = () =>
|
|
145
|
+
Provider.effect(
|
|
146
|
+
Snapshot,
|
|
147
|
+
Effect.gen(function* () {
|
|
148
|
+
return {
|
|
149
|
+
stables: ["name", "templateId"],
|
|
150
|
+
diff: Effect.fn(function* ({ id, olds, news, output }) {
|
|
151
|
+
if (!isResolved(news)) return undefined;
|
|
152
|
+
const name = output?.name ?? (yield* e2bName(id, olds?.name));
|
|
153
|
+
if (news.name !== undefined && news.name !== name) return { action: "replace" } as const;
|
|
154
|
+
if (output !== undefined && setupHashOf(news) !== output.setupHash) {
|
|
155
|
+
return { action: "update" } as const;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}),
|
|
159
|
+
lookup: Effect.fn(function* ({ name }) {
|
|
160
|
+
const template = yield* observe(name);
|
|
161
|
+
if (template === undefined) return undefined;
|
|
162
|
+
return {
|
|
163
|
+
snapshotId: template.names[0] ?? template.templateID,
|
|
164
|
+
name,
|
|
165
|
+
templateId: template.templateID,
|
|
166
|
+
names: [...template.names],
|
|
167
|
+
setupHash: "",
|
|
168
|
+
} satisfies SnapshotAttributes;
|
|
169
|
+
}),
|
|
170
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
171
|
+
const name = output?.name ?? (yield* e2bName(id, olds?.name));
|
|
172
|
+
const template = yield* observe(name);
|
|
173
|
+
if (template === undefined) return undefined;
|
|
174
|
+
const attrs: SnapshotAttributes = {
|
|
175
|
+
snapshotId: output?.snapshotId ?? template.names[0] ?? template.templateID,
|
|
176
|
+
name,
|
|
177
|
+
templateId: template.templateID,
|
|
178
|
+
names: [...template.names],
|
|
179
|
+
setupHash: output?.setupHash ?? "",
|
|
180
|
+
};
|
|
181
|
+
return output !== undefined || olds?.name === undefined ? attrs : Unowned(attrs);
|
|
182
|
+
}),
|
|
183
|
+
reconcile: Effect.fn(function* ({ id, news, output, session }) {
|
|
184
|
+
const name = output?.name ?? (yield* e2bName(id, news.name));
|
|
185
|
+
const setupHash = setupHashOf(news);
|
|
186
|
+
|
|
187
|
+
// Observe — an existing ready snapshot of this exact setup is kept.
|
|
188
|
+
const observed = yield* observe(name);
|
|
189
|
+
if (
|
|
190
|
+
observed !== undefined &&
|
|
191
|
+
observed.buildStatus === "ready" &&
|
|
192
|
+
output?.setupHash === setupHash
|
|
193
|
+
) {
|
|
194
|
+
return { ...output, names: [...observed.names], templateId: observed.templateID };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Ensure — snapshotting under an existing name adds a new build to
|
|
198
|
+
// the same template.
|
|
199
|
+
yield* session.note(`Snapshotting ${name}...`);
|
|
200
|
+
const snapshot = yield* takeSnapshot(name, {
|
|
201
|
+
...news,
|
|
202
|
+
template: news.template.templateId,
|
|
203
|
+
});
|
|
204
|
+
const template = yield* observe(name);
|
|
205
|
+
return {
|
|
206
|
+
snapshotId: snapshot.snapshotID,
|
|
207
|
+
name,
|
|
208
|
+
templateId: template?.templateID ?? snapshot.snapshotID,
|
|
209
|
+
names: [...snapshot.names],
|
|
210
|
+
setupHash,
|
|
211
|
+
};
|
|
212
|
+
}),
|
|
213
|
+
delete: Effect.fn(function* ({ output }) {
|
|
214
|
+
yield* templates
|
|
215
|
+
.deleteTemplate({ templateID: output.templateId })
|
|
216
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
217
|
+
}),
|
|
218
|
+
list: Effect.fn(function* () {
|
|
219
|
+
const all = Array.from(
|
|
220
|
+
yield* Stream.runCollect(templates.listTemplates.items({ limit: 100 })),
|
|
221
|
+
);
|
|
222
|
+
return all
|
|
223
|
+
.filter((template) => template.aliases.some(isGeneratedName))
|
|
224
|
+
.map((template): SnapshotAttributes => ({
|
|
225
|
+
snapshotId: template.names[0] ?? template.templateID,
|
|
226
|
+
name: template.aliases.find(isGeneratedName) ?? template.templateID,
|
|
227
|
+
templateId: template.templateID,
|
|
228
|
+
names: [...template.names],
|
|
229
|
+
setupHash: "",
|
|
230
|
+
}));
|
|
231
|
+
}),
|
|
232
|
+
};
|
|
233
|
+
}),
|
|
234
|
+
);
|