@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,230 @@
1
+ import type * as archil from "@distilled.cloud/archil";
2
+ import * as Data from "effect/Data";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Redacted from "effect/Redacted";
5
+ import type { Disk } from "./Disk.ts";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ /**
9
+ * An allowed destination, with optional changes to outbound HTTPS requests
10
+ * that match it. Headers set here overwrite whatever the sandbox sends, so
11
+ * a credential injected at the boundary never enters the sandbox.
12
+ */
13
+ export interface SandboxEgressTarget {
14
+ /** IPv4 address, CIDR range, exact domain, or `*.`-wildcard domain. */
15
+ target: string;
16
+ /** HTTP headers to set on matching HTTPS requests. */
17
+ headers?: Record<string, string | Redacted.Redacted<string>>;
18
+ /**
19
+ * Forward matching requests to this public HTTPS URL instead of their
20
+ * destination, such as a Worker that adds a credential. Archil appends
21
+ * the original path and query, applies `headers` first, and names the
22
+ * destination and the sandbox in `archil-forwarded-host`,
23
+ * `archil-forwarded-scheme`, `archil-forwarded-port`,
24
+ * `archil-forwarded-path` and `archil-sandbox-id` headers.
25
+ */
26
+ forwardUrl?: string;
27
+ }
28
+
29
+ /**
30
+ * Outbound network policy. Deny entries win when an address or domain
31
+ * matches both lists. Wildcards match subdomains, not the apex domain.
32
+ */
33
+ export interface SandboxEgress {
34
+ /** What happens to destinations neither list names. */
35
+ default: "allow" | "deny";
36
+ /** Allowed destinations: addresses, CIDRs, domains, or targets with request changes. */
37
+ allow?: ReadonlyArray<string | SandboxEgressTarget>;
38
+ /** Denied addresses, CIDRs, or domains. */
39
+ deny?: ReadonlyArray<string>;
40
+ /**
41
+ * Hosts whose in-flight HTTP(S) requests finish before the sandbox
42
+ * pauses (ports 80 and 443 only). Doesn't grant access.
43
+ */
44
+ drainOnPause?: ReadonlyArray<string>;
45
+ }
46
+
47
+ /**
48
+ * How a mount shares the disk with other clients.
49
+ *
50
+ * - `exclusive` — this client owns writes to the whole disk (local-disk
51
+ * speed); other clients can still mount `read-only`.
52
+ * - `shared` — many clients; the disk is read-only until a path is checked
53
+ * out with `archil checkout <path>`. FUSE client only.
54
+ * - `conditional` — many writers; writes go to the server unless a path is
55
+ * checked out (remote-filesystem speed).
56
+ * - `read-only` — no writes.
57
+ */
58
+ export type MountMode = "exclusive" | "shared" | "conditional" | "read-only";
59
+
60
+ /** A disk known at run time, e.g. a `DiskHandle` from `CreateDisk`. */
61
+ export interface DiskRef {
62
+ readonly diskId: string;
63
+ }
64
+
65
+ /**
66
+ * The disk of a mount entry: a declared `Archil.Disk` (or the Effect that
67
+ * declares it), or a run-time handle with its `diskId`.
68
+ */
69
+ export type MountableDisk = Disk | Effect.Effect<Disk, never, Providers> | DiskRef;
70
+
71
+ /** How a disk is mounted, in Archil's own terms. */
72
+ export interface DiskMountOptions {
73
+ /** Absolute directory to mount at. */
74
+ readonly path: string;
75
+ /** @default "exclusive" */
76
+ readonly mode?: MountMode;
77
+ /** Mount only this subdirectory of the disk. */
78
+ readonly subdirectory?: string;
79
+ /**
80
+ * Milliseconds to wait for the disk's exclusive root delegation before
81
+ * the mount fails. Only for `exclusive` mounts in an `Archil.Sandbox`.
82
+ */
83
+ readonly queueMs?: number;
84
+ /** Mount a branch of the disk instead of its root. FUSE client only. */
85
+ readonly branch?: string;
86
+ /**
87
+ * Take ownership of the root from a client that crashed without
88
+ * unmounting; that client's outstanding writes are lost. FUSE client only.
89
+ */
90
+ readonly force?: boolean;
91
+ }
92
+
93
+ /** A disk mounted into a sandbox (`mounts`), or by `MountDisk`. */
94
+ export interface DiskMount extends DiskMountOptions {
95
+ readonly disk: MountableDisk;
96
+ }
97
+
98
+ /** A mount option the target doesn't support was set. */
99
+ export class MountOptionUnsupported extends Data.TaggedError("Archil.MountOptionUnsupported")<{
100
+ readonly option: string;
101
+ readonly message: string;
102
+ }> {}
103
+
104
+ /** A mount entry whose disk isn't known yet. */
105
+ export class MountDiskUnresolved extends Data.TaggedError("Archil.MountDiskUnresolved")<{
106
+ readonly path: string;
107
+ readonly message: string;
108
+ }> {}
109
+
110
+ /** The disk id of a resolved mount entry's disk, if it has one. */
111
+ export const mountDiskId = (disk: unknown): string | undefined =>
112
+ typeof disk === "object" && disk !== null && "diskId" in disk && typeof disk.diskId === "string"
113
+ ? disk.diskId
114
+ : undefined;
115
+
116
+ /**
117
+ * The wire shape of a sandbox mount. Archil sandboxes mount natively: no
118
+ * `shared` mode, branches, or `force`.
119
+ */
120
+ export const toSandboxMount = Effect.fn(function* (diskId: string, mount: DiskMountOptions) {
121
+ const unsupported =
122
+ mount.mode === "shared"
123
+ ? 'mode: "shared"'
124
+ : mount.branch !== undefined
125
+ ? "branch"
126
+ : mount.force
127
+ ? "force"
128
+ : undefined;
129
+ if (unsupported !== undefined) {
130
+ return yield* new MountOptionUnsupported({
131
+ option: unsupported,
132
+ message: `An Archil sandbox can't mount a disk with ${unsupported} (only the FUSE client can); mount it at ${mount.path} without it.`,
133
+ });
134
+ }
135
+ if (mount.queueMs !== undefined && (mount.mode ?? "exclusive") !== "exclusive") {
136
+ return yield* new MountOptionUnsupported({
137
+ option: "queueMs",
138
+ message: `queueMs only applies to exclusive mounts (${mount.path} is ${mount.mode}).`,
139
+ });
140
+ }
141
+ return {
142
+ disk_id: diskId,
143
+ path: mount.path,
144
+ subdirectory: mount.subdirectory,
145
+ read_only: mount.mode === "read-only",
146
+ conditional: mount.mode === "conditional",
147
+ queue_ms: mount.queueMs,
148
+ } satisfies archil.SandboxMount;
149
+ });
150
+
151
+ /** A comparable form of a wire mount, defaults filled in. */
152
+ export const mountIdentity = (mount: archil.SandboxMount) => ({
153
+ disk_id: mount.disk_id,
154
+ path: mount.path ?? "/mnt/archil",
155
+ subdirectory: mount.subdirectory || undefined,
156
+ read_only: mount.read_only ?? false,
157
+ conditional: mount.conditional ?? false,
158
+ queue_ms: mount.queue_ms ?? undefined,
159
+ });
160
+
161
+ /** A port a sandbox serves publicly. */
162
+ export interface SandboxEndpoint {
163
+ port: number;
164
+ /** Public hostname that routes to the port. */
165
+ hostname: string;
166
+ }
167
+
168
+ const secretValue = (value: string | Redacted.Redacted<string>) =>
169
+ typeof value === "string" ? value : Redacted.value(value);
170
+
171
+ /** The wire shape of an egress policy. */
172
+ export const toEgressPolicy = (egress: SandboxEgress): archil.SandboxEgressPolicy => ({
173
+ default: egress.default,
174
+ allow: egress.allow?.map((entry) =>
175
+ typeof entry === "string"
176
+ ? entry
177
+ : {
178
+ target: entry.target,
179
+ transform: entry.headers
180
+ ? {
181
+ headers: Object.fromEntries(
182
+ Object.entries(entry.headers).map(([k, v]) => [k, secretValue(v)]),
183
+ ),
184
+ }
185
+ : undefined,
186
+ forward_url: entry.forwardUrl,
187
+ },
188
+ ),
189
+ deny: egress.deny ? [...egress.deny] : undefined,
190
+ drain_on_pause: egress.drainOnPause ? [...egress.drainOnPause] : undefined,
191
+ });
192
+
193
+ const isArchilEgress = (entry: string | object) =>
194
+ typeof entry === "string" && /^(mount|data)\.\*\.archil\.com$/.test(entry);
195
+
196
+ /**
197
+ * Split off the allow-list entries Archil adds itself to a sandbox that
198
+ * mounts disks (`mount.*.archil.com`, `data.*.archil.com`). They aren't
199
+ * part of the desired policy, but an update must keep them.
200
+ */
201
+ export const splitArchilEgress = (egress: archil.SandboxEgressPolicy | undefined) => ({
202
+ managed: (egress?.allow ?? []).filter(isArchilEgress),
203
+ policy: egress && { ...egress, allow: egress.allow?.filter((entry) => !isArchilEgress(entry)) },
204
+ });
205
+
206
+ /** Environment values, unwrapped for the wire. */
207
+ export const toEnv = (env: Record<string, string | Redacted.Redacted<string>> | undefined) =>
208
+ env === undefined
209
+ ? undefined
210
+ : Object.fromEntries(Object.entries(env).map(([k, v]) => [k, secretValue(v)]));
211
+
212
+ /** A comparable form of an egress policy (header values are hashed out). */
213
+ export const egressIdentity = (egress: SandboxEgress | undefined) =>
214
+ egress === undefined
215
+ ? undefined
216
+ : {
217
+ ...egress,
218
+ allow: egress.allow?.map((entry) =>
219
+ typeof entry === "string"
220
+ ? entry
221
+ : {
222
+ ...entry,
223
+ headers: entry.headers
224
+ ? Object.fromEntries(
225
+ Object.entries(entry.headers).map(([k, v]) => [k, secretValue(v)]),
226
+ )
227
+ : undefined,
228
+ },
229
+ ),
230
+ };
@@ -0,0 +1,125 @@
1
+ import * as archil from "@distilled.cloud/archil";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import * as Provider from "alchemy/Provider";
4
+ import { Resource } from "alchemy/Resource";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Redacted from "effect/Redacted";
7
+ import type { Providers } from "./Providers.ts";
8
+
9
+ export interface SandboxPortTokenProps {
10
+ /** Sandbox id (e.g. `sandbox.sandboxId`). Changing it replaces the token. */
11
+ sandbox: string;
12
+ /** The exposed port the token opens. Changing it replaces the token. */
13
+ port: number;
14
+ /**
15
+ * How long the token lasts, e.g. `"24h"` or `"30m"`. Omit for a token
16
+ * that lasts until revoked. Changing it replaces the token.
17
+ */
18
+ ttl?: string;
19
+ }
20
+
21
+ export interface SandboxPortTokenAttributes {
22
+ /** Token id. */
23
+ tokenId: string;
24
+ sandboxId: string;
25
+ port: number;
26
+ /** Hostname the token opens. */
27
+ hostname: string;
28
+ /** The token. Send it with requests to `hostname`. */
29
+ token: Redacted.Redacted<string>;
30
+ /** When the token expires, if it does. */
31
+ expiresAt: string | undefined;
32
+ }
33
+
34
+ export type SandboxPortToken = Resource<
35
+ "Archil.SandboxPortToken",
36
+ SandboxPortTokenProps,
37
+ SandboxPortTokenAttributes,
38
+ never,
39
+ Providers
40
+ >;
41
+
42
+ /**
43
+ * A token that opens one exposed port of one sandbox, for a client that
44
+ * shouldn't hold your API key. Archil only shows the token once, so Alchemy
45
+ * stores it in state as a `Redacted` value; destroying the resource revokes
46
+ * it.
47
+ * @see https://docs.archil.com/compute/sandboxes/introduction
48
+ *
49
+ * ### Creating a Port Token
50
+ * **Example:** A day-long token for a preview
51
+ * ```typescript
52
+ * const token = yield* Archil.SandboxPortToken("PreviewToken", {
53
+ * sandbox: preview.sandboxId,
54
+ * port: 3000,
55
+ * ttl: "24h",
56
+ * });
57
+ * ```
58
+ *
59
+ * @resource
60
+ * @product Sandbox
61
+ */
62
+ export const SandboxPortToken = Resource<SandboxPortToken>("Archil.SandboxPortToken");
63
+
64
+ /** Mint a port token. */
65
+ export const createPortToken = Effect.fn(function* (sid: string, port: number, ttl?: string) {
66
+ const { data } = yield* archil.createSandboxPortToken({ sid, port, ttl });
67
+ return {
68
+ tokenId: data.id,
69
+ sandboxId: sid,
70
+ port: data.port,
71
+ hostname: data.hostname,
72
+ token: typeof data.token === "string" ? Redacted.make(data.token) : data.token,
73
+ expiresAt: data.expires_at,
74
+ } satisfies SandboxPortTokenAttributes;
75
+ });
76
+
77
+ /** Revoke a port token. Revoking a missing token succeeds. */
78
+ export const revokePortToken = (sid: string, tokenId: string) =>
79
+ archil.deleteSandboxPortToken({ sid, token_id: tokenId }).pipe(
80
+ Effect.asVoid,
81
+ Effect.catchTag("NotFound", () => Effect.void),
82
+ );
83
+
84
+ export const SandboxPortTokenProvider = () =>
85
+ Provider.effect(
86
+ SandboxPortToken,
87
+ Effect.gen(function* () {
88
+ return {
89
+ stables: ["tokenId", "sandboxId", "port", "hostname", "token", "expiresAt"],
90
+ diff: Effect.fn(function* ({ olds, news }) {
91
+ if (!isResolved(news) || olds === undefined) return undefined;
92
+ if (news.sandbox !== olds.sandbox || news.port !== olds.port || news.ttl !== olds.ttl) {
93
+ return { action: "replace" } as const;
94
+ }
95
+ return undefined;
96
+ }),
97
+ read: Effect.fn(function* ({ output }) {
98
+ if (!output) return undefined;
99
+ return yield* archil
100
+ .getSandboxPortToken({ sid: output.sandboxId, token_id: output.tokenId })
101
+ .pipe(
102
+ Effect.map(() => output),
103
+ Effect.catchTag("NotFound", () => Effect.undefined),
104
+ );
105
+ }),
106
+ reconcile: Effect.fn(function* ({ news, output }) {
107
+ // Observe — a live token we minted is kept; tokens can't be updated.
108
+ if (output) {
109
+ const live = yield* archil
110
+ .getSandboxPortToken({ sid: output.sandboxId, token_id: output.tokenId })
111
+ .pipe(
112
+ Effect.as(true),
113
+ Effect.catchTag("NotFound", () => Effect.succeed(false)),
114
+ );
115
+ if (live) return output;
116
+ }
117
+ // Ensure
118
+ return yield* createPortToken(news.sandbox, news.port, news.ttl);
119
+ }),
120
+ delete: Effect.fn(function* ({ output }) {
121
+ yield* revokePortToken(output.sandboxId, output.tokenId);
122
+ }),
123
+ };
124
+ }),
125
+ );
@@ -0,0 +1,42 @@
1
+ import * as Binding from "alchemy/Binding";
2
+ import type * as Effect from "effect/Effect";
3
+ import type { WriteDiskClient } from "./Clients.ts";
4
+ import type { Disk } from "./Disk.ts";
5
+ import type { DiskObjectsOptions } from "./ReadDisk.ts";
6
+
7
+ export type { WriteDiskClient } from "./Clients.ts";
8
+
9
+ /**
10
+ * Write a disk's files as objects over Archil's S3-compatible API: put,
11
+ * append, copy, delete, and multipart uploads of any size. The host gets
12
+ * its own read-write S3 user on this disk, so it can't reach any other
13
+ * disk and never holds an API key. A write that conflicts with a mounted
14
+ * client's write delegation fails with `ObjectConflict`; `ExecDisk`'s
15
+ * `delegations` and `revokeDelegation` reclaim paths a crashed client
16
+ * still holds.
17
+ *
18
+ * ### Writing Files
19
+ * **Example:** Append events to a log on a disk
20
+ * ```typescript
21
+ * const files = yield* Archil.WriteDisk(Dataset);
22
+ * yield* files.append("events.ndjson", JSON.stringify(event) + "\n");
23
+ * ```
24
+ *
25
+ * ### Uploading Large Files
26
+ * **Example:** Stream a file of any size onto the disk
27
+ * ```typescript
28
+ * const files = yield* Archil.WriteDisk(Dataset);
29
+ * const request = yield* HttpServerRequest;
30
+ * yield* files.upload("uploads/video.mp4", request.stream, { contentType: "video/mp4" });
31
+ * ```
32
+ *
33
+ * @binding
34
+ * @product Disk
35
+ */
36
+ export interface WriteDisk extends Binding.Service<
37
+ WriteDisk,
38
+ "Archil.WriteDisk",
39
+ (disk: Disk, options?: DiskObjectsOptions) => Effect.Effect<WriteDiskClient>
40
+ > {}
41
+
42
+ export const WriteDisk = Binding.Service<WriteDisk>("Archil.WriteDisk");
@@ -0,0 +1,2 @@
1
+ // `globalThis.__ALCHEMY_RUNTIME__`, declared by alchemy's Phase module.
2
+ import type {} from "alchemy/Phase";
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ export * from "./ApiToken.ts";
2
+ export * from "./AuthProvider.ts";
3
+ export * from "./Branch.ts";
4
+ export * from "./BranchDisk.ts";
5
+ export * from "./Checkpoint.ts";
6
+ export {
7
+ SandboxPortNotExposed,
8
+ type Command,
9
+ type DiskDelegationsClient,
10
+ type DiskHandle,
11
+ type ExecOptions,
12
+ type ObjectsTransportError,
13
+ type ProcessCallError,
14
+ type PtyConnection,
15
+ type PtyOptions,
16
+ type ReadWriteDiskClient,
17
+ type SandboxFiles,
18
+ type SandboxHandle,
19
+ type SandboxOps,
20
+ type SandboxPort,
21
+ type SandboxPortError,
22
+ type SandboxProcess,
23
+ type SandboxProcessExit,
24
+ type SandboxProcessOutput,
25
+ type ShareError,
26
+ type StartOptions,
27
+ } from "./Clients.ts";
28
+ export * from "./ConnectSandbox.ts";
29
+ export * from "./CreateDisk.ts";
30
+ export * from "./CreateImage.ts";
31
+ export * from "./CreateSandbox.ts";
32
+ export {
33
+ Account,
34
+ account,
35
+ Credentials,
36
+ CredentialsFromEnv,
37
+ DEFAULT_REGION,
38
+ fromApiKey,
39
+ fromAuthProvider,
40
+ REGIONS,
41
+ type ArchilAccount,
42
+ } from "./Credentials.ts";
43
+ export * from "./Disk.ts";
44
+ export * from "./DiskObjectsHttp.ts";
45
+ export * from "./DiskUser.ts";
46
+ export * from "./ExecDisk.ts";
47
+ export * from "./ExecDiskHttp.ts";
48
+ export * from "./ExecDisks.ts";
49
+ export * from "./Image.ts";
50
+ export { ArchilFailed, ArchilNotReady } from "./internal/archil.ts";
51
+ export * from "./MountDisk.ts";
52
+ export * from "./Providers.ts";
53
+ export * from "./ReadDisk.ts";
54
+ export * from "./ReadWriteDisk.ts";
55
+ export * from "./Sandbox.ts";
56
+ export * from "./SandboxOptions.ts";
57
+ export * from "./SandboxPortToken.ts";
58
+ export type { InferEnv } from "./InferEnv.ts";
59
+ export * from "./WriteDisk.ts";
@@ -0,0 +1,63 @@
1
+ import { Credentials, DEFAULT_REGION } from "@distilled.cloud/archil/Credentials";
2
+ import * as Provider from "alchemy/Provider";
3
+ import { Resource } from "alchemy/Resource";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Redacted from "effect/Redacted";
6
+ import type { Providers } from "../Providers.ts";
7
+
8
+ export interface DeployerKeyAttributes {
9
+ /** The API key the stack deploys with. */
10
+ apiKey: Redacted.Redacted<string>;
11
+ /** Archil region the key acts in. */
12
+ region: string;
13
+ }
14
+
15
+ /**
16
+ * The deploying account's API key, recorded so bindings can hand it to
17
+ * their host. Archil only mints API keys from a console session (an API
18
+ * key gets a 401), and its keys can't be scoped, so a host gets the same
19
+ * key the stack deploys with. Rotating the deploy key updates every host.
20
+ */
21
+ export type DeployerKey = Resource<
22
+ "Archil.DeployerKey",
23
+ {},
24
+ DeployerKeyAttributes,
25
+ never,
26
+ Providers
27
+ >;
28
+
29
+ export const DeployerKey = Resource<DeployerKey>("Archil.DeployerKey");
30
+
31
+ const current = Effect.gen(function* () {
32
+ const { apiKey, region } = yield* yield* Credentials;
33
+ return { apiKey, region: region ?? DEFAULT_REGION } satisfies DeployerKeyAttributes;
34
+ });
35
+
36
+ export const DeployerKeyProvider = () =>
37
+ Provider.effect(
38
+ DeployerKey,
39
+ Effect.gen(function* () {
40
+ return {
41
+ // Nothing lives in Archil: the key belongs to the deployer.
42
+ nuke: { skip: true },
43
+ diff: Effect.fn(function* ({ output }) {
44
+ if (output === undefined) return undefined;
45
+ const next = yield* current;
46
+ if (
47
+ Redacted.value(next.apiKey) !== Redacted.value(output.apiKey) ||
48
+ next.region !== output.region
49
+ ) {
50
+ return { action: "update" } as const;
51
+ }
52
+ return undefined;
53
+ }),
54
+ read: Effect.fn(function* ({ output }) {
55
+ return output;
56
+ }),
57
+ reconcile: Effect.fn(function* () {
58
+ return yield* current;
59
+ }),
60
+ delete: () => Effect.void,
61
+ };
62
+ }),
63
+ );