@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.
- package/LICENSE +201 -0
- package/README.md +57 -4
- package/lib/AWS/S3Disk.d.ts +68 -0
- package/lib/AWS/S3Disk.d.ts.map +1 -0
- package/lib/AWS/S3Disk.js +100 -0
- package/lib/AWS/S3Disk.js.map +1 -0
- package/lib/AWS/index.d.ts +2 -0
- package/lib/AWS/index.d.ts.map +1 -0
- package/lib/AWS/index.js +2 -0
- package/lib/AWS/index.js.map +1 -0
- package/lib/ApiToken.d.ts +63 -0
- package/lib/ApiToken.d.ts.map +1 -0
- package/lib/ApiToken.js +121 -0
- package/lib/ApiToken.js.map +1 -0
- package/lib/ArchilBindings.d.ts +65 -0
- package/lib/ArchilBindings.d.ts.map +1 -0
- package/lib/ArchilBindings.js +128 -0
- package/lib/ArchilBindings.js.map +1 -0
- package/lib/AuthProvider.d.ts +40 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +87 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +27 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/Branch.d.ts +101 -0
- package/lib/Branch.d.ts.map +1 -0
- package/lib/Branch.js +117 -0
- package/lib/Branch.js.map +1 -0
- package/lib/BranchDisk.d.ts +42 -0
- package/lib/BranchDisk.d.ts.map +1 -0
- package/lib/BranchDisk.js +14 -0
- package/lib/BranchDisk.js.map +1 -0
- package/lib/Checkpoint.d.ts +78 -0
- package/lib/Checkpoint.d.ts.map +1 -0
- package/lib/Checkpoint.js +95 -0
- package/lib/Checkpoint.js.map +1 -0
- package/lib/Clients.d.ts +369 -0
- package/lib/Clients.d.ts.map +1 -0
- package/lib/Clients.js +334 -0
- package/lib/Clients.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +69 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +21 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateDisk.d.ts +90 -0
- package/lib/CreateDisk.d.ts.map +1 -0
- package/lib/CreateDisk.js +47 -0
- package/lib/CreateDisk.js.map +1 -0
- package/lib/CreateImage.d.ts +42 -0
- package/lib/CreateImage.d.ts.map +1 -0
- package/lib/CreateImage.js +19 -0
- package/lib/CreateImage.js.map +1 -0
- package/lib/CreateSandbox.d.ts +104 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +98 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/Credentials.d.ts +44 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +57 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Disk.d.ts +283 -0
- package/lib/Disk.d.ts.map +1 -0
- package/lib/Disk.js +294 -0
- package/lib/Disk.js.map +1 -0
- package/lib/DiskObjectsHttp.d.ts +20 -0
- package/lib/DiskObjectsHttp.d.ts.map +1 -0
- package/lib/DiskObjectsHttp.js +51 -0
- package/lib/DiskObjectsHttp.js.map +1 -0
- package/lib/DiskUser.d.ts +189 -0
- package/lib/DiskUser.d.ts.map +1 -0
- package/lib/DiskUser.js +243 -0
- package/lib/DiskUser.js.map +1 -0
- package/lib/ExecDisk.d.ts +57 -0
- package/lib/ExecDisk.d.ts.map +1 -0
- package/lib/ExecDisk.js +3 -0
- package/lib/ExecDisk.js.map +1 -0
- package/lib/ExecDiskHttp.d.ts +5 -0
- package/lib/ExecDiskHttp.d.ts.map +1 -0
- package/lib/ExecDiskHttp.js +18 -0
- package/lib/ExecDiskHttp.js.map +1 -0
- package/lib/ExecDisks.d.ts +54 -0
- package/lib/ExecDisks.d.ts.map +1 -0
- package/lib/ExecDisks.js +37 -0
- package/lib/ExecDisks.js.map +1 -0
- package/lib/Image.d.ts +188 -0
- package/lib/Image.d.ts.map +1 -0
- package/lib/Image.js +191 -0
- package/lib/Image.js.map +1 -0
- package/lib/InferEnv.d.ts +26 -0
- package/lib/InferEnv.d.ts.map +1 -0
- package/lib/InferEnv.js +1 -0
- package/lib/InferEnv.js.map +1 -0
- package/lib/MountDisk.d.ts +190 -0
- package/lib/MountDisk.d.ts.map +1 -0
- package/lib/MountDisk.js +227 -0
- package/lib/MountDisk.js.map +1 -0
- package/lib/Providers.d.ts +25 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +48 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadDisk.d.ts +55 -0
- package/lib/ReadDisk.d.ts.map +1 -0
- package/lib/ReadDisk.js +3 -0
- package/lib/ReadDisk.js.map +1 -0
- package/lib/ReadWriteDisk.d.ts +26 -0
- package/lib/ReadWriteDisk.d.ts.map +1 -0
- package/lib/ReadWriteDisk.js +3 -0
- package/lib/ReadWriteDisk.js.map +1 -0
- package/lib/Sandbox.d.ts +507 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +729 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxOptions.d.ts +185 -0
- package/lib/SandboxOptions.d.ts.map +1 -0
- package/lib/SandboxOptions.js +102 -0
- package/lib/SandboxOptions.js.map +1 -0
- package/lib/SandboxPortToken.d.ts +64 -0
- package/lib/SandboxPortToken.d.ts.map +1 -0
- package/lib/SandboxPortToken.js +77 -0
- package/lib/SandboxPortToken.js.map +1 -0
- package/lib/WriteDisk.d.ts +37 -0
- package/lib/WriteDisk.d.ts.map +1 -0
- package/lib/WriteDisk.js +3 -0
- package/lib/WriteDisk.js.map +1 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/DeployerKey.d.ts +21 -0
- package/lib/internal/DeployerKey.d.ts.map +1 -0
- package/lib/internal/DeployerKey.js +34 -0
- package/lib/internal/DeployerKey.js.map +1 -0
- package/lib/internal/archil.d.ts +87 -0
- package/lib/internal/archil.d.ts.map +1 -0
- package/lib/internal/archil.js +154 -0
- package/lib/internal/archil.js.map +1 -0
- package/lib/internal/hosted.d.ts +90 -0
- package/lib/internal/hosted.d.ts.map +1 -0
- package/lib/internal/hosted.js +291 -0
- package/lib/internal/hosted.js.map +1 -0
- package/package.json +79 -3
- package/src/AWS/S3Disk.ts +125 -0
- package/src/AWS/index.ts +1 -0
- package/src/ApiToken.ts +168 -0
- package/src/ArchilBindings.ts +179 -0
- package/src/AuthProvider.ts +122 -0
- package/src/Bootstrap.ts +54 -0
- package/src/Branch.ts +199 -0
- package/src/BranchDisk.ts +57 -0
- package/src/Checkpoint.ts +145 -0
- package/src/Clients.ts +954 -0
- package/src/ConnectSandbox.ts +91 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateDisk.ts +165 -0
- package/src/CreateImage.ts +73 -0
- package/src/CreateSandbox.ts +258 -0
- package/src/Credentials.ts +101 -0
- package/src/Disk.ts +468 -0
- package/src/DiskObjectsHttp.ts +64 -0
- package/src/DiskUser.ts +369 -0
- package/src/ExecDisk.ts +62 -0
- package/src/ExecDiskHttp.ts +25 -0
- package/src/ExecDisks.ts +100 -0
- package/src/Image.ts +316 -0
- package/src/InferEnv.ts +27 -0
- package/src/MountDisk.ts +466 -0
- package/src/Providers.ts +75 -0
- package/src/ReadDisk.ts +61 -0
- package/src/ReadWriteDisk.ts +31 -0
- package/src/Sandbox.ts +1064 -0
- package/src/SandboxOptions.ts +230 -0
- package/src/SandboxPortToken.ts +125 -0
- package/src/WriteDisk.ts +42 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +59 -0
- package/src/internal/DeployerKey.ts +63 -0
- package/src/internal/archil.ts +273 -0
- 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
|
+
);
|
package/src/WriteDisk.ts
ADDED
|
@@ -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");
|
package/src/globals.d.ts
ADDED
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
|
+
);
|