@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,97 @@
1
+ import * as BuildPlan from "@alchemy.run/docker/BuildPlan";
2
+ import * as DockerfileParser from "@alchemy.run/docker/DockerfileParser";
3
+ import * as config from "@distilled.cloud/daytona/config";
4
+ import * as snapshots from "@distilled.cloud/daytona/snapshots";
5
+ import {
6
+ checkMountSupport,
7
+ collectMountCapabilities,
8
+ mountInstallLines,
9
+ type MountCapability,
10
+ } from "alchemy/Mount";
11
+ import * as Effect from "effect/Effect";
12
+ import {
13
+ buildSnapshot,
14
+ capabilities,
15
+ deleteSnapshot,
16
+ getSnapshot,
17
+ waitForSnapshot,
18
+ } from "../Snapshot.ts";
19
+
20
+ /**
21
+ * INTERNAL — the snapshot a sandbox boots from when its bindings bind
22
+ * `alchemy/Mount` capabilities: `FROM` the base snapshot's image plus the
23
+ * capabilities' install lines, built on Daytona. It is named from the
24
+ * sandbox and a hash of its Dockerfile, belongs to the sandbox, and is
25
+ * deleted with it.
26
+ */
27
+
28
+ /** What Daytona sandboxes provide to mount clients (checked live 2026-11-04). */
29
+ export const MOUNT_SUPPORT = {
30
+ // `/dev/fuse` exists and the sandbox has CAP_SYS_ADMIN.
31
+ fuse: true,
32
+ // No NFS client is installed, and `mount -t nfs` was never verified.
33
+ nfs: false,
34
+ } as const;
35
+
36
+ /** A binding entry that is a mount capability (not a volume to mount). */
37
+ export const isMountCapability = (entry: unknown): entry is MountCapability =>
38
+ typeof entry === "object" && entry !== null && "install" in entry && "id" in entry;
39
+
40
+ /** The capabilities bound onto a sandbox, de-duplicated in a stable order. */
41
+ export const capabilitiesOf = (
42
+ bindings: ReadonlyArray<{ readonly data?: { readonly mounts?: ReadonlyArray<unknown> } }>,
43
+ ) =>
44
+ collectMountCapabilities(
45
+ bindings.map((binding) => ({
46
+ data: { mounts: (binding.data?.mounts ?? []).filter(isMountCapability) },
47
+ })),
48
+ );
49
+
50
+ /** The capabilities' install lines; run as root, as `alchemy/Mount` specifies. */
51
+ export const installLinesOf = (caps: ReadonlyArray<MountCapability>) =>
52
+ caps.length === 0 ? [] : ["USER root", ...mountInstallLines(caps)];
53
+
54
+ /** Fail the plan when a capability needs what a sandbox can't provide. */
55
+ export const checkSupport = (caps: ReadonlyArray<MountCapability>) => {
56
+ const unsupported = checkMountSupport("Daytona.Sandbox", caps, MOUNT_SUPPORT);
57
+ return unsupported ? Effect.fail(unsupported) : Effect.void;
58
+ };
59
+
60
+ /** The image a snapshot (or, without one, Daytona's default) was made from. */
61
+ const baseImageOf = Effect.fn(function* (snapshot: string | undefined) {
62
+ const name = snapshot ?? (yield* config.getConfigControllerConfig({})).defaultSnapshot;
63
+ return (yield* snapshots.getSnapshot({ id: name })).imageName;
64
+ });
65
+
66
+ /**
67
+ * Build (or reuse) the derived snapshot for a sandbox and return its name.
68
+ */
69
+ export const ensureDerivedSnapshot = Effect.fn(function* (input: {
70
+ readonly sandboxName: string;
71
+ readonly snapshot: string | undefined;
72
+ readonly caps: ReadonlyArray<MountCapability>;
73
+ readonly note: (message: string) => Effect.Effect<void>;
74
+ }) {
75
+ const baseImage = yield* baseImageOf(input.snapshot);
76
+ const plan = yield* BuildPlan.plan(
77
+ { dockerfile: { content: `FROM ${baseImage}\n` } },
78
+ { capabilities, install: installLinesOf(input.caps) },
79
+ );
80
+ const name = `${input.sandboxName.slice(0, 40)}-${plan.hash.slice(0, 12)}`.toLowerCase();
81
+ let existing = yield* getSnapshot(name);
82
+ // A failed build is rebuilt; the name is free once the old one is gone.
83
+ if (existing !== undefined && ["error", "build_failed", "removing"].includes(existing.state)) {
84
+ yield* deleteSnapshot(existing.id);
85
+ existing = undefined;
86
+ }
87
+ if (existing === undefined) {
88
+ yield* buildSnapshot({
89
+ name,
90
+ source: { dockerfile: DockerfileParser.render(plan.dockerfile), run: [] },
91
+ labels: {},
92
+ note: input.note,
93
+ });
94
+ }
95
+ yield* waitForSnapshot(name);
96
+ return name;
97
+ });
@@ -0,0 +1,4 @@
1
+ import type * as process from "@distilled.cloud/daytona/process";
2
+
3
+ /** Errors a toolbox command run can fail with. */
4
+ export type ProcessError = process.ExecuteCommandError;
@@ -0,0 +1,39 @@
1
+ import { createInternalTags, hasTags } from "alchemy/Tags";
2
+ import * as Effect from "effect/Effect";
3
+
4
+ /**
5
+ * Daytona sandbox labels carry alchemy's ownership tags. Tag keys
6
+ * (`alchemy::stack`) are written as `alchemy.stack` to stay within the
7
+ * conservative key alphabet every label API accepts.
8
+ */
9
+ export const ALCHEMY_LABEL_PREFIX = "alchemy.";
10
+ const TAG_PREFIX = "alchemy::";
11
+
12
+ export const toLabelKey = (key: string) =>
13
+ key.startsWith(TAG_PREFIX) ? `${ALCHEMY_LABEL_PREFIX}${key.slice(TAG_PREFIX.length)}` : key;
14
+
15
+ export const toLabels = (tags: Record<string, string> | undefined): Record<string, string> =>
16
+ Object.fromEntries(Object.entries(tags ?? {}).map(([k, v]) => [toLabelKey(k), v]));
17
+
18
+ export const createInternalLabels = Effect.fn(function* (id: string) {
19
+ return toLabels(yield* createInternalTags(id));
20
+ });
21
+
22
+ /** A label map as Daytona returns it (values typed optional). */
23
+ export type LabelMap = { readonly [key: string]: string | undefined };
24
+
25
+ /** Drop undefined values from a label map. */
26
+ export const definedLabels = (labels: LabelMap | undefined): Record<string, string> =>
27
+ Object.fromEntries(
28
+ Object.entries(labels ?? {}).filter((e): e is [string, string] => e[1] !== undefined),
29
+ );
30
+
31
+ export const hasAlchemyLabels = Effect.fn(function* (id: string, labels: LabelMap | undefined) {
32
+ return hasTags(yield* createInternalLabels(id), definedLabels(labels));
33
+ });
34
+
35
+ /** Labels without alchemy's ownership keys: what the user declared. */
36
+ export const userLabels = (labels: LabelMap | undefined): Record<string, string> =>
37
+ Object.fromEntries(
38
+ Object.entries(definedLabels(labels)).filter(([k]) => !k.startsWith(ALCHEMY_LABEL_PREFIX)),
39
+ );
@@ -0,0 +1,37 @@
1
+ import { createPhysicalName } from "alchemy/PhysicalName";
2
+ import * as Effect from "effect/Effect";
3
+
4
+ /**
5
+ * The name alchemy generates for a Daytona object when the user gives none:
6
+ * `<stack>-<id>-<stage>-<suffix>`, lowercase. Daytona volumes, secrets,
7
+ * snapshots and registries have no labels, so this name is also their
8
+ * ownership marker: only alchemy produces the random suffix, so an object
9
+ * with exactly this name was created by this logical id.
10
+ */
11
+ export const physicalName = (id: string, maxLength = 63) =>
12
+ createPhysicalName({ id, maxLength, delimiter: "-", lowercase: true });
13
+
14
+ /** The user's name, the name already in state, or the generated name. */
15
+ export const resolveName = Effect.fn(function* (
16
+ id: string,
17
+ name: string | undefined,
18
+ existing?: string,
19
+ maxLength?: number,
20
+ ) {
21
+ return name ?? existing ?? (yield* physicalName(id, maxLength));
22
+ });
23
+
24
+ /**
25
+ * Whether an object found by `name` is ours: a name in state always is; a
26
+ * user-chosen name with no state is not (adopting it needs `--adopt`); a
27
+ * generated name is.
28
+ */
29
+ export const isOwnedName = Effect.fn(function* (
30
+ id: string,
31
+ name: string,
32
+ inState: boolean,
33
+ maxLength?: number,
34
+ ) {
35
+ if (inState) return true;
36
+ return name === (yield* physicalName(id, maxLength));
37
+ });
@@ -0,0 +1,40 @@
1
+ import * as apiKeys from "@distilled.cloud/daytona/api_keys";
2
+ import { Credentials } from "@distilled.cloud/daytona/Credentials";
3
+ import * as Data from "effect/Data";
4
+ import * as Effect from "effect/Effect";
5
+ import { resolved, type Ref } from "./Ref.ts";
6
+
7
+ /** The credentials don't name an organization and the API key reports none. */
8
+ export class OrganizationUnknown extends Data.TaggedError("Daytona.OrganizationUnknown")<{}> {
9
+ override get message() {
10
+ return "Could not determine the Daytona organization: set organizationId in the profile or use an organization API key";
11
+ }
12
+ }
13
+
14
+ /**
15
+ * The organization the credentials act on: the profile's `organizationId`,
16
+ * else the organization the API key belongs to.
17
+ */
18
+ export const currentOrganizationId = Effect.gen(function* () {
19
+ const credentials = yield* Credentials;
20
+ const config = yield* credentials;
21
+ if (config.organizationId) return config.organizationId;
22
+ const key = yield* apiKeys.getCurrentApiKey({});
23
+ if (!key.organizationId) return yield* new OrganizationUnknown();
24
+ return key.organizationId;
25
+ });
26
+
27
+ /** An organization, by id. Accepts a `Daytona.Organization` or `{ organizationId }`. */
28
+ export type OrganizationRef = { readonly organizationId: string };
29
+
30
+ /** The organization a resource acts on: its `organization` prop, else the credentials'. */
31
+ export const organizationIdFor = (organization: Ref<OrganizationRef> | undefined) =>
32
+ organization === undefined
33
+ ? currentOrganizationId
34
+ : Effect.succeed(resolved(organization).organizationId);
35
+
36
+ /** Whether the props now point the resource at a different organization (a replacement). */
37
+ export const movesOrganization = (
38
+ organization: Ref<OrganizationRef> | undefined,
39
+ output: { readonly organizationId: string },
40
+ ) => Effect.map(organizationIdFor(organization), (id) => id !== output.organizationId);
@@ -0,0 +1,25 @@
1
+ import * as process from "@distilled.cloud/daytona/process";
2
+ import * as Effect from "effect/Effect";
3
+ import { inSandbox } from "./Toolbox.ts";
4
+
5
+ /**
6
+ * Run a shell command in a sandbox through the toolbox. The command is
7
+ * passed base64-encoded to `sh`, as Daytona's own SDKs do, so pipes,
8
+ * quotes and `&&` behave as in a shell.
9
+ */
10
+ export const runCommand = (
11
+ sandboxId: string,
12
+ toolboxProxyUrl: string | undefined,
13
+ request: { command: string; cwd?: string; env?: Record<string, string>; timeout?: number },
14
+ ) =>
15
+ Effect.sync(() => Buffer.from(request.command).toString("base64")).pipe(
16
+ Effect.flatMap((encoded) =>
17
+ process.executeCommand({
18
+ command: `sh -c "echo '${encoded}' | base64 -d | sh"`,
19
+ cwd: request.cwd,
20
+ envs: request.env,
21
+ timeout: request.timeout,
22
+ }),
23
+ ),
24
+ inSandbox(sandboxId, toolboxProxyUrl),
25
+ );
@@ -0,0 +1,239 @@
1
+ import * as fileSystem from "@distilled.cloud/daytona/file_system";
2
+ import * as port from "@distilled.cloud/daytona/port";
3
+ import * as process from "@distilled.cloud/daytona/process";
4
+ import * as sandboxes from "@distilled.cloud/daytona/sandbox";
5
+ import * as Toolbox from "@distilled.cloud/daytona/Toolbox";
6
+ import * as Bundle from "alchemy/Bundle/Bundle";
7
+ import { findCwdForBundle, resolveMainPath } from "alchemy/Bundle/TempRoot";
8
+ import { sha256Object } from "alchemy/Util/sha256";
9
+ import * as Data from "effect/Data";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Schedule from "effect/Schedule";
12
+ import type * as rolldown from "rolldown";
13
+ import { inSandbox } from "./Toolbox.ts";
14
+
15
+ /**
16
+ * INTERNAL — the program a `Daytona.Sandbox` with `main` runs: bundled with
17
+ * rolldown, uploaded into the sandbox, and run with Node in a session.
18
+ */
19
+
20
+ /** The program didn't come up: it exited, or never listened on its port. */
21
+ export class ProgramStartFailed extends Data.TaggedError("Daytona.ProgramStartFailed")<{
22
+ readonly message: string;
23
+ readonly stdout: string;
24
+ readonly stderr: string;
25
+ }> {}
26
+
27
+ /** What a sandbox's program is: the props that shape its bundle. */
28
+ export interface ProgramSource {
29
+ readonly main: string;
30
+ readonly handler?: string;
31
+ readonly isExternal?: boolean;
32
+ readonly build?: Bundle.BundleConfig;
33
+ }
34
+
35
+ /** The running program, as recorded in the sandbox's attributes. */
36
+ export interface ProgramState {
37
+ /** Port the program listens on. */
38
+ readonly port: number;
39
+ /** Session the process runs in. */
40
+ readonly sessionId: string;
41
+ /** The session command running the process. */
42
+ readonly commandId: string;
43
+ /** Content hash of the bundled program. */
44
+ readonly codeHash: string;
45
+ /** Hash of the process environment. */
46
+ readonly envHash: string;
47
+ }
48
+
49
+ export const DEFAULT_PORT = 3000;
50
+ const DIR = "/home/daytona/.alchemy/program";
51
+ const SESSION = "alchemy-program";
52
+
53
+ /** The generated entry: a shim importing only the bootstrap and `main`. */
54
+ const makeBootstrap =
55
+ (handler: string) =>
56
+ (importPath: string): string =>
57
+ `
58
+ import { bootstrap } from "@alchemy.run/daytona/Bootstrap";
59
+ import { ${handler} as entrypoint } from ${JSON.stringify(importPath)};
60
+
61
+ await bootstrap(entrypoint);
62
+ `;
63
+
64
+ const shellQuote = (value: unknown) => {
65
+ const text = typeof value === "string" ? value : JSON.stringify(value ?? null);
66
+ return `'${text.replaceAll("'", `'\\''`)}'`;
67
+ };
68
+
69
+ /** The process environment as a shell file, sorted so its hash is stable. */
70
+ export const renderEnv = (env: Record<string, unknown>) =>
71
+ Object.entries(env)
72
+ .filter(([, value]) => value !== undefined)
73
+ .sort(([a], [b]) => a.localeCompare(b))
74
+ .map(([key, value]) => `${key}=${shellQuote(value)}`)
75
+ .join("\n") + "\n";
76
+
77
+ /** Bundle `main` for Node, with the Daytona bootstrap around an Effect program. */
78
+ export const bundleProgram = Effect.fn(function* (source: ProgramSource) {
79
+ const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
80
+ const realMain = yield* resolveMainPath(source.main);
81
+ const cwd = yield* findCwdForBundle(realMain);
82
+ const plugins: rolldown.RolldownPluginOption = source.isExternal
83
+ ? undefined
84
+ : virtualEntryPlugin(makeBootstrap(source.handler ?? "default"));
85
+ const output = yield* Bundle.build(
86
+ {
87
+ ...source.build?.input,
88
+ input: realMain,
89
+ cwd,
90
+ platform: "node",
91
+ resolve: {
92
+ conditionNames: [...Bundle.NODE_CONDITION_NAMES],
93
+ ...source.build?.input?.resolve,
94
+ },
95
+ plugins: [source.build?.input?.plugins, plugins],
96
+ },
97
+ {
98
+ ...source.build?.output,
99
+ format: "esm",
100
+ sourcemap: source.build?.output?.sourcemap ?? false,
101
+ minify: source.build?.output?.minify ?? false,
102
+ entryFileNames: "index.mjs",
103
+ },
104
+ source.build,
105
+ );
106
+ const files = output.files.map((file) => ({
107
+ path: file.path,
108
+ content:
109
+ typeof file.content === "string" ? new TextEncoder().encode(file.content) : file.content,
110
+ }));
111
+ return { files, hash: (yield* sha256Object({ bundle: output.hash })).slice(0, 16) };
112
+ });
113
+
114
+ /** The hash of a process environment. */
115
+ export const envHashOf = (env: Record<string, unknown>) =>
116
+ Effect.map(sha256Object({ env: renderEnv(env) }), (hash) => hash.slice(0, 16));
117
+
118
+ /**
119
+ * Whether the program's process still runs. A stopped or deleted sandbox,
120
+ * or a gone session, is not running.
121
+ */
122
+ export const isProgramRunning = (sandboxId: string, state: ProgramState) =>
123
+ process.getSessionCommand({ sessionId: state.sessionId, commandId: state.commandId }).pipe(
124
+ inSandbox(sandboxId),
125
+ Effect.map((command) => command.exitCode === undefined),
126
+ Effect.orElseSucceed(() => false),
127
+ );
128
+
129
+ /**
130
+ * Upload the bundle and (re)start the process, unless the same bundle and
131
+ * environment already run. Returns the program's state and its URL.
132
+ */
133
+ export const deployProgram = Effect.fn(function* (input: {
134
+ readonly id: string;
135
+ readonly sandboxId: string;
136
+ readonly toolboxProxyUrl: string;
137
+ readonly source: ProgramSource;
138
+ readonly port: number;
139
+ readonly env: Record<string, unknown>;
140
+ readonly previous: ProgramState | undefined;
141
+ readonly note: (message: string) => Effect.Effect<void>;
142
+ }) {
143
+ const { id, sandboxId, port: programPort } = input;
144
+ const inBox = inSandbox(sandboxId, input.toolboxProxyUrl);
145
+ const envFile = renderEnv(input.env);
146
+ const envHash = yield* envHashOf(input.env);
147
+ const { files, hash } = yield* bundleProgram(input.source);
148
+
149
+ const previous = input.previous;
150
+ const running =
151
+ previous !== undefined &&
152
+ previous.codeHash === hash &&
153
+ previous.envHash === envHash &&
154
+ previous.port === programPort &&
155
+ (yield* isProgramRunning(sandboxId, previous));
156
+
157
+ let commandId = previous?.commandId ?? "";
158
+ if (!running) {
159
+ const dir = `${DIR}/${hash}`;
160
+ yield* input.note(`Uploading ${id}'s program...`);
161
+ yield* Toolbox.uploadFiles({
162
+ files: [
163
+ // Rolldown's entry chunk is `index.mjs`; other chunks keep their names.
164
+ ...files.map((file, i) => ({
165
+ path: `${dir}/${i === 0 ? "index.mjs" : file.path}`,
166
+ content: file.content,
167
+ })),
168
+ { path: `${dir}/.env`, content: envFile },
169
+ ],
170
+ }).pipe(inBox);
171
+ yield* fileSystem.setFilePermissions({ path: `${dir}/.env`, mode: "600" }).pipe(inBox);
172
+
173
+ // Replace the running process: deleting its session ends it.
174
+ yield* process.deleteSession({ sessionId: SESSION }).pipe(
175
+ inBox,
176
+ Effect.catchTag("SessionNotFound", () => Effect.void),
177
+ );
178
+ yield* process.createSession({ sessionId: SESSION }).pipe(inBox);
179
+ const started = yield* process
180
+ .sessionExecuteCommand({
181
+ sessionId: SESSION,
182
+ command: `cd ${dir} && set -a && . ./.env && set +a && exec node index.mjs`,
183
+ runAsync: true,
184
+ })
185
+ .pipe(inBox);
186
+ commandId = started.cmdId;
187
+
188
+ // Ready once the port listens; fail with the logs if the process exits.
189
+ yield* input.note(`Starting ${id}'s program on port ${programPort}...`);
190
+ const ready = yield* Effect.gen(function* () {
191
+ const command = yield* process
192
+ .getSessionCommand({ sessionId: SESSION, commandId })
193
+ .pipe(inBox);
194
+ if (command.exitCode !== undefined) return "exited" as const;
195
+ const { ports } = yield* port.getPorts({}).pipe(inBox);
196
+ return (ports ?? []).includes(programPort) ? ("listening" as const) : ("starting" as const);
197
+ }).pipe(
198
+ Effect.repeat({
199
+ schedule: Schedule.spaced("1 second"),
200
+ until: (state) => state !== "starting",
201
+ times: 60,
202
+ }),
203
+ );
204
+ if (ready !== "listening") {
205
+ const logs = yield* process
206
+ .getSessionCommandLogs({ sessionId: SESSION, commandId })
207
+ .pipe(inBox);
208
+ return yield* new ProgramStartFailed({
209
+ message:
210
+ ready === "exited"
211
+ ? `${id}'s program exited before listening on port ${programPort}`
212
+ : `${id}'s program did not listen on port ${programPort} within 60 seconds`,
213
+ stdout: logs.stdout,
214
+ stderr: logs.stderr,
215
+ });
216
+ }
217
+ // Keep only the running bundle.
218
+ yield* process
219
+ .executeCommand({
220
+ command: `find ${DIR} -mindepth 1 -maxdepth 1 ! -name ${hash} -exec rm -rf {} +`,
221
+ })
222
+ .pipe(inBox);
223
+ }
224
+
225
+ const preview = yield* sandboxes.getPortPreviewUrl({
226
+ sandboxIdOrName: sandboxId,
227
+ port: programPort,
228
+ });
229
+ return {
230
+ url: preview.url,
231
+ state: {
232
+ port: programPort,
233
+ sessionId: SESSION,
234
+ commandId,
235
+ codeHash: hash,
236
+ envHash,
237
+ } satisfies ProgramState,
238
+ };
239
+ });
@@ -0,0 +1,15 @@
1
+ import type * as Effect from "effect/Effect";
2
+ import type { Providers } from "../Providers.ts";
3
+
4
+ /**
5
+ * A prop that takes another resource, or a stub with the attributes it
6
+ * needs (`{ name }`, `{ volumeId }`). Users pass the declaration itself.
7
+ */
8
+ export type Ref<T> = T | Effect.Effect<T, never, Providers>;
9
+
10
+ /**
11
+ * A `Ref` prop as lifecycle operations see it. The engine resolves
12
+ * resource-valued props to their attributes before `diff`, `reconcile`
13
+ * and the other lifecycle operations run, so the value is always plain.
14
+ */
15
+ export const resolved = <T>(ref: Ref<T>): T => ref as T;
@@ -0,0 +1,102 @@
1
+ import type { Credentials } from "@distilled.cloud/daytona/Credentials";
2
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
3
+ import * as Effect from "effect/Effect";
4
+ import type * as HttpClient from "effect/http/HttpClient";
5
+ import * as SchemaAST from "effect/SchemaAST";
6
+
7
+ /** A generated Daytona operation: callable with its input, and an Effect. */
8
+ type Operation<I, O, E> = ((input: I) => Effect.Effect<O, E, Credentials | HttpClient.HttpClient>) &
9
+ Effect.Effect<unknown, never, unknown>;
10
+
11
+ /** Members a sandbox-scoped client fills in itself. */
12
+ export type SandboxFixed =
13
+ | "sandboxIdOrName"
14
+ | "sandboxId"
15
+ | "organizationId"
16
+ | "xDaytonaOrganizationID";
17
+
18
+ /** Members an organization-scoped client fills in itself. */
19
+ export type OrganizationFixed = "organizationId" | "xDaytonaOrganizationID";
20
+
21
+ /** One operation, with the fields the client fixes removed from its input. */
22
+ export type ScopedOperation<I, O, E, F extends PropertyKey = SandboxFixed> =
23
+ {} extends Omit<I, F>
24
+ ? (input?: Omit<I, F>) => Effect.Effect<O, E, RuntimeContext>
25
+ : (input: Omit<I, F>) => Effect.Effect<O, E, RuntimeContext>;
26
+
27
+ /**
28
+ * Every operation of a generated service module (`@distilled.cloud/daytona/process`,
29
+ * `/git`, …), re-exposed with Daytona's own parameters and typed errors,
30
+ * minus the fields the client supplies (the sandbox and organization).
31
+ */
32
+ export type ScopedModule<M, F extends PropertyKey = SandboxFixed> = {
33
+ readonly [
34
+ K in keyof M as M[K] extends Operation<any, any, any> ? K : never
35
+ ]: M[K] extends Operation<infer I, infer O, infer E> ? ScopedOperation<I, O, E, F> : never;
36
+ };
37
+
38
+ // Generated operations are plain callables, not Effects, so the name tells
39
+ // them apart: a service module exports operations camelCase and its error
40
+ // classes and schemas PascalCase.
41
+ const isOperation = (
42
+ name: string,
43
+ value: unknown,
44
+ ): value is (input: unknown) => Effect.Effect<unknown, unknown, unknown> =>
45
+ typeof value === "function" && /^[a-z]/.test(name);
46
+
47
+ /** The members an operation's input declares, read from its schema on first use. */
48
+ const inputMembers = (op: object): ReadonlySet<string> | undefined => {
49
+ if (!("input" in op)) return undefined;
50
+ const schema = op.input;
51
+ if (typeof schema !== "object" || schema === null || !("ast" in schema)) return undefined;
52
+ let ast = schema.ast as SchemaAST.AST;
53
+ while (SchemaAST.isSuspend(ast)) ast = ast.thunk();
54
+ return SchemaAST.isObjects(ast)
55
+ ? new Set(ast.propertySignatures.map((p) => String(p.name)))
56
+ : undefined;
57
+ };
58
+
59
+ /**
60
+ * Wrap every operation of `module`: merge `fixed` into each input and run
61
+ * the call through `run` (which supplies credentials and sandbox scope).
62
+ * `fixed` may be an Effect when the fields are only known at runtime (a
63
+ * sandbox declared at deploy).
64
+ */
65
+ export const scopeModule = <M extends object, F extends PropertyKey = SandboxFixed>(
66
+ module: M,
67
+ fixed: Record<string, unknown> | Effect.Effect<Record<string, unknown>, never, RuntimeContext>,
68
+ run: <A, E>(
69
+ effect: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
70
+ ) => Effect.Effect<A, E, RuntimeContext>,
71
+ ): ScopedModule<M, F> => {
72
+ const fields = Effect.isEffect(fixed) ? fixed : Effect.succeed(fixed);
73
+ return Object.fromEntries(
74
+ Object.entries(module)
75
+ .filter(([name, value]) => isOperation(name, value))
76
+ .map(([name, op]) => {
77
+ // Only the fixed fields the operation takes: the protocol sends any
78
+ // member it doesn't model as a body, which a GET can't carry.
79
+ let members: ReadonlySet<string> | undefined;
80
+ const fixedFor = (all: Record<string, unknown>) => {
81
+ members ??= inputMembers(op) ?? new Set(Object.keys(all));
82
+ return Object.fromEntries(Object.entries(all).filter(([key]) => members!.has(key)));
83
+ };
84
+ const call = op as (
85
+ input: unknown,
86
+ ) => Effect.Effect<unknown, unknown, Credentials | HttpClient.HttpClient>;
87
+ return [
88
+ name,
89
+ (input?: Record<string, unknown>) =>
90
+ Effect.flatMap(fields, (all) => run(call({ ...input, ...fixedFor(all) }))),
91
+ ];
92
+ }),
93
+ // Object.fromEntries can't produce a mapped type; the entries are exactly
94
+ // the module's operations, each wrapped with the signature ScopedModule gives it.
95
+ ) as unknown as ScopedModule<M, F>;
96
+ };
97
+
98
+ /** The named operations of a module, as a module-shaped object. */
99
+ export const pick = <M extends object, K extends keyof M>(
100
+ module: M,
101
+ names: readonly K[],
102
+ ): Pick<M, K> => Object.fromEntries(names.map((name) => [name, module[name]])) as Pick<M, K>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * A minimal POSIX ustar writer: regular files and their parent
3
+ * directories, no compression. Enough for a Docker build context.
4
+ */
5
+ export interface TarEntry {
6
+ /** `/`-separated, relative. */
7
+ readonly path: string;
8
+ readonly mode: number;
9
+ readonly bytes: Uint8Array;
10
+ }
11
+
12
+ const BLOCK = 512;
13
+ const encoder = new TextEncoder();
14
+
15
+ const octal = (value: number, width: number) => value.toString(8).padStart(width - 1, "0") + "\0";
16
+
17
+ const splitPath = (path: string): { name: string; prefix: string } => {
18
+ if (encoder.encode(path).length <= 100) return { name: path, prefix: "" };
19
+ const cut = path.lastIndexOf("/", 155);
20
+ if (cut <= 0 || encoder.encode(path.slice(cut + 1)).length > 100) {
21
+ throw new Error(`path too long for a ustar archive: ${path}`);
22
+ }
23
+ return { name: path.slice(cut + 1), prefix: path.slice(0, cut) };
24
+ };
25
+
26
+ const header = (path: string, mode: number, size: number, type: "0" | "5") => {
27
+ const block = new Uint8Array(BLOCK);
28
+ const write = (offset: number, text: string) => block.set(encoder.encode(text), offset);
29
+ const { name, prefix } = splitPath(path);
30
+ write(0, name);
31
+ write(100, octal(mode & 0o7777, 8));
32
+ write(108, octal(0, 8));
33
+ write(116, octal(0, 8));
34
+ write(124, octal(size, 12));
35
+ write(136, octal(0, 12));
36
+ write(148, " ");
37
+ write(156, type);
38
+ write(257, "ustar\0");
39
+ write(263, "00");
40
+ write(345, prefix);
41
+ let sum = 0;
42
+ for (const byte of block) sum += byte;
43
+ write(148, octal(sum, 7) + " ");
44
+ return block;
45
+ };
46
+
47
+ /** The archive bytes for `entries`, with a directory entry for every parent. */
48
+ export const tar = (entries: ReadonlyArray<TarEntry>): Uint8Array => {
49
+ const parts: Uint8Array[] = [];
50
+ const dirs = new Set<string>();
51
+ for (const entry of entries) {
52
+ const segments = entry.path.split("/").slice(0, -1);
53
+ for (let i = 1; i <= segments.length; i++) {
54
+ const dir = segments.slice(0, i).join("/");
55
+ if (dirs.has(dir)) continue;
56
+ dirs.add(dir);
57
+ parts.push(header(`${dir}/`, 0o755, 0, "5"));
58
+ }
59
+ parts.push(header(entry.path, entry.mode, entry.bytes.length, "0"));
60
+ parts.push(entry.bytes);
61
+ const pad = (BLOCK - (entry.bytes.length % BLOCK)) % BLOCK;
62
+ if (pad > 0) parts.push(new Uint8Array(pad));
63
+ }
64
+ parts.push(new Uint8Array(BLOCK * 2));
65
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
66
+ let offset = 0;
67
+ for (const part of parts) {
68
+ out.set(part, offset);
69
+ offset += part.length;
70
+ }
71
+ return out;
72
+ };