@alchemy.run/archil 0.0.0 → 2.0.0-beta.87

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 (184) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +57 -4
  3. package/lib/AWS/S3Disk.d.ts +68 -0
  4. package/lib/AWS/S3Disk.d.ts.map +1 -0
  5. package/lib/AWS/S3Disk.js +100 -0
  6. package/lib/AWS/S3Disk.js.map +1 -0
  7. package/lib/AWS/index.d.ts +2 -0
  8. package/lib/AWS/index.d.ts.map +1 -0
  9. package/lib/AWS/index.js +2 -0
  10. package/lib/AWS/index.js.map +1 -0
  11. package/lib/ApiToken.d.ts +63 -0
  12. package/lib/ApiToken.d.ts.map +1 -0
  13. package/lib/ApiToken.js +121 -0
  14. package/lib/ApiToken.js.map +1 -0
  15. package/lib/ArchilBindings.d.ts +65 -0
  16. package/lib/ArchilBindings.d.ts.map +1 -0
  17. package/lib/ArchilBindings.js +128 -0
  18. package/lib/ArchilBindings.js.map +1 -0
  19. package/lib/AuthProvider.d.ts +40 -0
  20. package/lib/AuthProvider.d.ts.map +1 -0
  21. package/lib/AuthProvider.js +87 -0
  22. package/lib/AuthProvider.js.map +1 -0
  23. package/lib/Bootstrap.d.ts +8 -0
  24. package/lib/Bootstrap.d.ts.map +1 -0
  25. package/lib/Bootstrap.js +27 -0
  26. package/lib/Bootstrap.js.map +1 -0
  27. package/lib/Branch.d.ts +101 -0
  28. package/lib/Branch.d.ts.map +1 -0
  29. package/lib/Branch.js +117 -0
  30. package/lib/Branch.js.map +1 -0
  31. package/lib/BranchDisk.d.ts +42 -0
  32. package/lib/BranchDisk.d.ts.map +1 -0
  33. package/lib/BranchDisk.js +14 -0
  34. package/lib/BranchDisk.js.map +1 -0
  35. package/lib/Checkpoint.d.ts +78 -0
  36. package/lib/Checkpoint.d.ts.map +1 -0
  37. package/lib/Checkpoint.js +95 -0
  38. package/lib/Checkpoint.js.map +1 -0
  39. package/lib/Clients.d.ts +369 -0
  40. package/lib/Clients.d.ts.map +1 -0
  41. package/lib/Clients.js +334 -0
  42. package/lib/Clients.js.map +1 -0
  43. package/lib/ConnectSandbox.d.ts +69 -0
  44. package/lib/ConnectSandbox.d.ts.map +1 -0
  45. package/lib/ConnectSandbox.js +21 -0
  46. package/lib/ConnectSandbox.js.map +1 -0
  47. package/lib/Contributions.d.ts +7 -0
  48. package/lib/Contributions.d.ts.map +1 -0
  49. package/lib/Contributions.js +6 -0
  50. package/lib/Contributions.js.map +1 -0
  51. package/lib/CreateDisk.d.ts +90 -0
  52. package/lib/CreateDisk.d.ts.map +1 -0
  53. package/lib/CreateDisk.js +47 -0
  54. package/lib/CreateDisk.js.map +1 -0
  55. package/lib/CreateImage.d.ts +42 -0
  56. package/lib/CreateImage.d.ts.map +1 -0
  57. package/lib/CreateImage.js +19 -0
  58. package/lib/CreateImage.js.map +1 -0
  59. package/lib/CreateSandbox.d.ts +104 -0
  60. package/lib/CreateSandbox.d.ts.map +1 -0
  61. package/lib/CreateSandbox.js +98 -0
  62. package/lib/CreateSandbox.js.map +1 -0
  63. package/lib/Credentials.d.ts +44 -0
  64. package/lib/Credentials.d.ts.map +1 -0
  65. package/lib/Credentials.js +57 -0
  66. package/lib/Credentials.js.map +1 -0
  67. package/lib/Disk.d.ts +283 -0
  68. package/lib/Disk.d.ts.map +1 -0
  69. package/lib/Disk.js +294 -0
  70. package/lib/Disk.js.map +1 -0
  71. package/lib/DiskObjectsHttp.d.ts +20 -0
  72. package/lib/DiskObjectsHttp.d.ts.map +1 -0
  73. package/lib/DiskObjectsHttp.js +51 -0
  74. package/lib/DiskObjectsHttp.js.map +1 -0
  75. package/lib/DiskUser.d.ts +189 -0
  76. package/lib/DiskUser.d.ts.map +1 -0
  77. package/lib/DiskUser.js +243 -0
  78. package/lib/DiskUser.js.map +1 -0
  79. package/lib/ExecDisk.d.ts +57 -0
  80. package/lib/ExecDisk.d.ts.map +1 -0
  81. package/lib/ExecDisk.js +3 -0
  82. package/lib/ExecDisk.js.map +1 -0
  83. package/lib/ExecDiskHttp.d.ts +5 -0
  84. package/lib/ExecDiskHttp.d.ts.map +1 -0
  85. package/lib/ExecDiskHttp.js +18 -0
  86. package/lib/ExecDiskHttp.js.map +1 -0
  87. package/lib/ExecDisks.d.ts +54 -0
  88. package/lib/ExecDisks.d.ts.map +1 -0
  89. package/lib/ExecDisks.js +37 -0
  90. package/lib/ExecDisks.js.map +1 -0
  91. package/lib/Image.d.ts +188 -0
  92. package/lib/Image.d.ts.map +1 -0
  93. package/lib/Image.js +191 -0
  94. package/lib/Image.js.map +1 -0
  95. package/lib/InferEnv.d.ts +26 -0
  96. package/lib/InferEnv.d.ts.map +1 -0
  97. package/lib/InferEnv.js +1 -0
  98. package/lib/InferEnv.js.map +1 -0
  99. package/lib/MountDisk.d.ts +190 -0
  100. package/lib/MountDisk.d.ts.map +1 -0
  101. package/lib/MountDisk.js +227 -0
  102. package/lib/MountDisk.js.map +1 -0
  103. package/lib/Providers.d.ts +25 -0
  104. package/lib/Providers.d.ts.map +1 -0
  105. package/lib/Providers.js +48 -0
  106. package/lib/Providers.js.map +1 -0
  107. package/lib/ReadDisk.d.ts +55 -0
  108. package/lib/ReadDisk.d.ts.map +1 -0
  109. package/lib/ReadDisk.js +3 -0
  110. package/lib/ReadDisk.js.map +1 -0
  111. package/lib/ReadWriteDisk.d.ts +26 -0
  112. package/lib/ReadWriteDisk.d.ts.map +1 -0
  113. package/lib/ReadWriteDisk.js +3 -0
  114. package/lib/ReadWriteDisk.js.map +1 -0
  115. package/lib/Sandbox.d.ts +507 -0
  116. package/lib/Sandbox.d.ts.map +1 -0
  117. package/lib/Sandbox.js +729 -0
  118. package/lib/Sandbox.js.map +1 -0
  119. package/lib/SandboxOptions.d.ts +185 -0
  120. package/lib/SandboxOptions.d.ts.map +1 -0
  121. package/lib/SandboxOptions.js +102 -0
  122. package/lib/SandboxOptions.js.map +1 -0
  123. package/lib/SandboxPortToken.d.ts +64 -0
  124. package/lib/SandboxPortToken.d.ts.map +1 -0
  125. package/lib/SandboxPortToken.js +77 -0
  126. package/lib/SandboxPortToken.js.map +1 -0
  127. package/lib/WriteDisk.d.ts +37 -0
  128. package/lib/WriteDisk.d.ts.map +1 -0
  129. package/lib/WriteDisk.js +3 -0
  130. package/lib/WriteDisk.js.map +1 -0
  131. package/lib/index.d.ts +29 -0
  132. package/lib/index.d.ts.map +1 -0
  133. package/lib/index.js +28 -0
  134. package/lib/index.js.map +1 -0
  135. package/lib/internal/DeployerKey.d.ts +21 -0
  136. package/lib/internal/DeployerKey.d.ts.map +1 -0
  137. package/lib/internal/DeployerKey.js +34 -0
  138. package/lib/internal/DeployerKey.js.map +1 -0
  139. package/lib/internal/archil.d.ts +87 -0
  140. package/lib/internal/archil.d.ts.map +1 -0
  141. package/lib/internal/archil.js +154 -0
  142. package/lib/internal/archil.js.map +1 -0
  143. package/lib/internal/hosted.d.ts +90 -0
  144. package/lib/internal/hosted.d.ts.map +1 -0
  145. package/lib/internal/hosted.js +291 -0
  146. package/lib/internal/hosted.js.map +1 -0
  147. package/package.json +79 -3
  148. package/src/AWS/S3Disk.ts +125 -0
  149. package/src/AWS/index.ts +1 -0
  150. package/src/ApiToken.ts +168 -0
  151. package/src/ArchilBindings.ts +179 -0
  152. package/src/AuthProvider.ts +122 -0
  153. package/src/Bootstrap.ts +54 -0
  154. package/src/Branch.ts +199 -0
  155. package/src/BranchDisk.ts +57 -0
  156. package/src/Checkpoint.ts +145 -0
  157. package/src/Clients.ts +954 -0
  158. package/src/ConnectSandbox.ts +91 -0
  159. package/src/Contributions.ts +9 -0
  160. package/src/CreateDisk.ts +165 -0
  161. package/src/CreateImage.ts +73 -0
  162. package/src/CreateSandbox.ts +258 -0
  163. package/src/Credentials.ts +101 -0
  164. package/src/Disk.ts +468 -0
  165. package/src/DiskObjectsHttp.ts +64 -0
  166. package/src/DiskUser.ts +369 -0
  167. package/src/ExecDisk.ts +62 -0
  168. package/src/ExecDiskHttp.ts +25 -0
  169. package/src/ExecDisks.ts +100 -0
  170. package/src/Image.ts +316 -0
  171. package/src/InferEnv.ts +27 -0
  172. package/src/MountDisk.ts +466 -0
  173. package/src/Providers.ts +75 -0
  174. package/src/ReadDisk.ts +61 -0
  175. package/src/ReadWriteDisk.ts +31 -0
  176. package/src/Sandbox.ts +1064 -0
  177. package/src/SandboxOptions.ts +230 -0
  178. package/src/SandboxPortToken.ts +125 -0
  179. package/src/WriteDisk.ts +42 -0
  180. package/src/globals.d.ts +2 -0
  181. package/src/index.ts +59 -0
  182. package/src/internal/DeployerKey.ts +63 -0
  183. package/src/internal/archil.ts +273 -0
  184. package/src/internal/hosted.ts +402 -0
