@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,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;
@@ -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
+ };