@alchemy.run/daytona 0.0.0-stage → 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 +80 -2
- package/lib/ApiKey.d.ts +99 -0
- package/lib/ApiKey.d.ts.map +1 -0
- package/lib/ApiKey.js +152 -0
- package/lib/ApiKey.js.map +1 -0
- package/lib/AuthProvider.d.ts +55 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +280 -0
- package/lib/AuthProvider.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 +72 -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 +5 -0
- package/lib/ConnectSandboxHttp.d.ts.map +1 -0
- package/lib/ConnectSandboxHttp.js +22 -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 +86 -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 +5 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +65 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CreateSecret.d.ts +56 -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 +5 -0
- package/lib/CreateSecretHttp.d.ts.map +1 -0
- package/lib/CreateSecretHttp.js +39 -0
- package/lib/CreateSecretHttp.js.map +1 -0
- package/lib/CreateSnapshot.d.ts +80 -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 +5 -0
- package/lib/CreateSnapshotHttp.d.ts.map +1 -0
- package/lib/CreateSnapshotHttp.js +47 -0
- package/lib/CreateSnapshotHttp.js.map +1 -0
- package/lib/CreateVolume.d.ts +81 -0
- package/lib/CreateVolume.d.ts.map +1 -0
- package/lib/CreateVolume.js +10 -0
- package/lib/CreateVolume.js.map +1 -0
- package/lib/CreateVolumeHttp.d.ts +5 -0
- package/lib/CreateVolumeHttp.d.ts.map +1 -0
- package/lib/CreateVolumeHttp.js +31 -0
- package/lib/CreateVolumeHttp.js.map +1 -0
- package/lib/Credentials.d.ts +9 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +18 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/DaytonaBindings.d.ts +51 -0
- package/lib/DaytonaBindings.d.ts.map +1 -0
- package/lib/DaytonaBindings.js +61 -0
- package/lib/DaytonaBindings.js.map +1 -0
- package/lib/IdentityProvider.d.ts +80 -0
- package/lib/IdentityProvider.d.ts.map +1 -0
- package/lib/IdentityProvider.js +111 -0
- package/lib/IdentityProvider.js.map +1 -0
- package/lib/MountVolume.d.ts +86 -0
- package/lib/MountVolume.d.ts.map +1 -0
- package/lib/MountVolume.js +27 -0
- package/lib/MountVolume.js.map +1 -0
- package/lib/OAuthClient.d.ts +14 -0
- package/lib/OAuthClient.d.ts.map +1 -0
- package/lib/OAuthClient.js +23 -0
- package/lib/OAuthClient.js.map +1 -0
- package/lib/Organization.d.ts +40 -0
- package/lib/Organization.d.ts.map +1 -0
- package/lib/Organization.js +80 -0
- package/lib/Organization.js.map +1 -0
- package/lib/OrganizationInvitation.d.ts +70 -0
- package/lib/OrganizationInvitation.d.ts.map +1 -0
- package/lib/OrganizationInvitation.js +109 -0
- package/lib/OrganizationInvitation.js.map +1 -0
- package/lib/OrganizationMember.d.ts +86 -0
- package/lib/OrganizationMember.d.ts.map +1 -0
- package/lib/OrganizationMember.js +109 -0
- package/lib/OrganizationMember.js.map +1 -0
- package/lib/OrganizationOtelConfig.d.ts +59 -0
- package/lib/OrganizationOtelConfig.d.ts.map +1 -0
- package/lib/OrganizationOtelConfig.js +87 -0
- package/lib/OrganizationOtelConfig.js.map +1 -0
- package/lib/OrganizationRole.d.ts +66 -0
- package/lib/OrganizationRole.d.ts.map +1 -0
- package/lib/OrganizationRole.js +106 -0
- package/lib/OrganizationRole.js.map +1 -0
- package/lib/OrganizationSettings.d.ts +76 -0
- package/lib/OrganizationSettings.d.ts.map +1 -0
- package/lib/OrganizationSettings.js +107 -0
- package/lib/OrganizationSettings.js.map +1 -0
- package/lib/Providers.d.ts +14 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +53 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadAuditLogs.d.ts +30 -0
- package/lib/ReadAuditLogs.d.ts.map +1 -0
- package/lib/ReadAuditLogs.js +17 -0
- package/lib/ReadAuditLogs.js.map +1 -0
- package/lib/ReadOrganization.d.ts +39 -0
- package/lib/ReadOrganization.d.ts.map +1 -0
- package/lib/ReadOrganization.js +39 -0
- package/lib/ReadOrganization.js.map +1 -0
- package/lib/ReadUsage.d.ts +31 -0
- package/lib/ReadUsage.d.ts.map +1 -0
- package/lib/ReadUsage.js +17 -0
- package/lib/ReadUsage.js.map +1 -0
- package/lib/Region.d.ts +78 -0
- package/lib/Region.d.ts.map +1 -0
- package/lib/Region.js +169 -0
- package/lib/Region.js.map +1 -0
- package/lib/Registry.d.ts +84 -0
- package/lib/Registry.d.ts.map +1 -0
- package/lib/Registry.js +97 -0
- package/lib/Registry.js.map +1 -0
- package/lib/Runner.d.ts +66 -0
- package/lib/Runner.d.ts.map +1 -0
- package/lib/Runner.js +116 -0
- package/lib/Runner.js.map +1 -0
- package/lib/Sandbox.d.ts +376 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +565 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +258 -0
- package/lib/SandboxClient.d.ts.map +1 -0
- package/lib/SandboxClient.js +233 -0
- package/lib/SandboxClient.js.map +1 -0
- package/lib/Secret.d.ts +93 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +130 -0
- package/lib/Secret.js.map +1 -0
- package/lib/Snapshot.d.ts +327 -0
- package/lib/Snapshot.d.ts.map +1 -0
- package/lib/Snapshot.js +447 -0
- package/lib/Snapshot.js.map +1 -0
- package/lib/Volume.d.ts +99 -0
- package/lib/Volume.d.ts.map +1 -0
- package/lib/Volume.js +152 -0
- package/lib/Volume.js.map +1 -0
- package/lib/WarmPool.d.ts +61 -0
- package/lib/WarmPool.d.ts.map +1 -0
- package/lib/WarmPool.js +81 -0
- package/lib/WarmPool.js.map +1 -0
- package/lib/Webhooks.d.ts +58 -0
- package/lib/Webhooks.d.ts.map +1 -0
- package/lib/Webhooks.js +75 -0
- package/lib/Webhooks.js.map +1 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +36 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/BuildContext.d.ts +16 -0
- package/lib/internal/BuildContext.d.ts.map +1 -0
- package/lib/internal/BuildContext.js +50 -0
- package/lib/internal/BuildContext.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/Derived.d.ts +40 -0
- package/lib/internal/Derived.d.ts.map +1 -0
- package/lib/internal/Derived.js +64 -0
- package/lib/internal/Derived.js.map +1 -0
- package/lib/internal/Errors.d.ts +4 -0
- package/lib/internal/Errors.d.ts.map +1 -0
- package/lib/internal/Errors.js +1 -0
- package/lib/internal/Errors.js.map +1 -0
- package/lib/internal/Labels.d.ts +20 -0
- package/lib/internal/Labels.d.ts.map +1 -0
- package/lib/internal/Labels.js +22 -0
- package/lib/internal/Labels.js.map +1 -0
- package/lib/internal/Names.d.ts +18 -0
- package/lib/internal/Names.d.ts.map +1 -0
- package/lib/internal/Names.js +25 -0
- package/lib/internal/Names.js.map +1 -0
- package/lib/internal/Organization.d.ts +27 -0
- package/lib/internal/Organization.d.ts.map +1 -0
- package/lib/internal/Organization.js +32 -0
- package/lib/internal/Organization.js.map +1 -0
- package/lib/internal/Process.d.ts +14 -0
- package/lib/internal/Process.d.ts.map +1 -0
- package/lib/internal/Process.js +15 -0
- package/lib/internal/Process.js.map +1 -0
- package/lib/internal/Program.d.ts +83 -0
- package/lib/internal/Program.d.ts.map +1 -0
- package/lib/internal/Program.js +169 -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/Scoped.d.ts +31 -0
- package/lib/internal/Scoped.d.ts.map +1 -0
- package/lib/internal/Scoped.js +48 -0
- package/lib/internal/Scoped.js.map +1 -0
- package/lib/internal/Tar.d.ts +13 -0
- package/lib/internal/Tar.d.ts.map +1 -0
- package/lib/internal/Tar.js +62 -0
- package/lib/internal/Tar.js.map +1 -0
- package/lib/internal/Toolbox.d.ts +11 -0
- package/lib/internal/Toolbox.d.ts.map +1 -0
- package/lib/internal/Toolbox.js +20 -0
- package/lib/internal/Toolbox.js.map +1 -0
- package/lib/internal/Wait.d.ts +49 -0
- package/lib/internal/Wait.d.ts.map +1 -0
- package/lib/internal/Wait.js +57 -0
- package/lib/internal/Wait.js.map +1 -0
- package/package.json +79 -3
- package/src/ApiKey.ts +221 -0
- package/src/AuthProvider.ts +406 -0
- package/src/Bootstrap.ts +55 -0
- package/src/ConnectSandbox.ts +76 -0
- package/src/ConnectSandboxHttp.ts +26 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateSandbox.ts +107 -0
- package/src/CreateSandboxHttp.ts +97 -0
- package/src/CreateSecret.ts +73 -0
- package/src/CreateSecretHttp.ts +58 -0
- package/src/CreateSnapshot.ts +133 -0
- package/src/CreateSnapshotHttp.ts +77 -0
- package/src/CreateVolume.ts +104 -0
- package/src/CreateVolumeHttp.ts +50 -0
- package/src/Credentials.ts +37 -0
- package/src/DaytonaBindings.ts +111 -0
- package/src/IdentityProvider.ts +181 -0
- package/src/MountVolume.ts +122 -0
- package/src/OAuthClient.ts +27 -0
- package/src/Organization.ts +121 -0
- package/src/OrganizationInvitation.ts +174 -0
- package/src/OrganizationMember.ts +174 -0
- package/src/OrganizationOtelConfig.ts +138 -0
- package/src/OrganizationRole.ts +167 -0
- package/src/OrganizationSettings.ts +187 -0
- package/src/Providers.ts +93 -0
- package/src/ReadAuditLogs.ts +50 -0
- package/src/ReadOrganization.ts +83 -0
- package/src/ReadUsage.ts +51 -0
- package/src/Region.ts +226 -0
- package/src/Registry.ts +164 -0
- package/src/Runner.ts +166 -0
- package/src/Sandbox.ts +927 -0
- package/src/SandboxClient.ts +625 -0
- package/src/Secret.ts +203 -0
- package/src/Snapshot.ts +685 -0
- package/src/Volume.ts +229 -0
- package/src/WarmPool.ts +128 -0
- package/src/Webhooks.ts +121 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +46 -0
- package/src/internal/BuildContext.ts +62 -0
- package/src/internal/Declared.ts +80 -0
- package/src/internal/Derived.ts +97 -0
- package/src/internal/Errors.ts +4 -0
- package/src/internal/Labels.ts +39 -0
- package/src/internal/Names.ts +37 -0
- package/src/internal/Organization.ts +40 -0
- package/src/internal/Process.ts +25 -0
- package/src/internal/Program.ts +239 -0
- package/src/internal/Ref.ts +15 -0
- package/src/internal/Scoped.ts +102 -0
- package/src/internal/Tar.ts +72 -0
- package/src/internal/Toolbox.ts +27 -0
- package/src/internal/Wait.ts +105 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { credentials, DEFAULT_API_BASE_URL } from "@distilled.cloud/daytona/Credentials";
|
|
2
|
+
import { listOrganizations } from "@distilled.cloud/daytona/organizations";
|
|
3
|
+
import {
|
|
4
|
+
AuthError,
|
|
5
|
+
AuthProviderLayer,
|
|
6
|
+
NeedsReauth,
|
|
7
|
+
refreshHint,
|
|
8
|
+
type ConfigureField,
|
|
9
|
+
type ConfigureMethod,
|
|
10
|
+
type ProviderDetails,
|
|
11
|
+
} from "alchemy/Auth/AuthProvider";
|
|
12
|
+
import { deviceOAuth } from "alchemy/Auth/BrowserOAuth";
|
|
13
|
+
import { displayRedacted } from "alchemy/Auth/Credentials";
|
|
14
|
+
import { getEnv, getEnvRedacted, mapPromptCancellation } from "alchemy/Auth/Env";
|
|
15
|
+
import { withProfileCredentialsLock } from "alchemy/Auth/Lock";
|
|
16
|
+
import {
|
|
17
|
+
collectFieldValues,
|
|
18
|
+
storedSecret,
|
|
19
|
+
storedValueText,
|
|
20
|
+
validateFieldValues,
|
|
21
|
+
type StoredValues,
|
|
22
|
+
} from "alchemy/Auth/StoredAuthProvider";
|
|
23
|
+
import * as Interaction from "alchemy/Interaction";
|
|
24
|
+
import * as Clock from "effect/Clock";
|
|
25
|
+
import * as Duration from "effect/Duration";
|
|
26
|
+
import * as Effect from "effect/Effect";
|
|
27
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
28
|
+
import * as Layer from "effect/Layer";
|
|
29
|
+
import * as Match from "effect/Match";
|
|
30
|
+
import * as Redacted from "effect/Redacted";
|
|
31
|
+
import * as Schema from "effect/Schema";
|
|
32
|
+
import * as OAuthClient from "./OAuthClient.ts";
|
|
33
|
+
|
|
34
|
+
export const DAYTONA_AUTH_PROVIDER_NAME = "Daytona";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Values stored in a Daytona profile: an organization API key, or a user
|
|
38
|
+
* session from `alchemy profile login` (Daytona's device login).
|
|
39
|
+
*/
|
|
40
|
+
export const DaytonaAuthConfigSchema = Schema.Union([
|
|
41
|
+
Schema.Struct({
|
|
42
|
+
method: Schema.Literal("stored"),
|
|
43
|
+
apiKey: Schema.String,
|
|
44
|
+
apiBaseUrl: Schema.optional(Schema.String),
|
|
45
|
+
organizationId: Schema.optional(Schema.String),
|
|
46
|
+
}),
|
|
47
|
+
Schema.Struct({
|
|
48
|
+
method: Schema.Literal("oauth"),
|
|
49
|
+
organizationId: Schema.String,
|
|
50
|
+
apiBaseUrl: Schema.optional(Schema.String),
|
|
51
|
+
clientId: Schema.optional(Schema.String),
|
|
52
|
+
access: Schema.String,
|
|
53
|
+
refresh: Schema.String,
|
|
54
|
+
expires: Schema.Number,
|
|
55
|
+
scopes: Schema.String.pipe(Schema.Array, Schema.mutable),
|
|
56
|
+
}),
|
|
57
|
+
]);
|
|
58
|
+
export type DaytonaAuthConfig = typeof DaytonaAuthConfigSchema.Type;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolved Daytona credentials: a bearer token and the API root. A user
|
|
62
|
+
* session (`type: "oauth"`) can also manage the organization, its roles,
|
|
63
|
+
* invitations and API keys, which Daytona refuses to organization API keys.
|
|
64
|
+
*/
|
|
65
|
+
export interface DaytonaResolvedCredentials {
|
|
66
|
+
type: "apiKey" | "oauth";
|
|
67
|
+
/** The organization API key, or the user's access token. */
|
|
68
|
+
token: Redacted.Redacted<string>;
|
|
69
|
+
apiBaseUrl: string;
|
|
70
|
+
/** Sent as `X-Daytona-Organization-ID`; an API key already belongs to one organization. */
|
|
71
|
+
organizationId?: string;
|
|
72
|
+
/** When a user access token expires (epoch ms). */
|
|
73
|
+
expires?: number;
|
|
74
|
+
source: { type: DaytonaAuthConfig["method"] | "env"; details?: string };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const apiKeyFields: ReadonlyArray<ConfigureField> = [
|
|
78
|
+
{
|
|
79
|
+
name: "apiKey",
|
|
80
|
+
label: "Daytona API key (https://app.daytona.io/dashboard/keys)",
|
|
81
|
+
secret: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "apiBaseUrl",
|
|
85
|
+
label: "Daytona API URL",
|
|
86
|
+
optional: true,
|
|
87
|
+
placeholder: DEFAULT_API_BASE_URL,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const storedConfig = (values: StoredValues): DaytonaAuthConfig => ({
|
|
92
|
+
method: "stored",
|
|
93
|
+
apiKey: Redacted.value(storedSecret(values.apiKey) ?? Redacted.make("")),
|
|
94
|
+
apiBaseUrl: storedValueText(values.apiBaseUrl),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const toOAuthCredentials = (
|
|
98
|
+
config: Extract<DaytonaAuthConfig, { method: "oauth" }>,
|
|
99
|
+
): OAuthClient.OAuthCredentials => ({
|
|
100
|
+
type: "oauth",
|
|
101
|
+
clientId: config.clientId,
|
|
102
|
+
access: Redacted.make(config.access),
|
|
103
|
+
refresh: Redacted.make(config.refresh),
|
|
104
|
+
expires: config.expires,
|
|
105
|
+
scopes: config.scopes,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const fromOAuthCredentials = (fresh: OAuthClient.OAuthCredentials) => ({
|
|
109
|
+
clientId: fresh.clientId,
|
|
110
|
+
access: Redacted.value(fresh.access),
|
|
111
|
+
refresh: Redacted.value(fresh.refresh),
|
|
112
|
+
expires: fresh.expires,
|
|
113
|
+
scopes: fresh.scopes,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** List the user's organizations and pick one (prompting when there are several). */
|
|
117
|
+
const selectOrganization = (accessToken: Redacted.Redacted<string>) =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
const organizations = yield* listOrganizations({});
|
|
120
|
+
const [only] = organizations;
|
|
121
|
+
if (only === undefined) {
|
|
122
|
+
return yield* AuthError.make({ message: "Daytona: this user belongs to no organization." });
|
|
123
|
+
}
|
|
124
|
+
if (organizations.length === 1) {
|
|
125
|
+
yield* Interaction.info(`Daytona: using organization ${only.name} (${only.id})`);
|
|
126
|
+
return only.id;
|
|
127
|
+
}
|
|
128
|
+
return yield* Interaction.select({
|
|
129
|
+
message: "Select a Daytona organization",
|
|
130
|
+
options: organizations.map((o) => ({ value: o.id, label: o.name, description: o.id })),
|
|
131
|
+
}).pipe(mapPromptCancellation);
|
|
132
|
+
}).pipe(
|
|
133
|
+
Effect.provide(Layer.mergeAll(credentials({ apiKey: accessToken }), FetchHttpClient.layer)),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Layer that registers the Daytona {@link AuthProvider} into the
|
|
138
|
+
* {@link AuthProviders} registry when built, so `alchemy profile edit` can
|
|
139
|
+
* configure it.
|
|
140
|
+
*
|
|
141
|
+
* Supported methods:
|
|
142
|
+
* - `oauth`: Daytona's device login (the same as `daytona login`); the
|
|
143
|
+
* session refreshes itself and can manage the organization.
|
|
144
|
+
* - `stored`: an organization API key stored inline.
|
|
145
|
+
*/
|
|
146
|
+
export const DaytonaAuth = AuthProviderLayer<DaytonaAuthConfig, DaytonaResolvedCredentials>()(
|
|
147
|
+
DAYTONA_AUTH_PROVIDER_NAME,
|
|
148
|
+
Effect.gen(function* () {
|
|
149
|
+
const configureOAuth = Effect.fn(function* (_profileName: string) {
|
|
150
|
+
const authorization = yield* OAuthClient.deviceAuthorize();
|
|
151
|
+
const session = yield* deviceOAuth({
|
|
152
|
+
provider: "Daytona",
|
|
153
|
+
authorization,
|
|
154
|
+
token: OAuthClient.deviceToken(authorization),
|
|
155
|
+
});
|
|
156
|
+
const organizationId = yield* selectOrganization(session.access);
|
|
157
|
+
yield* Interaction.success("Daytona: signed in.");
|
|
158
|
+
return {
|
|
159
|
+
method: "oauth" as const,
|
|
160
|
+
organizationId,
|
|
161
|
+
...fromOAuthCredentials(session),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const configureApiKey = (_profileName: string) =>
|
|
166
|
+
collectFieldValues(apiKeyFields).pipe(
|
|
167
|
+
Effect.map(storedConfig),
|
|
168
|
+
Effect.tap(() => Interaction.success("Daytona: credentials saved.")),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const configure = (profileName: string) =>
|
|
172
|
+
Interaction.select({
|
|
173
|
+
message: "Daytona authentication method",
|
|
174
|
+
options: [
|
|
175
|
+
{
|
|
176
|
+
value: "oauth" as const,
|
|
177
|
+
label: "Sign in",
|
|
178
|
+
description: "recommended — device login; can also manage the organization",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
value: "stored" as const,
|
|
182
|
+
label: "API key",
|
|
183
|
+
description: "an organization API key stored inline",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
}).pipe(
|
|
187
|
+
mapPromptCancellation,
|
|
188
|
+
Effect.flatMap((method) =>
|
|
189
|
+
Match.value(method).pipe(
|
|
190
|
+
Match.when("oauth", () => configureOAuth(profileName)),
|
|
191
|
+
Match.when("stored", () => configureApiKey(profileName)),
|
|
192
|
+
Match.exhaustive,
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
Effect.mapError((e) =>
|
|
196
|
+
AuthError.make({ message: "failed to configure credentials", cause: e }),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Sign-in needs a browser, so only the API key has a flag-driven form.
|
|
201
|
+
const configureMethods: ReadonlyArray<ConfigureMethod> = [
|
|
202
|
+
{ method: "stored", fields: apiKeyFields },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const configureWith = (
|
|
206
|
+
_profileName: string,
|
|
207
|
+
input: { readonly method: string; readonly values: Record<string, string> },
|
|
208
|
+
) =>
|
|
209
|
+
input.method === "stored"
|
|
210
|
+
? validateFieldValues(DAYTONA_AUTH_PROVIDER_NAME, apiKeyFields, input.values).pipe(
|
|
211
|
+
Effect.map(storedConfig),
|
|
212
|
+
Effect.tap(() => Interaction.success("Daytona: credentials saved.")),
|
|
213
|
+
)
|
|
214
|
+
: Effect.fail(
|
|
215
|
+
AuthError.make({
|
|
216
|
+
message: `Daytona: unknown method '${input.method}'. Valid methods: stored. (Sign-in is interactive-only.)`,
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const read = (
|
|
221
|
+
profileName: string,
|
|
222
|
+
config: DaytonaAuthConfig,
|
|
223
|
+
updateConfig?: (config: DaytonaAuthConfig) => Effect.Effect<void, AuthError>,
|
|
224
|
+
) =>
|
|
225
|
+
Match.value(config).pipe(
|
|
226
|
+
Match.when({ method: "stored" }, (stored) =>
|
|
227
|
+
Effect.succeed({
|
|
228
|
+
type: "apiKey" as const,
|
|
229
|
+
token: Redacted.make(stored.apiKey),
|
|
230
|
+
apiBaseUrl: stored.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
231
|
+
organizationId: stored.organizationId,
|
|
232
|
+
source: { type: "stored" as const },
|
|
233
|
+
} satisfies DaytonaResolvedCredentials),
|
|
234
|
+
),
|
|
235
|
+
Match.when({ method: "oauth" }, (oauth) =>
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
const reauth = refreshHint(DAYTONA_AUTH_PROVIDER_NAME, profileName);
|
|
238
|
+
const session = toOAuthCredentials(oauth);
|
|
239
|
+
if (!OAuthClient.usesCurrentClient(session)) {
|
|
240
|
+
return yield* NeedsReauth.make({
|
|
241
|
+
provider: DAYTONA_AUTH_PROVIDER_NAME,
|
|
242
|
+
profile: profileName,
|
|
243
|
+
message: `Daytona credentials for profile '${profileName}' were issued to another OAuth client. ${reauth}`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Refresh within a minute of expiry and persist it: WorkOS
|
|
247
|
+
// refresh tokens are single-use.
|
|
248
|
+
const now = yield* Clock.currentTimeMillis;
|
|
249
|
+
const fresh =
|
|
250
|
+
session.expires > now + 60_000
|
|
251
|
+
? session
|
|
252
|
+
: yield* OAuthClient.refresh(session).pipe(
|
|
253
|
+
Effect.mapError((e) =>
|
|
254
|
+
NeedsReauth.make({
|
|
255
|
+
provider: DAYTONA_AUTH_PROVIDER_NAME,
|
|
256
|
+
profile: profileName,
|
|
257
|
+
message: `Daytona session refresh failed. ${reauth}`,
|
|
258
|
+
cause: e,
|
|
259
|
+
}),
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
if (fresh !== session) {
|
|
263
|
+
yield* updateConfig?.({ ...oauth, ...fromOAuthCredentials(fresh) }) ?? Effect.void;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
type: "oauth" as const,
|
|
267
|
+
token: fresh.access,
|
|
268
|
+
apiBaseUrl: oauth.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
269
|
+
organizationId: oauth.organizationId,
|
|
270
|
+
expires: fresh.expires,
|
|
271
|
+
source: { type: "oauth" as const },
|
|
272
|
+
} satisfies DaytonaResolvedCredentials;
|
|
273
|
+
}),
|
|
274
|
+
),
|
|
275
|
+
Match.exhaustive,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const login = (
|
|
279
|
+
profileName: string,
|
|
280
|
+
config: DaytonaAuthConfig,
|
|
281
|
+
updateConfig?: (config: DaytonaAuthConfig) => Effect.Effect<void, AuthError>,
|
|
282
|
+
reload?: Effect.Effect<DaytonaAuthConfig, AuthError>,
|
|
283
|
+
) =>
|
|
284
|
+
Match.value(config).pipe(
|
|
285
|
+
Match.when({ method: "stored" }, (stored) => Effect.succeed<DaytonaAuthConfig>(stored)),
|
|
286
|
+
Match.when({ method: "oauth" }, (oauth) =>
|
|
287
|
+
Effect.gen(function* () {
|
|
288
|
+
if (!OAuthClient.usesCurrentClient(toOAuthCredentials(oauth))) {
|
|
289
|
+
return yield* configureOAuth(profileName);
|
|
290
|
+
}
|
|
291
|
+
// Refresh with the token stored now, read under the lock.
|
|
292
|
+
const outcome = yield* withProfileCredentialsLock(
|
|
293
|
+
profileName,
|
|
294
|
+
Effect.gen(function* () {
|
|
295
|
+
const current = yield* reload ?? Effect.succeed(oauth);
|
|
296
|
+
if (current.method !== "oauth") return { type: "signIn" as const };
|
|
297
|
+
const session = toOAuthCredentials(current);
|
|
298
|
+
if (!OAuthClient.usesCurrentClient(session)) return { type: "signIn" as const };
|
|
299
|
+
const fresh = yield* OAuthClient.refresh(session);
|
|
300
|
+
const config = { ...current, ...fromOAuthCredentials(fresh) };
|
|
301
|
+
if (updateConfig === undefined) return { type: "refreshed" as const, config };
|
|
302
|
+
yield* updateConfig(config);
|
|
303
|
+
return { type: "refreshed" as const, config: undefined };
|
|
304
|
+
}),
|
|
305
|
+
).pipe(
|
|
306
|
+
Effect.catchTag(
|
|
307
|
+
[
|
|
308
|
+
"OAuthError",
|
|
309
|
+
"OAuthNetworkError",
|
|
310
|
+
"OAuthServerUnavailable",
|
|
311
|
+
"OAuthInvalidResponse",
|
|
312
|
+
],
|
|
313
|
+
() => Effect.succeed({ type: "signIn" as const }),
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
if (outcome.type === "signIn") return yield* configureOAuth(profileName);
|
|
317
|
+
yield* Interaction.success("Daytona: session refreshed.");
|
|
318
|
+
return outcome.config;
|
|
319
|
+
}),
|
|
320
|
+
),
|
|
321
|
+
Match.exhaustive,
|
|
322
|
+
Effect.mapError((e) => AuthError.make({ message: "login failed", cause: e })),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const logout = (_profileName: string, _config: DaytonaAuthConfig) => Effect.void;
|
|
326
|
+
|
|
327
|
+
const details = (
|
|
328
|
+
profileName: string,
|
|
329
|
+
config: DaytonaAuthConfig,
|
|
330
|
+
updateConfig?: (config: DaytonaAuthConfig) => Effect.Effect<void, AuthError>,
|
|
331
|
+
) =>
|
|
332
|
+
Effect.all([read(profileName, config, updateConfig), Clock.currentTimeMillis]).pipe(
|
|
333
|
+
Effect.map(([creds, now]: [DaytonaResolvedCredentials, number]): ProviderDetails => {
|
|
334
|
+
const lines = [
|
|
335
|
+
{
|
|
336
|
+
key: creds.type === "oauth" ? "accessToken" : "apiKey",
|
|
337
|
+
value: displayRedacted(creds.token),
|
|
338
|
+
},
|
|
339
|
+
{ key: "apiBaseUrl", value: creds.apiBaseUrl },
|
|
340
|
+
];
|
|
341
|
+
if (creds.organizationId !== undefined) {
|
|
342
|
+
lines.push({ key: "organizationId", value: creds.organizationId });
|
|
343
|
+
}
|
|
344
|
+
if (creds.expires !== undefined) {
|
|
345
|
+
const remaining = creds.expires - now;
|
|
346
|
+
const at = new Date(creds.expires).toISOString();
|
|
347
|
+
lines.push({
|
|
348
|
+
key: "expires",
|
|
349
|
+
value:
|
|
350
|
+
remaining <= 0
|
|
351
|
+
? `expired (${at})`
|
|
352
|
+
: `in ${Duration.format(Duration.millis(remaining))} (${at})`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
lines.push({ key: "source", value: creds.source.type });
|
|
356
|
+
return { lines };
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const readEnvironment = Effect.gen(function* () {
|
|
361
|
+
const apiKey = yield* getEnvRedacted("DAYTONA_API_KEY");
|
|
362
|
+
if (!apiKey) {
|
|
363
|
+
return yield* AuthError.make({
|
|
364
|
+
message: "Daytona CI credentials not found. Set DAYTONA_API_KEY.",
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
type: "apiKey" as const,
|
|
369
|
+
token: apiKey,
|
|
370
|
+
apiBaseUrl: (yield* getEnv("DAYTONA_API_URL")) ?? DEFAULT_API_BASE_URL,
|
|
371
|
+
organizationId: yield* getEnv("DAYTONA_ORGANIZATION_ID"),
|
|
372
|
+
source: { type: "env" as const },
|
|
373
|
+
} satisfies DaytonaResolvedCredentials;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
configSchema: DaytonaAuthConfigSchema,
|
|
378
|
+
configure,
|
|
379
|
+
configureWith,
|
|
380
|
+
configureMethods,
|
|
381
|
+
login,
|
|
382
|
+
logout,
|
|
383
|
+
details,
|
|
384
|
+
read,
|
|
385
|
+
readEnvironment,
|
|
386
|
+
environment: [
|
|
387
|
+
{
|
|
388
|
+
name: "DAYTONA_API_KEY",
|
|
389
|
+
required: true,
|
|
390
|
+
secret: true,
|
|
391
|
+
description: "Organization API key.",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "DAYTONA_API_URL",
|
|
395
|
+
required: false,
|
|
396
|
+
description: "API root for a self-hosted Daytona.",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "DAYTONA_ORGANIZATION_ID",
|
|
400
|
+
required: false,
|
|
401
|
+
description: "Organization id; only needed with a user token.",
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
};
|
|
405
|
+
}),
|
|
406
|
+
);
|
package/src/Bootstrap.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process bootstrap for a `Daytona.Sandbox` with `main` (a Node process in
|
|
3
|
+
* the sandbox serving the bundled program). The generated entry imports this module
|
|
4
|
+
* and the user's `main`, nothing else — see `alchemy/Runtime/Bootstrap`
|
|
5
|
+
* for why.
|
|
6
|
+
*/
|
|
7
|
+
import { NodeServices } from "@effect/platform-node";
|
|
8
|
+
import { NodeHttpServer } from "alchemy/Http";
|
|
9
|
+
import { reifyBoundConfigProvider } from "alchemy/Runtime";
|
|
10
|
+
import {
|
|
11
|
+
entrypointLayer,
|
|
12
|
+
resolveProgram,
|
|
13
|
+
runProcess,
|
|
14
|
+
stackFromEnv,
|
|
15
|
+
} from "alchemy/Runtime/Bootstrap";
|
|
16
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
17
|
+
import * as SecureRandom from "effect/crypto/SecureRandom";
|
|
18
|
+
import * as Effect from "effect/Effect";
|
|
19
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
20
|
+
import * as Layer from "effect/Layer";
|
|
21
|
+
import * as Logger from "effect/Logger";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the bundled program (the runners registered via `host.run` /
|
|
25
|
+
* serve) and run it with a Node HTTP server bound to `PORT` on every
|
|
26
|
+
* interface, so the sandbox's preview proxy reaches the `{ fetch }`
|
|
27
|
+
* handler and `host.run` loops stay alive.
|
|
28
|
+
*/
|
|
29
|
+
export const bootstrap = (entrypoint: unknown): Promise<void> => {
|
|
30
|
+
const platform = Layer.mergeAll(
|
|
31
|
+
NodeServices.layer,
|
|
32
|
+
FetchHttpClient.layer,
|
|
33
|
+
SecureRandom.layerWebCrypto,
|
|
34
|
+
Logger.layer([Logger.consolePretty()]),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const program = resolveProgram("program").pipe(
|
|
38
|
+
Effect.provide(
|
|
39
|
+
entrypointLayer(entrypoint).pipe(
|
|
40
|
+
Layer.provideMerge(stackFromEnv),
|
|
41
|
+
Layer.provideMerge(NodeHttpServer({ hostname: "0.0.0.0" })),
|
|
42
|
+
Layer.provideMerge(platform),
|
|
43
|
+
Layer.provideMerge(
|
|
44
|
+
Layer.succeed(
|
|
45
|
+
ConfigProvider.ConfigProvider,
|
|
46
|
+
reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env),
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
Effect.scoped,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return runProcess("Daytona service", program);
|
|
55
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
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 at deploy, from any host: run
|
|
8
|
+
* commands, read and write files, use git, open previews, fork it or
|
|
9
|
+
* snapshot it. The handle has the same methods as the
|
|
10
|
+
* {@link DaytonaSandbox} that {@link CreateSandbox} returns; its
|
|
11
|
+
* `sandboxId`, `name` and `toolboxProxyUrl` are Effects, resolved at
|
|
12
|
+
* runtime.
|
|
13
|
+
*
|
|
14
|
+
* The host gets its own Daytona API key with `write:sandboxes` and
|
|
15
|
+
* `delete:sandboxes`; Daytona checks permissions per kind of object, so
|
|
16
|
+
* the key can reach other sandboxes in the organization too.
|
|
17
|
+
*
|
|
18
|
+
* ### Running Commands in a Dev Box
|
|
19
|
+
* **Example:** Pull and test on request
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const devBox = yield* Daytona.ConnectSandbox(DevBox);
|
|
22
|
+
* return {
|
|
23
|
+
* fetch: Effect.gen(function* () {
|
|
24
|
+
* yield* devBox.startSandbox();
|
|
25
|
+
* const test = yield* devBox.exec("git pull && pnpm test", { cwd: "/home/daytona/repo" });
|
|
26
|
+
* return yield* HttpServerResponse.json(test);
|
|
27
|
+
* }),
|
|
28
|
+
* };
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* ### Putting a Worker in Front of a Port
|
|
32
|
+
* **Example:** Proxy every request to the dev server on port 3000
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const devBox = yield* Daytona.ConnectSandbox(DevBox);
|
|
35
|
+
* return {
|
|
36
|
+
* fetch: Effect.gen(function* () {
|
|
37
|
+
* const port = yield* devBox.getTcpPort(3000);
|
|
38
|
+
* return yield* port.fetch(yield* HttpServerRequest);
|
|
39
|
+
* }),
|
|
40
|
+
* };
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* ### Streaming a Terminal
|
|
44
|
+
* Terminals, code interpreter runs and followed logs stream over a
|
|
45
|
+
* WebSocket, so they work from a Worker too.
|
|
46
|
+
*
|
|
47
|
+
* **Example:** Run a script in a terminal and stream its output
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const output = yield* Effect.scoped(
|
|
50
|
+
* Effect.gen(function* () {
|
|
51
|
+
* const pty = yield* devBox.process.createPty({ id: "build" });
|
|
52
|
+
* yield* pty.write("pnpm build; exit\n");
|
|
53
|
+
* return yield* pty.output.pipe(Stream.decodeText(), Stream.mkString);
|
|
54
|
+
* }),
|
|
55
|
+
* );
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* ### Forking a Dev Box
|
|
59
|
+
* Daytona forks `linux-vm` sandboxes; a container's fork fails with
|
|
60
|
+
* `ForkNotSupported`.
|
|
61
|
+
*
|
|
62
|
+
* **Example:** Fork it per pull request
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const fork = yield* devBox.forkSandbox({ name: `pr-${number}` });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @binding
|
|
68
|
+
* @product Sandbox
|
|
69
|
+
*/
|
|
70
|
+
export interface ConnectSandbox extends Binding.Service<
|
|
71
|
+
ConnectSandbox,
|
|
72
|
+
"Daytona.ConnectSandbox",
|
|
73
|
+
(sandbox: Sandbox) => Effect.Effect<ConnectedSandbox>
|
|
74
|
+
> {}
|
|
75
|
+
|
|
76
|
+
export const ConnectSandbox = Binding.Service<ConnectSandbox>("Daytona.ConnectSandbox");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import { ConnectSandbox } from "./ConnectSandbox.ts";
|
|
4
|
+
import { makeAccess } from "./DaytonaBindings.ts";
|
|
5
|
+
import type { Sandbox } from "./Sandbox.ts";
|
|
6
|
+
import { makeConnectedSandbox } from "./SandboxClient.ts";
|
|
7
|
+
|
|
8
|
+
/** {@link ConnectSandbox} over Daytona's HTTP API; works on any host. */
|
|
9
|
+
export const ConnectSandboxHttp = Layer.effect(
|
|
10
|
+
ConnectSandbox,
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const access = yield* makeAccess;
|
|
13
|
+
return Effect.fn(function* (sandbox: Sandbox) {
|
|
14
|
+
const { authorize, organizationId } = yield* access(`${sandbox.LogicalId}Connect`, [
|
|
15
|
+
"write:sandboxes",
|
|
16
|
+
"delete:sandboxes",
|
|
17
|
+
]);
|
|
18
|
+
const sandboxId = yield* sandbox.sandboxId;
|
|
19
|
+
const name = yield* sandbox.name;
|
|
20
|
+
const toolboxProxyUrl = yield* sandbox.toolboxProxyUrl;
|
|
21
|
+
// The sandbox's identity resolves at runtime, on each call.
|
|
22
|
+
const identity = Effect.all({ id: sandboxId, name, toolboxProxyUrl });
|
|
23
|
+
return makeConnectedSandbox(identity, organizationId, authorize);
|
|
24
|
+
});
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type * as sandboxes from "@distilled.cloud/daytona/sandbox";
|
|
2
|
+
import * as Binding from "alchemy/Binding";
|
|
3
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
4
|
+
import type * as Effect from "effect/Effect";
|
|
5
|
+
import type { StateFailed, StateTimeout } from "./internal/Wait.ts";
|
|
6
|
+
import type { VolumeMount } from "./MountVolume.ts";
|
|
7
|
+
import type { SandboxInfo } from "./Sandbox.ts";
|
|
8
|
+
import type { DaytonaSandbox } from "./SandboxClient.ts";
|
|
9
|
+
import type { Snapshot } from "./Snapshot.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `create` input: Daytona's own create request, with volumes as `mounts`.
|
|
13
|
+
* `snapshot` defaults to the binding's base; labels are merged with the
|
|
14
|
+
* binding's owner label.
|
|
15
|
+
*/
|
|
16
|
+
export type CreateSandboxInput = Omit<
|
|
17
|
+
sandboxes.CreateSandboxRequest,
|
|
18
|
+
"xDaytonaOrganizationID" | "volumes"
|
|
19
|
+
> & {
|
|
20
|
+
/**
|
|
21
|
+
* Volumes to mount: `MountVolume` references, or entries naming a
|
|
22
|
+
* `CreateVolume` handle (`{ volume: home, path: "/home/daytona/work" }`).
|
|
23
|
+
*/
|
|
24
|
+
readonly mounts?: ReadonlyArray<VolumeMount>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** `list` filters: Daytona's own, with `labels` as an object. */
|
|
28
|
+
export type ListSandboxesInput = Omit<
|
|
29
|
+
sandboxes.ListSandboxesRequest,
|
|
30
|
+
"xDaytonaOrganizationID" | "labels" | "cursor"
|
|
31
|
+
> & { labels?: Record<string, string> };
|
|
32
|
+
|
|
33
|
+
export interface CreateSandboxClient {
|
|
34
|
+
/**
|
|
35
|
+
* Create a sandbox and wait until it has started. Returns its handle.
|
|
36
|
+
*/
|
|
37
|
+
create(
|
|
38
|
+
input?: CreateSandboxInput,
|
|
39
|
+
): Effect.Effect<
|
|
40
|
+
DaytonaSandbox,
|
|
41
|
+
sandboxes.CreateSandboxError | sandboxes.GetSandboxError | StateTimeout | StateFailed,
|
|
42
|
+
RuntimeContext
|
|
43
|
+
>;
|
|
44
|
+
/** The sandbox with this id or name, or `undefined`. */
|
|
45
|
+
get(
|
|
46
|
+
idOrName: string,
|
|
47
|
+
): Effect.Effect<DaytonaSandbox | undefined, sandboxes.GetSandboxError, RuntimeContext>;
|
|
48
|
+
/** Sandboxes matching the filters (by default, the ones from the binding's base). */
|
|
49
|
+
list(
|
|
50
|
+
input?: ListSandboxesInput,
|
|
51
|
+
): Effect.Effect<SandboxInfo[], sandboxes.ListSandboxesError, RuntimeContext>;
|
|
52
|
+
/** Delete a sandbox. Deleting a missing one succeeds. */
|
|
53
|
+
delete(idOrName: string): Effect.Effect<void, sandboxes.DeleteSandboxError, RuntimeContext>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create, find and delete Daytona sandboxes at runtime, from a Worker,
|
|
58
|
+
* Lambda or any other host: a sandbox per request, per user or per task.
|
|
59
|
+
* Pass a {@link Snapshot} to create every sandbox from it (a warm base),
|
|
60
|
+
* or nothing to pick the snapshot per call.
|
|
61
|
+
*
|
|
62
|
+
* The host gets its own Daytona API key, with only `write:sandboxes` and
|
|
63
|
+
* `delete:sandboxes`. Every sandbox it creates is labeled with the
|
|
64
|
+
* stack, stage and host as Daytona labels (`alchemy.stack`,
|
|
65
|
+
* `alchemy.stage`, `alchemy.id`; Daytona doesn't allow `::` in a label
|
|
66
|
+
* key), on top of your own labels. They are not deleted when the stack is
|
|
67
|
+
* destroyed: list them by those labels to clean them up, e.g.
|
|
68
|
+
* `sandboxes.list({ labels: { "alchemy.id": "Api" } })`.
|
|
69
|
+
* Each sandbox comes back as a {@link DaytonaSandbox} handle with the whole
|
|
70
|
+
* toolbox: processes, files, git, LSP, previews, forks and snapshots.
|
|
71
|
+
*
|
|
72
|
+
* ### Sandbox per Request
|
|
73
|
+
* **Example:** Run a command in a fresh sandbox and delete it
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const sandboxes = yield* Daytona.CreateSandbox(Base);
|
|
76
|
+
* return {
|
|
77
|
+
* fetch: Effect.gen(function* () {
|
|
78
|
+
* const sandbox = yield* Effect.acquireRelease(
|
|
79
|
+
* sandboxes.create({ autoDeleteInterval: 0 }),
|
|
80
|
+
* (s) => sandboxes.delete(s.sandboxId).pipe(Effect.ignore),
|
|
81
|
+
* );
|
|
82
|
+
* const ls = yield* sandbox.exec("ls -la");
|
|
83
|
+
* return yield* HttpServerResponse.json(ls);
|
|
84
|
+
* }),
|
|
85
|
+
* };
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* ### Sandbox per User
|
|
89
|
+
* **Example:** Find the user's sandbox by name, or create it
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const sandbox =
|
|
92
|
+
* (yield* sandboxes.get(`dev-${user}`)) ??
|
|
93
|
+
* (yield* sandboxes.create({ name: `dev-${user}`, labels: { user } }));
|
|
94
|
+
* yield* sandbox.startSandbox();
|
|
95
|
+
* yield* sandbox.git.cloneRepository({ url: repoUrl, path: "/home/daytona/repo" });
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @binding
|
|
99
|
+
* @product Sandbox
|
|
100
|
+
*/
|
|
101
|
+
export interface CreateSandbox extends Binding.Service<
|
|
102
|
+
CreateSandbox,
|
|
103
|
+
"Daytona.CreateSandbox",
|
|
104
|
+
(base?: Snapshot) => Effect.Effect<CreateSandboxClient>
|
|
105
|
+
> {}
|
|
106
|
+
|
|
107
|
+
export const CreateSandbox = Binding.Service<CreateSandbox>("Daytona.CreateSandbox");
|