@alchemy.run/daytona 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.
Files changed (274) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +76 -4
  3. package/lib/ApiKey.d.ts +99 -0
  4. package/lib/ApiKey.d.ts.map +1 -0
  5. package/lib/ApiKey.js +152 -0
  6. package/lib/ApiKey.js.map +1 -0
  7. package/lib/AuthProvider.d.ts +55 -0
  8. package/lib/AuthProvider.d.ts.map +1 -0
  9. package/lib/AuthProvider.js +280 -0
  10. package/lib/AuthProvider.js.map +1 -0
  11. package/lib/Bootstrap.d.ts +8 -0
  12. package/lib/Bootstrap.d.ts.map +1 -0
  13. package/lib/Bootstrap.js +28 -0
  14. package/lib/Bootstrap.js.map +1 -0
  15. package/lib/ConnectSandbox.d.ts +72 -0
  16. package/lib/ConnectSandbox.d.ts.map +1 -0
  17. package/lib/ConnectSandbox.js +3 -0
  18. package/lib/ConnectSandbox.js.map +1 -0
  19. package/lib/ConnectSandboxHttp.d.ts +5 -0
  20. package/lib/ConnectSandboxHttp.d.ts.map +1 -0
  21. package/lib/ConnectSandboxHttp.js +22 -0
  22. package/lib/ConnectSandboxHttp.js.map +1 -0
  23. package/lib/Contributions.d.ts +7 -0
  24. package/lib/Contributions.d.ts.map +1 -0
  25. package/lib/Contributions.js +6 -0
  26. package/lib/Contributions.js.map +1 -0
  27. package/lib/CreateSandbox.d.ts +86 -0
  28. package/lib/CreateSandbox.d.ts.map +1 -0
  29. package/lib/CreateSandbox.js +3 -0
  30. package/lib/CreateSandbox.js.map +1 -0
  31. package/lib/CreateSandboxHttp.d.ts +5 -0
  32. package/lib/CreateSandboxHttp.d.ts.map +1 -0
  33. package/lib/CreateSandboxHttp.js +65 -0
  34. package/lib/CreateSandboxHttp.js.map +1 -0
  35. package/lib/CreateSecret.d.ts +56 -0
  36. package/lib/CreateSecret.d.ts.map +1 -0
  37. package/lib/CreateSecret.js +3 -0
  38. package/lib/CreateSecret.js.map +1 -0
  39. package/lib/CreateSecretHttp.d.ts +5 -0
  40. package/lib/CreateSecretHttp.d.ts.map +1 -0
  41. package/lib/CreateSecretHttp.js +39 -0
  42. package/lib/CreateSecretHttp.js.map +1 -0
  43. package/lib/CreateSnapshot.d.ts +80 -0
  44. package/lib/CreateSnapshot.d.ts.map +1 -0
  45. package/lib/CreateSnapshot.js +3 -0
  46. package/lib/CreateSnapshot.js.map +1 -0
  47. package/lib/CreateSnapshotHttp.d.ts +5 -0
  48. package/lib/CreateSnapshotHttp.d.ts.map +1 -0
  49. package/lib/CreateSnapshotHttp.js +47 -0
  50. package/lib/CreateSnapshotHttp.js.map +1 -0
  51. package/lib/CreateVolume.d.ts +81 -0
  52. package/lib/CreateVolume.d.ts.map +1 -0
  53. package/lib/CreateVolume.js +10 -0
  54. package/lib/CreateVolume.js.map +1 -0
  55. package/lib/CreateVolumeHttp.d.ts +5 -0
  56. package/lib/CreateVolumeHttp.d.ts.map +1 -0
  57. package/lib/CreateVolumeHttp.js +31 -0
  58. package/lib/CreateVolumeHttp.js.map +1 -0
  59. package/lib/Credentials.d.ts +9 -0
  60. package/lib/Credentials.d.ts.map +1 -0
  61. package/lib/Credentials.js +18 -0
  62. package/lib/Credentials.js.map +1 -0
  63. package/lib/DaytonaBindings.d.ts +51 -0
  64. package/lib/DaytonaBindings.d.ts.map +1 -0
  65. package/lib/DaytonaBindings.js +61 -0
  66. package/lib/DaytonaBindings.js.map +1 -0
  67. package/lib/IdentityProvider.d.ts +80 -0
  68. package/lib/IdentityProvider.d.ts.map +1 -0
  69. package/lib/IdentityProvider.js +111 -0
  70. package/lib/IdentityProvider.js.map +1 -0
  71. package/lib/MountVolume.d.ts +86 -0
  72. package/lib/MountVolume.d.ts.map +1 -0
  73. package/lib/MountVolume.js +27 -0
  74. package/lib/MountVolume.js.map +1 -0
  75. package/lib/OAuthClient.d.ts +14 -0
  76. package/lib/OAuthClient.d.ts.map +1 -0
  77. package/lib/OAuthClient.js +23 -0
  78. package/lib/OAuthClient.js.map +1 -0
  79. package/lib/Organization.d.ts +40 -0
  80. package/lib/Organization.d.ts.map +1 -0
  81. package/lib/Organization.js +80 -0
  82. package/lib/Organization.js.map +1 -0
  83. package/lib/OrganizationInvitation.d.ts +70 -0
  84. package/lib/OrganizationInvitation.d.ts.map +1 -0
  85. package/lib/OrganizationInvitation.js +109 -0
  86. package/lib/OrganizationInvitation.js.map +1 -0
  87. package/lib/OrganizationMember.d.ts +86 -0
  88. package/lib/OrganizationMember.d.ts.map +1 -0
  89. package/lib/OrganizationMember.js +109 -0
  90. package/lib/OrganizationMember.js.map +1 -0
  91. package/lib/OrganizationOtelConfig.d.ts +59 -0
  92. package/lib/OrganizationOtelConfig.d.ts.map +1 -0
  93. package/lib/OrganizationOtelConfig.js +87 -0
  94. package/lib/OrganizationOtelConfig.js.map +1 -0
  95. package/lib/OrganizationRole.d.ts +66 -0
  96. package/lib/OrganizationRole.d.ts.map +1 -0
  97. package/lib/OrganizationRole.js +106 -0
  98. package/lib/OrganizationRole.js.map +1 -0
  99. package/lib/OrganizationSettings.d.ts +76 -0
  100. package/lib/OrganizationSettings.d.ts.map +1 -0
  101. package/lib/OrganizationSettings.js +107 -0
  102. package/lib/OrganizationSettings.js.map +1 -0
  103. package/lib/Providers.d.ts +14 -0
  104. package/lib/Providers.d.ts.map +1 -0
  105. package/lib/Providers.js +53 -0
  106. package/lib/Providers.js.map +1 -0
  107. package/lib/ReadAuditLogs.d.ts +30 -0
  108. package/lib/ReadAuditLogs.d.ts.map +1 -0
  109. package/lib/ReadAuditLogs.js +17 -0
  110. package/lib/ReadAuditLogs.js.map +1 -0
  111. package/lib/ReadOrganization.d.ts +39 -0
  112. package/lib/ReadOrganization.d.ts.map +1 -0
  113. package/lib/ReadOrganization.js +39 -0
  114. package/lib/ReadOrganization.js.map +1 -0
  115. package/lib/ReadUsage.d.ts +31 -0
  116. package/lib/ReadUsage.d.ts.map +1 -0
  117. package/lib/ReadUsage.js +17 -0
  118. package/lib/ReadUsage.js.map +1 -0
  119. package/lib/Region.d.ts +78 -0
  120. package/lib/Region.d.ts.map +1 -0
  121. package/lib/Region.js +169 -0
  122. package/lib/Region.js.map +1 -0
  123. package/lib/Registry.d.ts +84 -0
  124. package/lib/Registry.d.ts.map +1 -0
  125. package/lib/Registry.js +97 -0
  126. package/lib/Registry.js.map +1 -0
  127. package/lib/Runner.d.ts +66 -0
  128. package/lib/Runner.d.ts.map +1 -0
  129. package/lib/Runner.js +116 -0
  130. package/lib/Runner.js.map +1 -0
  131. package/lib/Sandbox.d.ts +376 -0
  132. package/lib/Sandbox.d.ts.map +1 -0
  133. package/lib/Sandbox.js +565 -0
  134. package/lib/Sandbox.js.map +1 -0
  135. package/lib/SandboxClient.d.ts +258 -0
  136. package/lib/SandboxClient.d.ts.map +1 -0
  137. package/lib/SandboxClient.js +233 -0
  138. package/lib/SandboxClient.js.map +1 -0
  139. package/lib/Secret.d.ts +93 -0
  140. package/lib/Secret.d.ts.map +1 -0
  141. package/lib/Secret.js +130 -0
  142. package/lib/Secret.js.map +1 -0
  143. package/lib/Snapshot.d.ts +327 -0
  144. package/lib/Snapshot.d.ts.map +1 -0
  145. package/lib/Snapshot.js +447 -0
  146. package/lib/Snapshot.js.map +1 -0
  147. package/lib/Volume.d.ts +99 -0
  148. package/lib/Volume.d.ts.map +1 -0
  149. package/lib/Volume.js +152 -0
  150. package/lib/Volume.js.map +1 -0
  151. package/lib/WarmPool.d.ts +61 -0
  152. package/lib/WarmPool.d.ts.map +1 -0
  153. package/lib/WarmPool.js +81 -0
  154. package/lib/WarmPool.js.map +1 -0
  155. package/lib/Webhooks.d.ts +58 -0
  156. package/lib/Webhooks.d.ts.map +1 -0
  157. package/lib/Webhooks.js +75 -0
  158. package/lib/Webhooks.js.map +1 -0
  159. package/lib/index.d.ts +37 -0
  160. package/lib/index.d.ts.map +1 -0
  161. package/lib/index.js +36 -0
  162. package/lib/index.js.map +1 -0
  163. package/lib/internal/BuildContext.d.ts +16 -0
  164. package/lib/internal/BuildContext.d.ts.map +1 -0
  165. package/lib/internal/BuildContext.js +50 -0
  166. package/lib/internal/BuildContext.js.map +1 -0
  167. package/lib/internal/Declared.d.ts +29 -0
  168. package/lib/internal/Declared.d.ts.map +1 -0
  169. package/lib/internal/Declared.js +31 -0
  170. package/lib/internal/Declared.js.map +1 -0
  171. package/lib/internal/Derived.d.ts +40 -0
  172. package/lib/internal/Derived.d.ts.map +1 -0
  173. package/lib/internal/Derived.js +64 -0
  174. package/lib/internal/Derived.js.map +1 -0
  175. package/lib/internal/Errors.d.ts +4 -0
  176. package/lib/internal/Errors.d.ts.map +1 -0
  177. package/lib/internal/Errors.js +1 -0
  178. package/lib/internal/Errors.js.map +1 -0
  179. package/lib/internal/Labels.d.ts +20 -0
  180. package/lib/internal/Labels.d.ts.map +1 -0
  181. package/lib/internal/Labels.js +22 -0
  182. package/lib/internal/Labels.js.map +1 -0
  183. package/lib/internal/Names.d.ts +18 -0
  184. package/lib/internal/Names.d.ts.map +1 -0
  185. package/lib/internal/Names.js +25 -0
  186. package/lib/internal/Names.js.map +1 -0
  187. package/lib/internal/Organization.d.ts +27 -0
  188. package/lib/internal/Organization.d.ts.map +1 -0
  189. package/lib/internal/Organization.js +32 -0
  190. package/lib/internal/Organization.js.map +1 -0
  191. package/lib/internal/Process.d.ts +14 -0
  192. package/lib/internal/Process.d.ts.map +1 -0
  193. package/lib/internal/Process.js +15 -0
  194. package/lib/internal/Process.js.map +1 -0
  195. package/lib/internal/Program.d.ts +83 -0
  196. package/lib/internal/Program.d.ts.map +1 -0
  197. package/lib/internal/Program.js +169 -0
  198. package/lib/internal/Program.js.map +1 -0
  199. package/lib/internal/Ref.d.ts +14 -0
  200. package/lib/internal/Ref.d.ts.map +1 -0
  201. package/lib/internal/Ref.js +7 -0
  202. package/lib/internal/Ref.js.map +1 -0
  203. package/lib/internal/Scoped.d.ts +31 -0
  204. package/lib/internal/Scoped.d.ts.map +1 -0
  205. package/lib/internal/Scoped.js +48 -0
  206. package/lib/internal/Scoped.js.map +1 -0
  207. package/lib/internal/Tar.d.ts +13 -0
  208. package/lib/internal/Tar.d.ts.map +1 -0
  209. package/lib/internal/Tar.js +62 -0
  210. package/lib/internal/Tar.js.map +1 -0
  211. package/lib/internal/Toolbox.d.ts +11 -0
  212. package/lib/internal/Toolbox.d.ts.map +1 -0
  213. package/lib/internal/Toolbox.js +20 -0
  214. package/lib/internal/Toolbox.js.map +1 -0
  215. package/lib/internal/Wait.d.ts +49 -0
  216. package/lib/internal/Wait.d.ts.map +1 -0
  217. package/lib/internal/Wait.js +57 -0
  218. package/lib/internal/Wait.js.map +1 -0
  219. package/package.json +64 -3
  220. package/src/ApiKey.ts +221 -0
  221. package/src/AuthProvider.ts +406 -0
  222. package/src/Bootstrap.ts +55 -0
  223. package/src/ConnectSandbox.ts +76 -0
  224. package/src/ConnectSandboxHttp.ts +26 -0
  225. package/src/Contributions.ts +9 -0
  226. package/src/CreateSandbox.ts +107 -0
  227. package/src/CreateSandboxHttp.ts +97 -0
  228. package/src/CreateSecret.ts +73 -0
  229. package/src/CreateSecretHttp.ts +58 -0
  230. package/src/CreateSnapshot.ts +133 -0
  231. package/src/CreateSnapshotHttp.ts +77 -0
  232. package/src/CreateVolume.ts +104 -0
  233. package/src/CreateVolumeHttp.ts +50 -0
  234. package/src/Credentials.ts +37 -0
  235. package/src/DaytonaBindings.ts +111 -0
  236. package/src/IdentityProvider.ts +181 -0
  237. package/src/MountVolume.ts +122 -0
  238. package/src/OAuthClient.ts +27 -0
  239. package/src/Organization.ts +121 -0
  240. package/src/OrganizationInvitation.ts +174 -0
  241. package/src/OrganizationMember.ts +174 -0
  242. package/src/OrganizationOtelConfig.ts +138 -0
  243. package/src/OrganizationRole.ts +167 -0
  244. package/src/OrganizationSettings.ts +187 -0
  245. package/src/Providers.ts +93 -0
  246. package/src/ReadAuditLogs.ts +50 -0
  247. package/src/ReadOrganization.ts +83 -0
  248. package/src/ReadUsage.ts +51 -0
  249. package/src/Region.ts +226 -0
  250. package/src/Registry.ts +164 -0
  251. package/src/Runner.ts +166 -0
  252. package/src/Sandbox.ts +927 -0
  253. package/src/SandboxClient.ts +625 -0
  254. package/src/Secret.ts +203 -0
  255. package/src/Snapshot.ts +685 -0
  256. package/src/Volume.ts +229 -0
  257. package/src/WarmPool.ts +128 -0
  258. package/src/Webhooks.ts +121 -0
  259. package/src/globals.d.ts +2 -0
  260. package/src/index.ts +46 -0
  261. package/src/internal/BuildContext.ts +62 -0
  262. package/src/internal/Declared.ts +80 -0
  263. package/src/internal/Derived.ts +97 -0
  264. package/src/internal/Errors.ts +4 -0
  265. package/src/internal/Labels.ts +39 -0
  266. package/src/internal/Names.ts +37 -0
  267. package/src/internal/Organization.ts +40 -0
  268. package/src/internal/Process.ts +25 -0
  269. package/src/internal/Program.ts +239 -0
  270. package/src/internal/Ref.ts +15 -0
  271. package/src/internal/Scoped.ts +102 -0
  272. package/src/internal/Tar.ts +72 -0
  273. package/src/internal/Toolbox.ts +27 -0
  274. 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
+ );
@@ -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,9 @@
1
+ import * as Contributions from "alchemy/Contributions";
2
+
3
+ export default Contributions.make({
4
+ name: "daytona",
5
+ auth: Contributions.lazy(
6
+ () => import("./AuthProvider.ts"),
7
+ (module) => module.DaytonaAuth,
8
+ ),
9
+ });
@@ -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");