@@ -0,0 +1,101 @@
1
+ import {
2
+ controlPlaneUrl,
3
+ Credentials,
4
+ DEFAULT_REGION,
5
+ REGIONS,
6
+ } from "@distilled.cloud/archil/Credentials";
7
+ import { ConfigError } from "@distilled.cloud/core/errors";
8
+ import { lazyCredentials } from "alchemy/Auth/Resolve";
9
+ import * as Context from "effect/Context";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Layer from "effect/Layer";
12
+ import * as Redacted from "effect/Redacted";
13
+ import { ARCHIL_AUTH_PROVIDER_NAME, type ArchilResolvedCredentials } from "./AuthProvider.ts";
14
+
15
+ export {
16
+ Credentials,
17
+ CredentialsFromEnv,
18
+ DEFAULT_REGION,
19
+ REGIONS,
20
+ } from "@distilled.cloud/archil/Credentials";
21
+
22
+ /** Where the current Archil credentials act. */
23
+ export interface ArchilAccount {
24
+ /** Archil region id, e.g. `aws-us-east-1`. */
25
+ readonly region: string;
26
+ /** Archil organization id, when configured. */
27
+ readonly organization: string | undefined;
28
+ }
29
+
30
+ /**
31
+ * The region (and organization) the current Archil credentials act in.
32
+ * Holds an effect so it resolves on first use, never at layer build.
33
+ */
34
+ export class Account extends Context.Service<Account, Effect.Effect<ArchilAccount>>()(
35
+ "Archil.Account",
36
+ ) {}
37
+
38
+ /** The region and organization of the current Archil credentials. */
39
+ export const account = Effect.gen(function* () {
40
+ return yield* yield* Account;
41
+ });
42
+
43
+ const make = (
44
+ resolve: Effect.Effect<ArchilResolvedCredentials, ConfigError>,
45
+ ): Effect.Effect<Context.Context<Credentials | Account>> =>
46
+ Effect.gen(function* () {
47
+ const credentials = Effect.flatMap(resolve, (creds) => {
48
+ const apiBaseUrl = controlPlaneUrl(creds.region);
49
+ return apiBaseUrl
50
+ ? Effect.succeed({ apiKey: creds.apiKey, apiBaseUrl, region: creds.region })
51
+ : Effect.fail(
52
+ new ConfigError({
53
+ message: `Unknown Archil region "${creds.region}". Valid regions: ${Object.keys(REGIONS).join(", ")}`,
54
+ }),
55
+ );
56
+ });
57
+ // `Account` is typed as never failing, so a credentials failure dies
58
+ // here; the engine still reports it as `CredentialsRequired`.
59
+ const accountInfo = yield* resolve.pipe(
60
+ Effect.map((creds) => ({ region: creds.region, organization: creds.organization })),
61
+ Effect.orDie,
62
+ Effect.cached,
63
+ );
64
+ return Context.make(Credentials, credentials).pipe(Context.add(Account, accountInfo));
65
+ });
66
+
67
+ /**
68
+ * Build Archil `Credentials` + `Account` from an explicit API key. Useful
69
+ * for tests, or for a runtime that reads its key from a binding.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * Effect.provide(Archil.fromApiKey({ apiKey: "key-...", region: "aws-us-east-1" }))
74
+ * ```
75
+ */
76
+ export const fromApiKey = (input: {
77
+ apiKey: string | Redacted.Redacted<string>;
78
+ region?: string;
79
+ organization?: string;
80
+ }) =>
81
+ Effect.succeed<ArchilResolvedCredentials>({
82
+ type: "apiKey",
83
+ apiKey: typeof input.apiKey === "string" ? Redacted.make(input.apiKey) : input.apiKey,
84
+ region: input.region ?? DEFAULT_REGION,
85
+ organization: input.organization,
86
+ source: { type: "stored" },
87
+ }).pipe(make, Layer.effectContext);
88
+
89
+ /**
90
+ * Build Archil `Credentials` + `Account` that resolve through the Alchemy
91
+ * AuthProvider: environment credentials (`ARCHIL_API_KEY`) when present,
92
+ * otherwise the selected profile. Resolution is deferred until the first
93
+ * API call, so building the provider layers never needs a profile.
94
+ */
95
+ export const fromAuthProvider = () =>
96
+ Layer.effectContext(
97
+ lazyCredentials<ArchilResolvedCredentials, ConfigError>()(
98
+ ARCHIL_AUTH_PROVIDER_NAME,
99
+ (creds) => creds,
100
+ ).pipe(Effect.flatMap(make)),
101
+ );
package/src/Disk.ts ADDED
@@ -0,0 +1,468 @@
1
+ import * as archil from "@distilled.cloud/archil";
2
+ import { Unowned } from "alchemy/AdoptPolicy";
3
+ import { deepEqual, isResolved } from "alchemy/Diff";
4
+ import * as Provider from "alchemy/Provider";
5
+ import { Resource } from "alchemy/Resource";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Redacted from "effect/Redacted";
8
+ import * as Stream from "effect/Stream";
9
+ import {
10
+ diskName,
11
+ findDiskByName,
12
+ getDisk,
13
+ isAlchemyName,
14
+ isRuntimeObjectName,
15
+ waitForDisk,
16
+ waitForDiskGone,
17
+ listAllDisks,
18
+ } from "./internal/archil.ts";
19
+ import type { Providers } from "./Providers.ts";
20
+
21
+ /**
22
+ * Amazon S3 bucket behind a disk. Omit the keys when the bucket policy
23
+ * trusts Archil's role for the region (see `ArchilAWS.S3Disk`, which writes
24
+ * that policy for you).
25
+ */
26
+ export interface S3DiskStorage {
27
+ type: "s3";
28
+ /** Bucket name. */
29
+ bucketName: string;
30
+ /** Prefix within the bucket the disk lives under, e.g. `"data/"`. */
31
+ bucketPrefix?: string;
32
+ /** Access key id. Omit for IAM-role access through the bucket policy. */
33
+ accessKeyId?: Redacted.Redacted<string>;
34
+ /** Secret access key. */
35
+ secretAccessKey?: Redacted.Redacted<string>;
36
+ /** Session token for temporary credentials. */
37
+ sessionToken?: Redacted.Redacted<string>;
38
+ }
39
+
40
+ /** Google Cloud Storage bucket behind a disk, through HMAC keys. */
41
+ export interface GcsDiskStorage {
42
+ type: "gcs";
43
+ bucketName: string;
44
+ bucketPrefix?: string;
45
+ accessKeyId: Redacted.Redacted<string>;
46
+ secretAccessKey: Redacted.Redacted<string>;
47
+ }
48
+
49
+ /** Cloudflare R2 bucket behind a disk. */
50
+ export interface R2DiskStorage {
51
+ type: "r2";
52
+ bucketName: string;
53
+ /** R2 S3 endpoint, `https://<account>.r2.cloudflarestorage.com`. */
54
+ bucketEndpoint: string;
55
+ bucketPrefix?: string;
56
+ accessKeyId: Redacted.Redacted<string>;
57
+ secretAccessKey: Redacted.Redacted<string>;
58
+ }
59
+
60
+ /** Any S3-compatible object store behind a disk. */
61
+ export interface S3CompatibleDiskStorage {
62
+ type: "s3-compatible";
63
+ bucketName: string;
64
+ bucketEndpoint: string;
65
+ bucketPrefix?: string;
66
+ accessKeyId: Redacted.Redacted<string>;
67
+ secretAccessKey: Redacted.Redacted<string>;
68
+ }
69
+
70
+ /** Azure Blob Storage container behind a disk, through a service principal. */
71
+ export interface AzureBlobDiskStorage {
72
+ type: "azure-blob";
73
+ storageAccount: string;
74
+ containerName: string;
75
+ bucketPrefix?: string;
76
+ tenantId: string;
77
+ clientId: string;
78
+ clientSecret: Redacted.Redacted<string>;
79
+ }
80
+
81
+ /** Where a disk's files live. Omit for Archil-managed storage. */
82
+ export type DiskStorage =
83
+ | S3DiskStorage
84
+ | GcsDiskStorage
85
+ | R2DiskStorage
86
+ | S3CompatibleDiskStorage
87
+ | AzureBlobDiskStorage;
88
+
89
+ export interface DiskProps {
90
+ /**
91
+ * Disk name, unique within your Archil account: letters, digits, `-` and
92
+ * `_`, at most 100 characters. If omitted, a unique name is generated
93
+ * from the stack, stage, and logical ID. Changing it replaces the disk.
94
+ */
95
+ name?: string;
96
+ /**
97
+ * Your own bucket to keep the disk's files in, as ordinary objects any
98
+ * S3 tool can read. Omit for Archil-managed storage. Changing it replaces
99
+ * the disk.
100
+ */
101
+ storage?: DiskStorage;
102
+ /**
103
+ * IPv4 addresses and CIDR ranges allowed to mount the disk. Empty or
104
+ * omitted allows any address.
105
+ */
106
+ allowedIps?: string[];
107
+ /**
108
+ * Whether sandboxes and serverless exec may mount the disk.
109
+ * @default true
110
+ */
111
+ allowSandboxMount?: boolean;
112
+ }
113
+
114
+ export interface DiskAttributes {
115
+ /** Disk id (`dsk-…`). */
116
+ diskId: string;
117
+ /** Disk name. */
118
+ name: string;
119
+ /** Archil organization that owns the disk. */
120
+ organization: string;
121
+ /** Archil region the disk lives in, e.g. `aws-us-east-1`. */
122
+ region: string;
123
+ /** Storage backend: `archil` for Archil-managed storage, else the bucket type. */
124
+ storageType: string;
125
+ /** Bucket name, for a disk on your own bucket. */
126
+ bucketName: string | undefined;
127
+ /** Bucket prefix, for a disk on your own bucket. */
128
+ bucketPrefix: string | undefined;
129
+ /**
130
+ * The session id Archil assumes your bucket's role with. Bucket policies
131
+ * can scope access to exactly this disk with
132
+ * `aws:PrincipalTag/archilSessionId`.
133
+ */
134
+ sessionId: string | undefined;
135
+ /** IPv4 addresses and CIDR ranges allowed to mount the disk. */
136
+ allowedIps: string[];
137
+ /** Whether sandboxes and serverless exec may mount the disk. */
138
+ allowSandboxMount: boolean;
139
+ }
140
+
141
+ export type Disk = Resource<"Archil.Disk", DiskProps, DiskAttributes, never, Providers, DiskLookup>;
142
+
143
+ /**
144
+ * Props for {@link Disk.lookup}: find a disk this stack
145
+ * doesn't manage by its name.
146
+ */
147
+ export interface DiskLookup {
148
+ /** Disk name. */
149
+ name: string;
150
+ }
151
+
152
+ /**
153
+ * An Archil disk: a durable filesystem that mounts in about 50 ms on any
154
+ * Linux machine, sandbox, or serverless exec, with hot data on SSD and the
155
+ * rest in object storage. Keep its files in Archil-managed storage, or in
156
+ * your own bucket as ordinary objects.
157
+ *
158
+ * Declare a disk at deploy time for the few long-lived ones an app shares,
159
+ * like a dataset or a seeded base you branch from. Disks that belong to one
160
+ * user, session, or task are created at run time with
161
+ * {@link CreateDisk}, which returns the same disk shape.
162
+ *
163
+ * Archil has no tags, so Alchemy owns a disk through its generated name.
164
+ * A disk with a fixed `name` that already exists is adopted only with
165
+ * `--adopt`.
166
+ *
167
+ * :::caution
168
+ * Changing `name` or `storage` replaces the disk, and destroying a disk on
169
+ * Archil-managed storage deletes its files. A disk on your own bucket
170
+ * leaves the objects in the bucket.
171
+ * :::
172
+ * @see https://docs.archil.com
173
+ *
174
+ * ### Creating a Disk
175
+ * **Example:** Archil-managed storage
176
+ * ```typescript
177
+ * export const Dataset = Archil.Disk("Dataset", {});
178
+ * ```
179
+ *
180
+ * **Example:** On your own S3 bucket, with no keys
181
+ * ```typescript
182
+ * export const Store = AWS.S3.Bucket("Store", {});
183
+ * export const Dataset = ArchilAWS.S3Disk("Dataset", { bucket: Store, prefix: "datasets/" });
184
+ * ```
185
+ *
186
+ * **Example:** On an R2 bucket
187
+ * ```typescript
188
+ * export const Dataset = Archil.Disk("Dataset", {
189
+ * storage: {
190
+ * type: "r2",
191
+ * bucketName: "datasets",
192
+ * bucketEndpoint: "https://<account>.r2.cloudflarestorage.com",
193
+ * accessKeyId: Alchemy.Secret("R2_ACCESS_KEY_ID"),
194
+ * secretAccessKey: Alchemy.Secret("R2_SECRET_ACCESS_KEY"),
195
+ * },
196
+ * });
197
+ * ```
198
+ *
199
+ * ### Restricting Access
200
+ * **Example:** Only mount from known addresses
201
+ * ```typescript
202
+ * export const Dataset = Archil.Disk("Dataset", {
203
+ * allowedIps: ["203.0.113.0/24"],
204
+ * allowSandboxMount: false,
205
+ * });
206
+ * ```
207
+ *
208
+ * ### Binding to a Runtime
209
+ * Object bindings give each host its own S3 user on this disk (read-only
210
+ * for `ReadDisk`), so the host never holds an API key.
211
+ *
212
+ * **Example:** Read and write the disk from a program sandbox
213
+ * ```typescript
214
+ * export default class Api extends Archil.Sandbox<Api>()(
215
+ * "Api",
216
+ * { main: import.meta.url },
217
+ * Effect.gen(function* () {
218
+ * const files = yield* Archil.ReadWriteDisk(Dataset);
219
+ * return {
220
+ * fetch: Effect.map(files.getText("README.md"), (text) => HttpServerResponse.text(text)),
221
+ * };
222
+ * }).pipe(Effect.provide(Archil.ReadWriteDiskHttp)),
223
+ * ) {}
224
+ * ```
225
+ *
226
+ * **Example:** Mount it into a sandbox
227
+ * ```typescript
228
+ * export const Box = Archil.Sandbox("Box", {
229
+ * mounts: [{ disk: Dataset, path: "/data", mode: "read-only" }],
230
+ * });
231
+ * ```
232
+ *
233
+ * **Example:** Mount it in a Cloudflare Container
234
+ * ```typescript
235
+ * const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" });
236
+ * const path = yield* dataset.mount();
237
+ * ```
238
+ *
239
+ * ### Runtime Operations
240
+ * **Example:** Run commands and read files
241
+ * ```typescript
242
+ * const dataset = yield* Archil.ExecDisk(Dataset);
243
+ * const files = yield* Archil.ReadDisk(Dataset);
244
+ * const result = yield* dataset.exec("wc -l /mnt/archil/*.csv");
245
+ * const readme = yield* files.getText("README.md");
246
+ * ```
247
+ *
248
+ * **Example:** Upload a large file and share a link
249
+ * ```typescript
250
+ * const files = yield* Archil.ReadWriteDisk(Dataset);
251
+ * yield* files.upload("exports/2026-11.parquet", body);
252
+ * const url = yield* files.share("exports/2026-11.parquet", 3600);
253
+ * ```
254
+ *
255
+ * ### Branches and Checkpoints
256
+ * **Example:** Seed a base, then branch per session
257
+ * ```typescript
258
+ * export const Seed = Archil.Disk("Seed", {});
259
+ * export const SeedBase = Effect.gen(function* () {
260
+ * const seed = yield* Seed;
261
+ * return yield* Archil.Checkpoint("SeedBase", {
262
+ * disk: seed.diskId,
263
+ * name: "base",
264
+ * run: ["git clone https://github.com/acme/app ."],
265
+ * });
266
+ * });
267
+ * // runtime
268
+ * const seeds = yield* Archil.BranchDisk(Seed);
269
+ * const branch = yield* seeds.branch({ name: `session-${id}`, fromCheckpoint: "base" });
270
+ * ```
271
+ *
272
+ * ### Looking up an existing disk
273
+ * `Disk.lookup` references a disk Alchemy doesn't manage (created in
274
+ * the Archil console or by another stack). It is read during plan/deploy,
275
+ * never created, updated or deleted.
276
+ *
277
+ * **Example:** Branch an existing disk
278
+ * ```typescript
279
+ * const data = Archil.Disk.lookup({ name: "datasets" });
280
+ * const preview = yield* Archil.Branch("Preview", { disk: data.diskId });
281
+ * ```
282
+ *
283
+ * @resource
284
+ * @product Disk
285
+ */
286
+ export const Disk = Resource<Disk>("Archil.Disk");
287
+
288
+ const unwrap = (value: Redacted.Redacted<string> | string | undefined) =>
289
+ value === undefined ? undefined : typeof value === "string" ? value : Redacted.value(value);
290
+
291
+ /** The `mounts` entry Archil's create call takes for a storage backend. */
292
+ export const toMountConfig = (storage: DiskStorage): archil.MountConfig => {
293
+ switch (storage.type) {
294
+ case "s3":
295
+ return {
296
+ type: "s3",
297
+ bucketName: storage.bucketName,
298
+ bucketPrefix: storage.bucketPrefix,
299
+ accessKeyId: unwrap(storage.accessKeyId),
300
+ secretAccessKey: unwrap(storage.secretAccessKey),
301
+ sessionToken: unwrap(storage.sessionToken),
302
+ } as archil.MountConfig;
303
+ case "gcs":
304
+ case "r2":
305
+ case "s3-compatible":
306
+ return {
307
+ ...storage,
308
+ accessKeyId: unwrap(storage.accessKeyId),
309
+ secretAccessKey: unwrap(storage.secretAccessKey),
310
+ } as archil.MountConfig;
311
+ case "azure-blob":
312
+ return { ...storage, clientSecret: unwrap(storage.clientSecret) } as archil.MountConfig;
313
+ }
314
+ };
315
+
316
+ /** The comparable identity of a storage backend (no secrets). */
317
+ const storageIdentity = (storage: DiskStorage | undefined) =>
318
+ storage === undefined
319
+ ? undefined
320
+ : {
321
+ type: storage.type,
322
+ bucketName: "bucketName" in storage ? storage.bucketName : undefined,
323
+ bucketPrefix: storage.bucketPrefix,
324
+ bucketEndpoint: "bucketEndpoint" in storage ? storage.bucketEndpoint : undefined,
325
+ containerName: "containerName" in storage ? storage.containerName : undefined,
326
+ storageAccount: "storageAccount" in storage ? storage.storageAccount : undefined,
327
+ };
328
+
329
+ /** Map a disk response (and its synced settings) to attributes. */
330
+ export const toDiskAttributes = (
331
+ disk: archil.DiskResponse,
332
+ settings: { allowedIps: string[]; allowSandboxMount: boolean },
333
+ ): DiskAttributes => {
334
+ const first = disk.mounts?.[0];
335
+ // Archil-managed storage is an `s3` mount on Archil's own bucket.
336
+ const mount = first?.config?.bucketName?.startsWith("archil.prod.") ? undefined : first;
337
+ return {
338
+ diskId: disk.id,
339
+ name: disk.name,
340
+ organization: disk.organization,
341
+ region: disk.region,
342
+ storageType: mount?.type ?? "archil",
343
+ bucketName: mount?.config?.bucketName,
344
+ bucketPrefix: mount?.config?.bucketPrefix,
345
+ sessionId: mount?.config?.sessionId,
346
+ allowedIps: settings.allowedIps,
347
+ allowSandboxMount: settings.allowSandboxMount,
348
+ };
349
+ };
350
+
351
+ /** Read a disk's mount-access settings. */
352
+ export const readDiskSettings = Effect.fn(function* (id: string) {
353
+ const [ips, mount] = yield* Effect.all(
354
+ [archil.getAllowedIps({ id }), archil.getSandboxMount({ id })],
355
+ { concurrency: 2 },
356
+ );
357
+ return {
358
+ allowedIps: ips.data.allowedIps ?? [],
359
+ allowSandboxMount: mount.data.allowSandboxMount,
360
+ };
361
+ });
362
+
363
+ /** Converge a disk's mount-access settings to the desired ones. */
364
+ export const syncDiskSettings = Effect.fn(function* (
365
+ id: string,
366
+ desired: { allowedIps?: string[]; allowSandboxMount?: boolean },
367
+ ) {
368
+ const observed = yield* readDiskSettings(id);
369
+ const allowedIps = desired.allowedIps ?? [];
370
+ const allowSandboxMount = desired.allowSandboxMount ?? true;
371
+ if (!deepEqual([...observed.allowedIps].sort(), [...allowedIps].sort())) {
372
+ yield* archil.setAllowedIps({ id, allowedIps });
373
+ }
374
+ if (observed.allowSandboxMount !== allowSandboxMount) {
375
+ yield* archil.setSandboxMount({ id, allowSandboxMount });
376
+ }
377
+ return { allowedIps, allowSandboxMount };
378
+ });
379
+
380
+ /** Ensure a disk with this name exists and is available (create-or-observe). */
381
+ export const ensureDisk = Effect.fn(function* (
382
+ name: string,
383
+ storage: DiskStorage | undefined,
384
+ knownId?: string,
385
+ ) {
386
+ const found = (knownId ? yield* getDisk(knownId) : undefined) ?? (yield* findDiskByName(name));
387
+ // A disk still being deleted (an earlier destroy) holds the name: let it go.
388
+ if (found?.status === "deleting") yield* waitForDiskGone(found.id);
389
+ const observed = found?.status === "deleting" ? undefined : found;
390
+ if (observed) return yield* waitForDisk(observed.id);
391
+ // Archil answers a create of an existing name with that disk's id, so a
392
+ // concurrent create of the same name needs no special case.
393
+ const { data } = yield* archil.createDisk({
394
+ name,
395
+ mounts: storage ? [toMountConfig(storage)] : undefined,
396
+ });
397
+ return yield* waitForDisk(data.diskId);
398
+ });
399
+
400
+ /** Delete a disk and wait until it is gone. Deleting a missing disk succeeds. */
401
+ export const deleteDisk = Effect.fn(function* (id: string) {
402
+ yield* archil.deleteDisk({ id }).pipe(Effect.catchTag("NotFound", () => Effect.void));
403
+ yield* waitForDiskGone(id);
404
+ });
405
+
406
+ export const DiskProvider = () =>
407
+ Provider.effect(
408
+ Disk,
409
+ Effect.gen(function* () {
410
+ return {
411
+ stables: ["diskId", "name", "organization", "region", "storageType"],
412
+ list: Effect.fn(function* () {
413
+ const disks = yield* Stream.runCollect(listAllDisks());
414
+ return yield* Effect.forEach(
415
+ disks.filter(
416
+ (disk) =>
417
+ (isAlchemyName(disk.name) || isRuntimeObjectName(disk.name)) &&
418
+ disk.status !== "deleted",
419
+ ),
420
+ (disk) => readDiskSettings(disk.id).pipe(Effect.map((s) => toDiskAttributes(disk, s))),
421
+ { concurrency: 4 },
422
+ );
423
+ }),
424
+ diff: Effect.fn(function* ({ id, olds, news, output }) {
425
+ if (!isResolved(news)) return undefined;
426
+ const name = output?.name ?? (yield* diskName(id, olds?.name));
427
+ if (news.name !== undefined && news.name !== name) {
428
+ return { action: "replace" } as const;
429
+ }
430
+ if (!deepEqual(storageIdentity(news.storage), storageIdentity(olds?.storage))) {
431
+ return { action: "replace" } as const;
432
+ }
433
+ return undefined;
434
+ }),
435
+ lookup: Effect.fn(function* ({ name }) {
436
+ const disk = yield* findDiskByName(name);
437
+ return disk ? toDiskAttributes(disk, yield* readDiskSettings(disk.id)) : undefined;
438
+ }),
439
+
440
+ read: Effect.fn(function* ({ id, olds, output }) {
441
+ const name = output?.name ?? (yield* diskName(id, olds?.name));
442
+ const disk =
443
+ (output ? yield* getDisk(output.diskId) : undefined) ?? (yield* findDiskByName(name));
444
+ if (!disk) return undefined;
445
+ const attrs = toDiskAttributes(disk, yield* readDiskSettings(disk.id));
446
+ // A generated name carries this resource's instance suffix, so a
447
+ // disk with that name is ours even when state was lost. A fixed
448
+ // name may belong to anyone.
449
+ if (output === undefined && olds?.name !== undefined && !isAlchemyName(disk.name)) {
450
+ return Unowned(attrs);
451
+ }
452
+ return attrs;
453
+ }),
454
+ reconcile: Effect.fn(function* ({ id, news, output, session }) {
455
+ // Observe + ensure
456
+ const name = output?.name ?? (yield* diskName(id, news.name));
457
+ yield* session.note(`Ensuring disk ${name}...`);
458
+ const disk = yield* ensureDisk(name, news.storage, output?.diskId);
459
+ // Sync
460
+ const settings = yield* syncDiskSettings(disk.id, news);
461
+ return toDiskAttributes(disk, settings);
462
+ }),
463
+ delete: Effect.fn(function* ({ output }) {
464
+ yield* deleteDisk(output.diskId);
465
+ }),
466
+ };
467
+ }),
468
+ );
@@ -0,0 +1,64 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import { DiskKeyAccess } from "./ArchilBindings.ts";
4
+ import { makeReadDiskClient, makeWriteDiskClient, presignedShare } from "./Clients.ts";
5
+ import type { Disk } from "./Disk.ts";
6
+ import { ReadDisk, type DiskObjectsOptions } from "./ReadDisk.ts";
7
+ import { ReadWriteDisk } from "./ReadWriteDisk.ts";
8
+ import { WriteDisk } from "./WriteDisk.ts";
9
+
10
+ const diskRef = Effect.fn(function* (disk: Disk, options: DiskObjectsOptions | undefined) {
11
+ const diskId = yield* disk.diskId;
12
+ return Effect.map(diskId, (id) => ({ disk: id, branch: options?.branch }));
13
+ });
14
+
15
+ /**
16
+ * {@link ReadDisk} over Archil's S3-compatible API, usable from any host.
17
+ * The host gets a read-only S3 user on the disk.
18
+ */
19
+ export const ReadDiskHttp = Layer.effect(
20
+ ReadDisk,
21
+ Effect.gen(function* () {
22
+ const keys = yield* DiskKeyAccess;
23
+ return Effect.fn(function* (disk: Disk, options?: DiskObjectsOptions) {
24
+ const ref = yield* diskRef(disk, options);
25
+ const run = yield* keys(disk, "read");
26
+ return makeReadDiskClient(ref, run, presignedShare(run));
27
+ });
28
+ }),
29
+ );
30
+
31
+ /**
32
+ * {@link WriteDisk} over Archil's S3-compatible API, usable from any host.
33
+ * The host gets a read-write S3 user on the disk.
34
+ */
35
+ export const WriteDiskHttp = Layer.effect(
36
+ WriteDisk,
37
+ Effect.gen(function* () {
38
+ const keys = yield* DiskKeyAccess;
39
+ return Effect.fn(function* (disk: Disk, options?: DiskObjectsOptions) {
40
+ const ref = yield* diskRef(disk, options);
41
+ const run = yield* keys(disk, "write");
42
+ return makeWriteDiskClient(ref, run);
43
+ });
44
+ }),
45
+ );
46
+
47
+ /**
48
+ * {@link ReadWriteDisk} over Archil's S3-compatible API, usable from any
49
+ * host. The host gets a read-write S3 user on the disk.
50
+ */
51
+ export const ReadWriteDiskHttp = Layer.effect(
52
+ ReadWriteDisk,
53
+ Effect.gen(function* () {
54
+ const keys = yield* DiskKeyAccess;
55
+ return Effect.fn(function* (disk: Disk, options?: DiskObjectsOptions) {
56
+ const ref = yield* diskRef(disk, options);
57
+ const run = yield* keys(disk, "write");
58
+ return {
59
+ ...makeReadDiskClient(ref, run, presignedShare(run)),
60
+ ...makeWriteDiskClient(ref, run),
61
+ };
62
+ });
63
+ }),
64
+ );