@alchemy.run/archil 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 (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,369 @@
1
+ import * as archil from "@distilled.cloud/archil";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import { createPhysicalName } from "alchemy/PhysicalName";
4
+ import * as Provider from "alchemy/Provider";
5
+ import { Resource } from "alchemy/Resource";
6
+ import * as Data from "effect/Data";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Redacted from "effect/Redacted";
9
+ import * as Schedule from "effect/Schedule";
10
+ import type { Providers } from "./Providers.ts";
11
+
12
+ export class DiskUserUnexpectedResponse extends Data.TaggedError(
13
+ "Archil.DiskUserUnexpectedResponse",
14
+ )<{
15
+ readonly message: string;
16
+ }> {}
17
+
18
+ export interface DiskUserProps {
19
+ /** Id of the disk the user can mount (e.g. `disk.diskId`). Changing it replaces the user. */
20
+ disk: string;
21
+ /**
22
+ * Name shown in the console. If omitted, a unique name is generated from
23
+ * the stack, stage, and logical ID. Changing it replaces the user.
24
+ */
25
+ nickname?: string;
26
+ /**
27
+ * An AWS IAM role or user ARN that mounts with its own AWS identity
28
+ * instead of a token, e.g. an ECS task role. Changing it replaces the user.
29
+ */
30
+ principal?: string;
31
+ /**
32
+ * `token` mounts with a disk token; `s3` gets an access key pair for the
33
+ * disk's S3-compatible API instead. Ignored when `principal` is set (an
34
+ * `awssts` user). Changing it replaces the user.
35
+ * @default "token"
36
+ */
37
+ type?: "token" | "s3";
38
+ /**
39
+ * Mounts made with this user reject writes (`EROFS`); an `s3` user can
40
+ * only `GET` and `HEAD`. Changing it replaces the user.
41
+ * @default false
42
+ */
43
+ readOnly?: boolean;
44
+ /**
45
+ * When the user stops working, as an RFC 3339 timestamp in the future
46
+ * (at most 365 days away). Requests signed with an expired `s3` key,
47
+ * presigned URLs included, are rejected. Changing it replaces the user.
48
+ */
49
+ expiresAt?: string;
50
+ /**
51
+ * The user works for one mount only; later mounts with it are rejected.
52
+ * Not available for `s3` users. Changing it replaces the user.
53
+ * @default false
54
+ */
55
+ oneUse?: boolean;
56
+ }
57
+
58
+ export interface DiskUserAttributes {
59
+ /** Id of the disk. */
60
+ diskId: string;
61
+ /** `token`, `s3` for an access key pair, or `awssts` for an IAM principal. */
62
+ type: "token" | "awssts" | "s3";
63
+ /** Archil's identifier for the user, used to revoke it. */
64
+ identifier: string;
65
+ /** Name shown in the console. */
66
+ nickname: string;
67
+ /** The IAM ARN, for an `awssts` user. */
68
+ principal: string | undefined;
69
+ /**
70
+ * The disk token, for a `token` user. Pass it to `archil mount` as
71
+ * `ARCHIL_MOUNT_TOKEN`. It works only for this disk.
72
+ */
73
+ token: Redacted.Redacted<string> | undefined;
74
+ /** The S3 access key id, for an `s3` user. */
75
+ accessKeyId: string | undefined;
76
+ /**
77
+ * The S3 secret access key, for an `s3` user. Sign requests to the disk's
78
+ * S3-compatible API with it; it works only for this disk.
79
+ */
80
+ secretAccessKey: Redacted.Redacted<string> | undefined;
81
+ /** Whether the user is read-only. */
82
+ readOnly: boolean;
83
+ /** When the user expires, if it does. */
84
+ expiresAt: string | undefined;
85
+ /** Whether the user works for one mount only. */
86
+ oneUse: boolean;
87
+ }
88
+
89
+ export type DiskUser = Resource<
90
+ "Archil.DiskUser",
91
+ DiskUserProps,
92
+ DiskUserAttributes,
93
+ never,
94
+ Providers
95
+ >;
96
+
97
+ /**
98
+ * Access to one disk. A token user gets a disk token for `archil mount`; an
99
+ * `s3` user gets an access key pair for the disk's S3-compatible API; an
100
+ * IAM user lets an AWS principal mount with its own identity, so no secret
101
+ * exists at all. Archil checks each one itself, so a host holding a disk
102
+ * user can't reach any other disk. `readOnly`, `expiresAt` and `oneUse`
103
+ * narrow it further.
104
+ *
105
+ * Archil only shows a token or secret key once, when it's created, so
106
+ * Alchemy stores it in state as a `Redacted` value. Destroying the user
107
+ * revokes it. `Archil.MountDisk(disk)` and the object bindings
108
+ * (`Archil.ReadDisk`, `Archil.WriteDisk`, `Archil.ReadWriteDisk`) create
109
+ * one per host for you.
110
+ *
111
+ * :::caution
112
+ * Archil can't change a user, so changing any prop replaces it: the old
113
+ * token or key stops working once the new one exists.
114
+ * :::
115
+ * @see https://docs.archil.com/concepts/sharing-disks
116
+ *
117
+ * ### Creating a Disk User
118
+ * **Example:** A token for one machine
119
+ * ```typescript
120
+ * const user = yield* Archil.DiskUser("Indexer", { disk: dataset.diskId });
121
+ * // ARCHIL_MOUNT_TOKEN=<user.token> archil mount <disk> /mnt/data --region aws-us-east-1
122
+ * ```
123
+ *
124
+ * **Example:** An ECS task role that mounts with its own identity
125
+ * ```typescript
126
+ * const user = yield* Archil.DiskUser("Workers", {
127
+ * disk: dataset.diskId,
128
+ * principal: taskRole.roleArn,
129
+ * });
130
+ * ```
131
+ *
132
+ * **Example:** A read-only token that expires
133
+ * ```typescript
134
+ * const user = yield* Archil.DiskUser("Viewer", {
135
+ * disk: dataset.diskId,
136
+ * readOnly: true,
137
+ * expiresAt: "2027-01-01T00:00:00Z",
138
+ * });
139
+ * ```
140
+ *
141
+ * ### S3 Keys
142
+ * **Example:** A read-only access key pair for one disk
143
+ * ```typescript
144
+ * const reader = yield* Archil.DiskUser("Reader", {
145
+ * disk: dataset.diskId,
146
+ * type: "s3",
147
+ * readOnly: true,
148
+ * });
149
+ * // reader.accessKeyId, reader.secretAccessKey sign requests to
150
+ * // https://s3.green.us-east-1.aws.prod.archil.com/<disk id>/<key>
151
+ * ```
152
+ *
153
+ * @resource
154
+ * @product Disk
155
+ */
156
+ export const DiskUser = Resource<DiskUser>("Archil.DiskUser");
157
+
158
+ const userNickname = (id: string, nickname: string | undefined) =>
159
+ nickname ? Effect.succeed(nickname) : createPhysicalName({ id, maxLength: 64, delimiter: "-" });
160
+
161
+ const lookupDiskUser = Effect.fn(function* (
162
+ diskId: string,
163
+ match: { nickname?: string; principal?: string; identifier?: string },
164
+ ) {
165
+ const { data } = yield* archil.getDisk({ id: diskId });
166
+ return (data.authorizedUsers ?? []).find((user) =>
167
+ match.identifier !== undefined
168
+ ? user.identifier === match.identifier
169
+ : match.principal !== undefined
170
+ ? user.principal === match.principal
171
+ : user.nickname === match.nickname,
172
+ );
173
+ });
174
+
175
+ /**
176
+ * The authorized user on a disk with this identifier, nickname or principal.
177
+ *
178
+ * For a few seconds after a user is added, some `getDisk` reads still list
179
+ * the disk without it (removals show at once). So a listed user exists, but
180
+ * a user we created (`match.identifier`) counts as missing only after reads
181
+ * across that window all miss it.
182
+ */
183
+ export const findDiskUser = Effect.fn(function* (
184
+ diskId: string,
185
+ match: { nickname?: string; principal?: string; identifier?: string },
186
+ ) {
187
+ const first = yield* lookupDiskUser(diskId, match);
188
+ if (first || match.identifier === undefined) return first;
189
+ return yield* lookupDiskUser(diskId, match).pipe(
190
+ Effect.repeat({
191
+ schedule: Schedule.spaced("500 millis"),
192
+ until: (user) => user !== undefined,
193
+ times: 14,
194
+ }),
195
+ );
196
+ });
197
+
198
+ /** What a disk user is created with. */
199
+ export interface DiskUserSpec {
200
+ readonly nickname: string;
201
+ readonly principal?: string;
202
+ readonly type?: "token" | "s3";
203
+ readonly readOnly?: boolean;
204
+ readonly expiresAt?: string;
205
+ readonly oneUse?: boolean;
206
+ }
207
+
208
+ const userType = (user: Pick<DiskUserSpec, "principal" | "type">) =>
209
+ user.principal !== undefined ? ("awssts" as const) : (user.type ?? "token");
210
+
211
+ const redacted = (value: string | Redacted.Redacted<string> | undefined) =>
212
+ value === undefined ? undefined : typeof value === "string" ? Redacted.make(value) : value;
213
+
214
+ /** Add a token, S3, or IAM user to a disk. */
215
+ export const addDiskUser = Effect.fn(function* (diskId: string, user: DiskUserSpec) {
216
+ const type = userType(user);
217
+ const limits = { readOnly: user.readOnly, expiresAt: user.expiresAt };
218
+ const { data } = yield* archil.addDiskUser({
219
+ id: diskId,
220
+ body:
221
+ type === "awssts"
222
+ ? { type, principal: user.principal!, ...limits, oneUse: user.oneUse }
223
+ : type === "s3"
224
+ ? { type, nickname: user.nickname, ...limits }
225
+ : { type, nickname: user.nickname, ...limits, oneUse: user.oneUse },
226
+ });
227
+ const missing =
228
+ type === "token"
229
+ ? !data.token || !data.identifier
230
+ : type === "s3"
231
+ ? !data.s3AccessKeyId || !data.s3SecretKey
232
+ : false;
233
+ if (missing) {
234
+ return yield* new DiskUserUnexpectedResponse({
235
+ message: `Archil returned no ${type === "s3" ? "S3 keys" : "token"} for disk user ${user.nickname} on ${diskId}`,
236
+ });
237
+ }
238
+ return {
239
+ diskId,
240
+ type,
241
+ identifier: data.identifier ?? data.s3AccessKeyId ?? user.principal ?? "",
242
+ nickname: data.nickname ?? user.nickname,
243
+ principal: user.principal,
244
+ token: redacted(data.token),
245
+ accessKeyId: data.s3AccessKeyId,
246
+ secretAccessKey: redacted(data.s3SecretKey),
247
+ readOnly: data.readOnly ?? user.readOnly ?? false,
248
+ expiresAt: data.expiresAt ?? user.expiresAt,
249
+ oneUse: data.oneUse ?? user.oneUse ?? false,
250
+ } satisfies DiskUserAttributes;
251
+ });
252
+
253
+ /** Revoke a disk user. Revoking a missing user (or one on a deleted disk) succeeds. */
254
+ export const removeDiskUser = Effect.fn(function* (attrs: {
255
+ diskId: string;
256
+ type: "token" | "awssts" | "s3";
257
+ identifier: string;
258
+ principal?: string;
259
+ }) {
260
+ yield* archil
261
+ .removeDiskUser({
262
+ id: attrs.diskId,
263
+ userType: attrs.type,
264
+ identifier: attrs.type === "awssts" ? undefined : attrs.identifier,
265
+ principal: attrs.type === "awssts" ? attrs.principal : undefined,
266
+ })
267
+ .pipe(Effect.catchTag("NotFound", () => Effect.void));
268
+ });
269
+
270
+ export const DiskUserProvider = () =>
271
+ Provider.effect(
272
+ DiskUser,
273
+ Effect.gen(function* () {
274
+ return {
275
+ stables: [
276
+ "diskId",
277
+ "type",
278
+ "identifier",
279
+ "nickname",
280
+ "principal",
281
+ "token",
282
+ "accessKeyId",
283
+ "secretAccessKey",
284
+ "readOnly",
285
+ "expiresAt",
286
+ "oneUse",
287
+ ],
288
+ diff: Effect.fn(function* ({ id, olds, news, output }) {
289
+ if (!isResolved(news)) return undefined;
290
+ const nickname = output?.nickname ?? (yield* userNickname(id, olds?.nickname));
291
+ if (
292
+ news.disk !== (output?.diskId ?? olds?.disk) ||
293
+ (news.nickname !== undefined && news.nickname !== nickname) ||
294
+ news.principal !== (output?.principal ?? olds?.principal) ||
295
+ userType(news) !== (output?.type ?? (olds ? userType(olds) : userType(news))) ||
296
+ (news.readOnly ?? false) !== (output?.readOnly ?? olds?.readOnly ?? false) ||
297
+ news.expiresAt !== (output?.expiresAt ?? olds?.expiresAt) ||
298
+ (news.oneUse ?? false) !== (output?.oneUse ?? olds?.oneUse ?? false)
299
+ ) {
300
+ return { action: "replace" } as const;
301
+ }
302
+ return undefined;
303
+ }),
304
+ read: Effect.fn(function* ({ id, olds, output }) {
305
+ if (!output && !olds) return undefined;
306
+ const diskId = output?.diskId ?? olds!.disk;
307
+ const nickname = output?.nickname ?? (yield* userNickname(id, olds?.nickname));
308
+ const observed = yield* findDiskUser(diskId, {
309
+ identifier: output?.identifier,
310
+ principal: output?.principal ?? olds?.principal,
311
+ nickname,
312
+ }).pipe(Effect.catchTag("NotFound", () => Effect.undefined));
313
+ if (!observed) return undefined;
314
+ return (
315
+ output ?? {
316
+ diskId,
317
+ type: observed.type ?? "token",
318
+ identifier: observed.identifier ?? observed.principal ?? "",
319
+ nickname: observed.nickname ?? nickname,
320
+ principal: observed.principal,
321
+ // Secrets are only shown at creation; reconcile re-creates.
322
+ token: undefined,
323
+ accessKeyId: observed.s3AccessKeyId,
324
+ secretAccessKey: undefined,
325
+ readOnly: observed.readOnly ?? false,
326
+ expiresAt: observed.expiresAt,
327
+ oneUse: observed.oneUse ?? false,
328
+ }
329
+ );
330
+ }),
331
+ reconcile: Effect.fn(function* ({ id, news, output, session }) {
332
+ const nickname = output?.nickname ?? (yield* userNickname(id, news.nickname));
333
+ // Observe
334
+ const observed = yield* findDiskUser(news.disk, {
335
+ identifier: output?.identifier,
336
+ principal: news.principal,
337
+ nickname,
338
+ });
339
+ const type = userType(news);
340
+ const hasSecret =
341
+ type === "awssts" ||
342
+ (type === "s3" ? output?.secretAccessKey !== undefined : output?.token !== undefined);
343
+ if (observed && output && hasSecret) return output;
344
+ // Ensure — a user we hold no secret for (a lost create, or an
345
+ // adoption) is revoked and created again.
346
+ if (observed) {
347
+ yield* session.note(`Re-creating disk user ${nickname}...`);
348
+ yield* removeDiskUser({
349
+ diskId: news.disk,
350
+ type: observed.type ?? "token",
351
+ identifier: observed.identifier ?? "",
352
+ principal: observed.principal,
353
+ });
354
+ }
355
+ return yield* addDiskUser(news.disk, {
356
+ nickname,
357
+ principal: news.principal,
358
+ type: news.type,
359
+ readOnly: news.readOnly,
360
+ expiresAt: news.expiresAt,
361
+ oneUse: news.oneUse,
362
+ });
363
+ }),
364
+ delete: Effect.fn(function* ({ output }) {
365
+ yield* removeDiskUser(output);
366
+ }),
367
+ };
368
+ }),
369
+ );
@@ -0,0 +1,62 @@
1
+ import * as Binding from "alchemy/Binding";
2
+ import type * as Effect from "effect/Effect";
3
+ import type { DiskDelegationsClient, ExecDiskClient } from "./Clients.ts";
4
+ import type { Disk } from "./Disk.ts";
5
+
6
+ export type { DiskDelegationsClient, ExecDiskClient, ExecResult, GrepOptions } from "./Clients.ts";
7
+
8
+ /**
9
+ * Run serverless commands against one disk, and search it, from a Worker,
10
+ * Lambda, or any other host. Each `exec` boots a fresh microVM with the
11
+ * disk mounted at `/mnt/archil`, runs the command to completion (up to five
12
+ * minutes), and shuts down, so you pay only for the call. Files the command
13
+ * writes stay on the disk.
14
+ *
15
+ * The host calls Archil with the API key you deploy with: Archil only
16
+ * creates API keys from the console and can't scope them to one disk.
17
+ *
18
+ * ### Running Commands
19
+ * **Example:** Count rows in a dataset from a Worker
20
+ * ```typescript
21
+ * export default class Api extends Cloudflare.Worker<Api>()(
22
+ * "Api",
23
+ * { main: import.meta.url },
24
+ * Effect.gen(function* () {
25
+ * const dataset = yield* Archil.ExecDisk(Dataset);
26
+ * return {
27
+ * fetch: Effect.gen(function* () {
28
+ * const result = yield* dataset.exec("wc -l /mnt/archil/*.csv");
29
+ * return HttpServerResponse.text(result.stdout);
30
+ * }),
31
+ * };
32
+ * }).pipe(Effect.provide(Archil.ExecDiskHttp)),
33
+ * ) {}
34
+ * ```
35
+ *
36
+ * ### Searching
37
+ * **Example:** Grep the whole disk in constant time
38
+ * ```typescript
39
+ * const hits = yield* dataset.grep({ pattern: "TODO", directory: "src", recursive: true });
40
+ * ```
41
+ *
42
+ * ### Recovering From a Crashed Mount
43
+ * A mounted client holds write delegations on the paths it writes. When it
44
+ * crashes without unmounting, reclaim them so other clients can write.
45
+ *
46
+ * **Example:** Revoke orphaned delegations
47
+ * ```typescript
48
+ * for (const d of yield* dataset.delegations()) {
49
+ * if (d.isOrphaned) yield* dataset.revokeDelegation(d);
50
+ * }
51
+ * ```
52
+ *
53
+ * @binding
54
+ * @product Disk
55
+ */
56
+ export interface ExecDisk extends Binding.Service<
57
+ ExecDisk,
58
+ "Archil.ExecDisk",
59
+ (disk: Disk) => Effect.Effect<ExecDiskClient & DiskDelegationsClient>
60
+ > {}
61
+
62
+ export const ExecDisk = Binding.Service<ExecDisk>("Archil.ExecDisk");
@@ -0,0 +1,25 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import { ApiAccess } from "./ArchilBindings.ts";
4
+ import { makeDiskDelegationsClient, makeExecDiskClient } from "./Clients.ts";
5
+ import type { Disk } from "./Disk.ts";
6
+ import { ExecDisk } from "./ExecDisk.ts";
7
+
8
+ /** {@link ExecDisk} over Archil's HTTP API, usable from any host. */
9
+ export const ExecDiskHttp = Layer.effect(
10
+ ExecDisk,
11
+ Effect.gen(function* () {
12
+ const access = yield* ApiAccess;
13
+ return Effect.fn(function* (disk: Disk) {
14
+ const authorize = yield* access();
15
+ const diskId = yield* disk.diskId;
16
+ return {
17
+ ...makeExecDiskClient(diskId, authorize),
18
+ ...makeDiskDelegationsClient(
19
+ Effect.map(diskId, (id) => ({ disk: id })),
20
+ authorize,
21
+ ),
22
+ };
23
+ });
24
+ }),
25
+ );
@@ -0,0 +1,100 @@
1
+ import * as archil from "@distilled.cloud/archil";
2
+ import * as Binding from "alchemy/Binding";
3
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import { ApiAccess } from "./ArchilBindings.ts";
7
+ import type { ExecResult } from "./Clients.ts";
8
+
9
+ /** One disk in a multi-disk exec. */
10
+ export interface ExecDisksMount {
11
+ /** Disk id (`dsk-…`). */
12
+ disk: string;
13
+ /** Mount this subdirectory of the disk instead of its root. */
14
+ subdirectory?: string;
15
+ /** Mount without write access. */
16
+ readOnly?: boolean;
17
+ /** Write without the disk's root delegation, so other writers can mount it too. */
18
+ conditional?: boolean;
19
+ }
20
+
21
+ export interface ExecDisksClient {
22
+ /**
23
+ * Run a shell command in a fresh microVM with several disks mounted.
24
+ * Keys are paths under `/mnt/archil`; values are disk ids or mounts.
25
+ */
26
+ exec(request: {
27
+ readonly disks: Record<string, string | ExecDisksMount>;
28
+ readonly command: string;
29
+ }): Effect.Effect<ExecResult, archil.ExecError, RuntimeContext>;
30
+ }
31
+
32
+ /**
33
+ * Run one serverless command against several disks at once, e.g. read a
34
+ * shared dataset and write into a user's own disk in the same call. Disks
35
+ * are named at run time, so it works with disks created per user.
36
+ *
37
+ * ### Running Across Disks
38
+ * **Example:** Copy a dataset into a user's disk
39
+ * ```typescript
40
+ * const exec = yield* Archil.ExecDisks();
41
+ * const result = yield* exec.exec({
42
+ * disks: {
43
+ * data: { disk: datasetId, readOnly: true },
44
+ * home: userDisk.diskId,
45
+ * },
46
+ * command: "cp -r /mnt/archil/data/samples /mnt/archil/home/",
47
+ * });
48
+ * ```
49
+ *
50
+ * @binding
51
+ * @product Disk
52
+ */
53
+ export interface ExecDisks extends Binding.Service<
54
+ ExecDisks,
55
+ "Archil.ExecDisks",
56
+ () => Effect.Effect<ExecDisksClient>
57
+ > {}
58
+
59
+ export const ExecDisks = Binding.Service<ExecDisks>("Archil.ExecDisks");
60
+
61
+ /** {@link ExecDisks} over Archil's HTTP API, usable from any host. */
62
+ export const ExecDisksHttp = Layer.effect(
63
+ ExecDisks,
64
+ Effect.gen(function* () {
65
+ const access = yield* ApiAccess;
66
+ return Effect.fn(function* () {
67
+ const authorize = yield* access();
68
+ return {
69
+ exec: (request) =>
70
+ authorize(
71
+ archil
72
+ .exec({
73
+ command: request.command,
74
+ disks: Object.fromEntries(
75
+ Object.entries(request.disks).map(([path, mount]) => [
76
+ path,
77
+ typeof mount === "string"
78
+ ? mount
79
+ : {
80
+ disk: mount.disk,
81
+ subdirectory: mount.subdirectory,
82
+ readOnly: mount.readOnly,
83
+ conditional: mount.conditional,
84
+ },
85
+ ]),
86
+ ),
87
+ })
88
+ .pipe(
89
+ Effect.map(({ data }) => ({
90
+ exitCode: data.exitCode,
91
+ stdout: data.stdout,
92
+ stderr: data.stderr,
93
+ timing: data.timing,
94
+ })),
95
+ ),
96
+ ),
97
+ } satisfies ExecDisksClient;
98
+ });
99
+ }),
100
+ );