@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.
- 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
package/src/ApiToken.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as archil from "@distilled.cloud/archil";
|
|
2
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import { 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 { account } from "./Credentials.ts";
|
|
9
|
+
import { isAlchemyName, tokenName } from "./internal/archil.ts";
|
|
10
|
+
import type { Providers } from "./Providers.ts";
|
|
11
|
+
|
|
12
|
+
export interface ApiTokenProps {
|
|
13
|
+
/**
|
|
14
|
+
* Token name, unique within your account. If omitted, a unique name is
|
|
15
|
+
* generated from the stack, stage, and logical ID. Changing it replaces
|
|
16
|
+
* the token.
|
|
17
|
+
*/
|
|
18
|
+
name?: string;
|
|
19
|
+
/** Description shown in the console. Changing it replaces the token. */
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ApiTokenAttributes {
|
|
24
|
+
/** Token id. */
|
|
25
|
+
tokenId: string;
|
|
26
|
+
/** Token name. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Last characters of the key, as shown in the console. */
|
|
29
|
+
tokenSuffix: string | undefined;
|
|
30
|
+
/** The API key (`key-…`). */
|
|
31
|
+
token: Redacted.Redacted<string>;
|
|
32
|
+
/** Archil region the key was created for, e.g. `aws-us-east-1`. */
|
|
33
|
+
region: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ApiToken = Resource<
|
|
37
|
+
"Archil.ApiToken",
|
|
38
|
+
ApiTokenProps,
|
|
39
|
+
ApiTokenAttributes,
|
|
40
|
+
never,
|
|
41
|
+
Providers
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* An Archil API key. It can call the whole control plane of your account:
|
|
46
|
+
* Archil has no scoped keys, so give each app or host its own key and
|
|
47
|
+
* revoke it on its own. To mount one disk, use a {@link DiskUser}, whose
|
|
48
|
+
* token works only for that disk.
|
|
49
|
+
*
|
|
50
|
+
* Archil only shows the key once, when it's created, so Alchemy stores it
|
|
51
|
+
* in state as a `Redacted` value. Changing a prop creates a new key, and
|
|
52
|
+
* destroying it revokes the key.
|
|
53
|
+
*
|
|
54
|
+
* Archil refuses API keys on its API key operations: deploying with one
|
|
55
|
+
* fails with `Unauthorized`, the same error a bad key gets.
|
|
56
|
+
* @see https://docs.archil.com/api-reference
|
|
57
|
+
*
|
|
58
|
+
* ### Creating a Key
|
|
59
|
+
* **Example:** A key for CI
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const key = yield* Archil.ApiToken("Ci", { description: "GitHub Actions" });
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @resource
|
|
65
|
+
* @product API Key
|
|
66
|
+
*/
|
|
67
|
+
export const ApiToken = Resource<ApiToken>("Archil.ApiToken");
|
|
68
|
+
|
|
69
|
+
const observe = Effect.fn(function* (match: { tokenId?: string; name: string }) {
|
|
70
|
+
const { data } = yield* archil.listApiTokens({});
|
|
71
|
+
return (data.tokens ?? []).find((token) =>
|
|
72
|
+
match.tokenId !== undefined ? token.id === match.tokenId : token.name === match.name,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const revoke = (tokenId: string) =>
|
|
77
|
+
archil.deleteToken({ id: tokenId }).pipe(
|
|
78
|
+
Effect.asVoid,
|
|
79
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
/** Create an API key and return its attributes. */
|
|
83
|
+
export const createApiToken = Effect.fn(function* (name: string, description?: string) {
|
|
84
|
+
const { data } = yield* archil.createApiToken({ name, description });
|
|
85
|
+
const { region } = yield* account;
|
|
86
|
+
return {
|
|
87
|
+
tokenId: data.id,
|
|
88
|
+
name,
|
|
89
|
+
tokenSuffix: data.tokenSuffix,
|
|
90
|
+
token: typeof data.token === "string" ? Redacted.make(data.token) : data.token,
|
|
91
|
+
region,
|
|
92
|
+
} satisfies ApiTokenAttributes;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const ApiTokenProvider = () =>
|
|
96
|
+
Provider.effect(
|
|
97
|
+
ApiToken,
|
|
98
|
+
Effect.gen(function* () {
|
|
99
|
+
return {
|
|
100
|
+
stables: ["tokenId", "name", "tokenSuffix", "token", "region"],
|
|
101
|
+
list: Effect.fn(function* () {
|
|
102
|
+
const { region } = yield* account;
|
|
103
|
+
const { data } = yield* archil.listApiTokens({});
|
|
104
|
+
return (data.tokens ?? [])
|
|
105
|
+
.filter((token) => isAlchemyName(token.name))
|
|
106
|
+
.map((token) => ({
|
|
107
|
+
tokenId: token.id ?? "",
|
|
108
|
+
name: token.name ?? "",
|
|
109
|
+
tokenSuffix: token.tokenSuffix,
|
|
110
|
+
token: Redacted.make(""),
|
|
111
|
+
region,
|
|
112
|
+
}));
|
|
113
|
+
}),
|
|
114
|
+
diff: Effect.fn(function* ({ id, olds, news, output }) {
|
|
115
|
+
if (!isResolved(news)) return undefined;
|
|
116
|
+
const name = output?.name ?? (yield* tokenName(id, olds?.name));
|
|
117
|
+
if (
|
|
118
|
+
(news.name !== undefined && news.name !== name) ||
|
|
119
|
+
news.description !== olds?.description
|
|
120
|
+
) {
|
|
121
|
+
return {
|
|
122
|
+
action: "replace",
|
|
123
|
+
deleteFirst: news.name !== undefined && news.name === name,
|
|
124
|
+
} as const;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}),
|
|
128
|
+
read: Effect.fn(function* ({ id, olds, output, previous }) {
|
|
129
|
+
const name = output?.name ?? (yield* tokenName(id, olds?.name));
|
|
130
|
+
const observed = yield* observe({ tokenId: output?.tokenId, name });
|
|
131
|
+
if (!observed) return undefined;
|
|
132
|
+
if (output !== undefined && output.tokenId === observed.id) return output;
|
|
133
|
+
if (previous !== undefined && previous.tokenId === observed.id) return previous;
|
|
134
|
+
// The key is only revealed at creation, so a token we have no state
|
|
135
|
+
// for can't be adopted with its value — reconcile re-mints.
|
|
136
|
+
return Unowned({
|
|
137
|
+
tokenId: observed.id ?? "",
|
|
138
|
+
name,
|
|
139
|
+
tokenSuffix: observed.tokenSuffix,
|
|
140
|
+
token: Redacted.make(""),
|
|
141
|
+
region: (yield* account).region,
|
|
142
|
+
});
|
|
143
|
+
}),
|
|
144
|
+
reconcile: Effect.fn(function* ({ id, news, output, session }) {
|
|
145
|
+
const name = output?.name ?? (yield* tokenName(id, news.name));
|
|
146
|
+
// Observe
|
|
147
|
+
const observed = yield* observe({ tokenId: output?.tokenId, name });
|
|
148
|
+
if (
|
|
149
|
+
observed &&
|
|
150
|
+
output &&
|
|
151
|
+
Redacted.value(output.token) !== "" &&
|
|
152
|
+
output.tokenId === observed.id
|
|
153
|
+
) {
|
|
154
|
+
return output;
|
|
155
|
+
}
|
|
156
|
+
// Ensure — a key we hold no secret for is revoked and re-minted.
|
|
157
|
+
if (observed?.id) {
|
|
158
|
+
yield* session.note(`Re-minting API token ${name}...`);
|
|
159
|
+
yield* revoke(observed.id);
|
|
160
|
+
}
|
|
161
|
+
return yield* createApiToken(name, news.description);
|
|
162
|
+
}),
|
|
163
|
+
delete: Effect.fn(function* ({ output }) {
|
|
164
|
+
yield* revoke(output.tokenId);
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How Archil bindings reach Archil from any host.
|
|
3
|
+
*
|
|
4
|
+
* Archil only mints API keys from a console session and can't scope them,
|
|
5
|
+
* so a host's control-plane clients use the key the stack deploys with,
|
|
6
|
+
* recorded per host and read back at run time. Hosts that only touch one
|
|
7
|
+
* disk never hold an API key: the object bindings get a per-host S3
|
|
8
|
+
* `Archil.DiskUser` (read-only for `ReadDisk`), and `MountDisk` a per-host
|
|
9
|
+
* token user.
|
|
10
|
+
*/
|
|
11
|
+
import { controlPlaneUrl, Credentials, DEFAULT_REGION } from "@distilled.cloud/archil/Credentials";
|
|
12
|
+
import * as Objects from "@distilled.cloud/archil/Objects";
|
|
13
|
+
import { ConfigError } from "@distilled.cloud/core/errors";
|
|
14
|
+
import * as Binding from "alchemy/Binding";
|
|
15
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
16
|
+
import { Stack } from "alchemy/Stack";
|
|
17
|
+
import * as Effect from "effect/Effect";
|
|
18
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
19
|
+
import type * as HttpClient from "effect/http/HttpClient";
|
|
20
|
+
import * as Layer from "effect/Layer";
|
|
21
|
+
import * as Option from "effect/Option";
|
|
22
|
+
import * as Stream from "effect/Stream";
|
|
23
|
+
import type { Disk } from "./Disk.ts";
|
|
24
|
+
import { DiskUser } from "./DiskUser.ts";
|
|
25
|
+
import { DeployerKey } from "./internal/DeployerKey.ts";
|
|
26
|
+
|
|
27
|
+
/** Runs disk object effects and streams with one set of object credentials. */
|
|
28
|
+
export interface ObjectsRunner {
|
|
29
|
+
<A, E, R>(
|
|
30
|
+
effect: Effect.Effect<A, E, R>,
|
|
31
|
+
): Effect.Effect<
|
|
32
|
+
A,
|
|
33
|
+
E,
|
|
34
|
+
Exclude<R, Objects.ObjectCredentials | HttpClient.HttpClient> | RuntimeContext
|
|
35
|
+
>;
|
|
36
|
+
stream<A, E, R>(
|
|
37
|
+
stream: Stream.Stream<A, E, R>,
|
|
38
|
+
): Stream.Stream<
|
|
39
|
+
A,
|
|
40
|
+
E,
|
|
41
|
+
Exclude<R, Objects.ObjectCredentials | HttpClient.HttpClient> | RuntimeContext
|
|
42
|
+
>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Runs Archil SDK effects and streams with the host's own API key. */
|
|
46
|
+
export interface Authorize {
|
|
47
|
+
<A, E, R>(
|
|
48
|
+
effect: Effect.Effect<A, E, R>,
|
|
49
|
+
): Effect.Effect<A, E, Exclude<R, Credentials | HttpClient.HttpClient> | RuntimeContext>;
|
|
50
|
+
stream<A, E, R>(
|
|
51
|
+
stream: Stream.Stream<A, E, R>,
|
|
52
|
+
): Stream.Stream<A, E, Exclude<R, Credentials | HttpClient.HttpClient> | RuntimeContext>;
|
|
53
|
+
/** Disk object calls with the same API key. */
|
|
54
|
+
readonly objects: ObjectsRunner;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TypeScript can't infer a generic call signature whose requirement is
|
|
58
|
+
// `Exclude<R, …>` from `Effect.provide`, so runners assert their shape.
|
|
59
|
+
const runnerOf = <Rm>(layer: Layer.Layer<Rm>) => ({
|
|
60
|
+
run: <A, E, R>(effect: Effect.Effect<A, E, R>) => effect.pipe(Effect.provide(layer)),
|
|
61
|
+
stream: <A, E, R>(stream: Stream.Stream<A, E, R>) => stream.pipe(Stream.provide(layer)),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Record (at deploy) and read back (at run time) the API key a host's
|
|
66
|
+
* Archil clients call the control plane with. Every Archil binding on one
|
|
67
|
+
* host shares one record (the engine de-duplicates the declaration).
|
|
68
|
+
*/
|
|
69
|
+
export const ApiAccess = Effect.gen(function* () {
|
|
70
|
+
const Key = yield* DeployerKey;
|
|
71
|
+
return Effect.fn(function* () {
|
|
72
|
+
const host = yield* Binding.Host;
|
|
73
|
+
const token = yield* Key(`${host?.LogicalId ?? "Script"}ArchilApiKey`, {});
|
|
74
|
+
const apiKey = yield* token.apiKey;
|
|
75
|
+
const region = yield* token.region;
|
|
76
|
+
const credentials = Effect.all([apiKey, region]).pipe(
|
|
77
|
+
Effect.map(([key, r]) => ({
|
|
78
|
+
apiKey: key,
|
|
79
|
+
apiBaseUrl: controlPlaneUrl(r) ?? controlPlaneUrl(DEFAULT_REGION)!,
|
|
80
|
+
region: r,
|
|
81
|
+
})),
|
|
82
|
+
);
|
|
83
|
+
const layer = Layer.mergeAll(Objects.ObjectCredentialsFromApiKey, FetchHttpClient.layer).pipe(
|
|
84
|
+
Layer.provideMerge(Layer.succeed(Credentials, credentials)),
|
|
85
|
+
);
|
|
86
|
+
const { run, stream } = runnerOf(layer);
|
|
87
|
+
const objects = Object.assign(run, { stream }) as unknown as ObjectsRunner;
|
|
88
|
+
return Object.assign(run, { stream, objects }) as unknown as Authorize;
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create (at deploy) and read back (at run time) a per-host S3 user on one
|
|
94
|
+
* disk, and run object calls signed with its keys. The keys reach only
|
|
95
|
+
* that disk, and only `GET`/`HEAD` when `readOnly`. Every object binding
|
|
96
|
+
* of one kind on one host shares one user (the engine de-duplicates the
|
|
97
|
+
* declaration).
|
|
98
|
+
*/
|
|
99
|
+
export const DiskKeyAccess = Effect.gen(function* () {
|
|
100
|
+
const Users = yield* DiskUser;
|
|
101
|
+
return Effect.fn(function* (disk: Disk, access: "read" | "write") {
|
|
102
|
+
const host = yield* Binding.Host;
|
|
103
|
+
const user = yield* Users(
|
|
104
|
+
`${host?.LogicalId ?? "Script"}${disk.LogicalId}${access === "read" ? "Reader" : "Writer"}`,
|
|
105
|
+
{ disk: disk.diskId, type: "s3", readOnly: access === "read" },
|
|
106
|
+
);
|
|
107
|
+
const accessKeyId = yield* user.accessKeyId;
|
|
108
|
+
const secretAccessKey = yield* user.secretAccessKey;
|
|
109
|
+
const region = yield* disk.region;
|
|
110
|
+
const auth = Effect.gen(function* () {
|
|
111
|
+
const keyId = yield* accessKeyId;
|
|
112
|
+
const secret = yield* secretAccessKey;
|
|
113
|
+
if (keyId === undefined || secret === undefined) {
|
|
114
|
+
return yield* new ConfigError({
|
|
115
|
+
message: `The S3 user for ${disk.LogicalId} has no keys; redeploy to create them again.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return yield* Objects.diskKeysAuth({
|
|
119
|
+
region: yield* region,
|
|
120
|
+
accessKeyId: keyId,
|
|
121
|
+
secretAccessKey: secret,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
const layer = Layer.mergeAll(
|
|
125
|
+
Layer.succeed(Objects.ObjectCredentials, auth),
|
|
126
|
+
FetchHttpClient.layer,
|
|
127
|
+
);
|
|
128
|
+
const { run, stream } = runnerOf(layer);
|
|
129
|
+
return Object.assign(run, { stream }) as unknown as ObjectsRunner;
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/** What every name a run-time client creates starts with. */
|
|
134
|
+
export const RUNTIME_NAME_MARKER = "alc-";
|
|
135
|
+
|
|
136
|
+
const toNameLabel = (value: string) =>
|
|
137
|
+
value
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
140
|
+
.replace(/-+/g, "-")
|
|
141
|
+
.replace(/^-|-$/g, "");
|
|
142
|
+
|
|
143
|
+
/** FNV-1a, 32-bit, as 8 hex digits: the same on every host, no crypto API needed. */
|
|
144
|
+
const fnv1a = (value: string) => {
|
|
145
|
+
let hash = 0x811c9dc5;
|
|
146
|
+
for (let i = 0; i < value.length; i++) {
|
|
147
|
+
hash ^= value.charCodeAt(i);
|
|
148
|
+
hash = Math.imul(hash, 0x01000193);
|
|
149
|
+
}
|
|
150
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The owner a binding's run-time client stamps on what it creates. Archil
|
|
155
|
+
* disks and sandboxes have no tags, labels or descriptions, only a name,
|
|
156
|
+
* so the owner (`alchemy::stack`, `alchemy::stage`, `alchemy::id`) is a
|
|
157
|
+
* name prefix: `alc-<id>-<hash of stack/stage/id>`. Hosts know their stack
|
|
158
|
+
* and stage at run time (`ALCHEMY_STACK_NAME`, `ALCHEMY_STAGE`), so the
|
|
159
|
+
* prefix is the same at deploy and at run time without a resource.
|
|
160
|
+
* Destroying the stack doesn't delete what carries it.
|
|
161
|
+
*/
|
|
162
|
+
export const runtimePrefix = Effect.fn(function* (label: string) {
|
|
163
|
+
const host = yield* Binding.Host;
|
|
164
|
+
const stack = yield* Effect.serviceOption(Stack);
|
|
165
|
+
const tags = {
|
|
166
|
+
"alchemy::stack": Option.isSome(stack) ? stack.value.name : "",
|
|
167
|
+
"alchemy::stage": Option.isSome(stack) ? stack.value.stage : "",
|
|
168
|
+
"alchemy::id": `${host?.LogicalId ?? "Script"}${label}`,
|
|
169
|
+
};
|
|
170
|
+
const owner = `${tags["alchemy::stack"]}/${tags["alchemy::stage"]}/${tags["alchemy::id"]}`;
|
|
171
|
+
const readable = toNameLabel(tags["alchemy::id"]).slice(0, 14).replace(/-+$/, "") || "x";
|
|
172
|
+
return {
|
|
173
|
+
prefix: `${RUNTIME_NAME_MARKER}${readable}-${fnv1a(owner)}`,
|
|
174
|
+
tags,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/** `<prefix>-<name>`. */
|
|
179
|
+
export const runtimeName = (prefix: string, name: string) => `${prefix}-${name}`;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { DEFAULT_REGION, REGIONS } from "@distilled.cloud/archil/Credentials";
|
|
2
|
+
import { AuthError } from "alchemy/Auth/AuthProvider";
|
|
3
|
+
import { getEnv, getEnvRedacted } from "alchemy/Auth/Env";
|
|
4
|
+
import {
|
|
5
|
+
makeStoredAuthProvider,
|
|
6
|
+
storedSecret,
|
|
7
|
+
storedValueText,
|
|
8
|
+
type StoredAuthConfig,
|
|
9
|
+
} from "alchemy/Auth/StoredAuthProvider";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Redacted from "effect/Redacted";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Canonical name registered in the AuthProviders registry. Use this key to
|
|
15
|
+
* look up the Archil AuthProvider from inside provider Layers.
|
|
16
|
+
*/
|
|
17
|
+
export const ARCHIL_AUTH_PROVIDER_NAME = "Archil";
|
|
18
|
+
|
|
19
|
+
export type ArchilAuthConfig = StoredAuthConfig;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolved Archil credentials.
|
|
23
|
+
*
|
|
24
|
+
* Archil's control plane authenticates with account API keys (created in
|
|
25
|
+
* the console). Every disk, sandbox and image
|
|
26
|
+
* lives in one region, which picks the control plane and S3 endpoints.
|
|
27
|
+
*/
|
|
28
|
+
export interface ArchilResolvedCredentials {
|
|
29
|
+
type: "apiKey";
|
|
30
|
+
apiKey: Redacted.Redacted<string>;
|
|
31
|
+
/** Archil region id, e.g. `aws-us-east-1`. */
|
|
32
|
+
region: string;
|
|
33
|
+
/**
|
|
34
|
+
* Archil organization id. Only needed to scope bucket policies on
|
|
35
|
+
* bring-your-own-bucket disks to your organization.
|
|
36
|
+
*/
|
|
37
|
+
organization: string | undefined;
|
|
38
|
+
source: { type: ArchilAuthConfig["method"] | "env"; details?: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const validateRegion = (value: string) =>
|
|
42
|
+
value in REGIONS ? undefined : `One of ${Object.keys(REGIONS).join(", ")}`;
|
|
43
|
+
|
|
44
|
+
const readEnvironment = Effect.gen(function* () {
|
|
45
|
+
const apiKey = yield* getEnvRedacted("ARCHIL_API_KEY");
|
|
46
|
+
if (apiKey === undefined || Redacted.value(apiKey).trim().length === 0) {
|
|
47
|
+
return yield* AuthError.make({
|
|
48
|
+
message: "Archil CI credentials not found. Set ARCHIL_API_KEY.",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
type: "apiKey" as const,
|
|
53
|
+
apiKey: Redacted.make(Redacted.value(apiKey).trim()),
|
|
54
|
+
region: (yield* getEnv("ARCHIL_REGION")) ?? DEFAULT_REGION,
|
|
55
|
+
organization: yield* getEnv("ARCHIL_ORGANIZATION"),
|
|
56
|
+
source: { type: "env" as const, details: "ARCHIL_API_KEY" },
|
|
57
|
+
} satisfies ArchilResolvedCredentials;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const archilAuth = makeStoredAuthProvider<ArchilResolvedCredentials>({
|
|
61
|
+
provider: ARCHIL_AUTH_PROVIDER_NAME,
|
|
62
|
+
fields: [
|
|
63
|
+
{
|
|
64
|
+
name: "apiKey",
|
|
65
|
+
label: "Archil API Key",
|
|
66
|
+
description: "Create one at https://console.archil.com (API Keys). It starts with `key-`.",
|
|
67
|
+
secret: true,
|
|
68
|
+
validate: (value) => (value.trim().length === 0 ? "Required" : undefined),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "region",
|
|
72
|
+
label: "Archil Region",
|
|
73
|
+
description: `Region your disks and sandboxes live in. Defaults to ${DEFAULT_REGION}.`,
|
|
74
|
+
optional: true,
|
|
75
|
+
validate: (value) => (value.trim().length === 0 ? undefined : validateRegion(value.trim())),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "organization",
|
|
79
|
+
label: "Archil Organization ID",
|
|
80
|
+
description:
|
|
81
|
+
"Only needed for disks on your own S3 bucket: scopes the bucket policy to your organization.",
|
|
82
|
+
optional: true,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
toResolved: (values, source) => ({
|
|
86
|
+
type: "apiKey",
|
|
87
|
+
apiKey: storedSecret(values.apiKey) ?? Redacted.make(""),
|
|
88
|
+
region: storedValueText(values.region) || DEFAULT_REGION,
|
|
89
|
+
organization: storedValueText(values.organization) || undefined,
|
|
90
|
+
source: { type: source },
|
|
91
|
+
}),
|
|
92
|
+
readEnvironment,
|
|
93
|
+
environment: [
|
|
94
|
+
{
|
|
95
|
+
name: "ARCHIL_API_KEY",
|
|
96
|
+
required: true,
|
|
97
|
+
secret: true,
|
|
98
|
+
description: "Archil API key.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "ARCHIL_REGION",
|
|
102
|
+
required: false,
|
|
103
|
+
description: `Archil region id. Defaults to ${DEFAULT_REGION}.`,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "ARCHIL_ORGANIZATION",
|
|
107
|
+
required: false,
|
|
108
|
+
description: "Archil organization id, for bring-your-own-bucket disk policies.",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Layer that registers the Archil AuthProvider into the AuthProviders
|
|
115
|
+
* registry when built. Included in `Archil.providers()` so the alchemy CLI
|
|
116
|
+
* (`alchemy profile edit`) can configure it.
|
|
117
|
+
*/
|
|
118
|
+
export const ArchilAuth = archilAuth.layer;
|
|
119
|
+
|
|
120
|
+
/** Schema of Archil's inline API-key values. */
|
|
121
|
+
export const ArchilStoredCredentials = archilAuth.storedSchema;
|
|
122
|
+
export type ArchilStoredCredentials = typeof ArchilStoredCredentials.Type;
|
package/src/Bootstrap.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process bootstrap for an `Archil.Sandbox` program (a Node process serving the
|
|
3
|
+
* bundled program). The generated entry imports this module and the user's
|
|
4
|
+
* `main`, nothing else — see `alchemy/Runtime/Bootstrap` for why.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeServices } from "@effect/platform-node";
|
|
7
|
+
import { NodeHttpServer } from "alchemy/Http";
|
|
8
|
+
import { reifyBoundConfigProvider } from "alchemy/Runtime";
|
|
9
|
+
import {
|
|
10
|
+
entrypointLayer,
|
|
11
|
+
resolveProgram,
|
|
12
|
+
runProcess,
|
|
13
|
+
stackFromEnv,
|
|
14
|
+
} from "alchemy/Runtime/Bootstrap";
|
|
15
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
16
|
+
import * as SecureRandom from "effect/crypto/SecureRandom";
|
|
17
|
+
import * as Effect from "effect/Effect";
|
|
18
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
19
|
+
import * as Layer from "effect/Layer";
|
|
20
|
+
import * as Logger from "effect/Logger";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the bundled program (the runners registered via `host.run` /
|
|
24
|
+
* serve) and run it with a Node HTTP server bound to `PORT`, so the
|
|
25
|
+
* returned `{ fetch }` handler is actually served and `host.run` loops
|
|
26
|
+
* stay alive.
|
|
27
|
+
*/
|
|
28
|
+
export const bootstrap = (entrypoint: unknown): Promise<void> => {
|
|
29
|
+
const platform = Layer.mergeAll(
|
|
30
|
+
NodeServices.layer,
|
|
31
|
+
FetchHttpClient.layer,
|
|
32
|
+
SecureRandom.layerWebCrypto,
|
|
33
|
+
Logger.layer([Logger.consolePretty()]),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const program = resolveProgram("program").pipe(
|
|
37
|
+
Effect.provide(
|
|
38
|
+
entrypointLayer(entrypoint).pipe(
|
|
39
|
+
Layer.provideMerge(stackFromEnv),
|
|
40
|
+
Layer.provideMerge(NodeHttpServer({ hostname: "0.0.0.0" })),
|
|
41
|
+
Layer.provideMerge(platform),
|
|
42
|
+
Layer.provideMerge(
|
|
43
|
+
Layer.succeed(
|
|
44
|
+
ConfigProvider.ConfigProvider,
|
|
45
|
+
reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env),
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
Effect.scoped,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return runProcess("Archil service", program);
|
|
54
|
+
};
|