alchemy 2.0.0-beta.3 → 2.0.0-beta.4

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 (67) hide show
  1. package/bin/{alchemy-effect.js → alchemy.js} +170 -170
  2. package/bin/alchemy.js.map +1 -0
  3. package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
  4. package/lib/cli/index.d.ts.map +1 -1
  5. package/lib/cli/index.js +167 -167
  6. package/lib/cli/index.js.map +1 -1
  7. package/package.json +38 -38
  8. package/src/AWS/AGENTS.md +10 -10
  9. package/src/AWS/Assets.ts +1 -1
  10. package/src/AWS/AuthProvider.ts +392 -0
  11. package/src/AWS/Credentials.ts +0 -1
  12. package/src/AWS/DynamoDB/Table.ts +63 -10
  13. package/src/AWS/EC2/VpcEndpoint.ts +4 -4
  14. package/src/AWS/EC2/hosted.ts +1 -1
  15. package/src/AWS/ECS/Task.ts +1 -1
  16. package/src/AWS/Kinesis/Stream.ts +44 -8
  17. package/src/AWS/Lambda/Function.ts +218 -10
  18. package/src/AWS/S3/Bucket.ts +26 -19
  19. package/src/AWS/S3/BucketNotifications.ts +1 -1
  20. package/src/AWS/SNS/Topic.ts +47 -10
  21. package/src/AWS/SQS/Queue.ts +66 -0
  22. package/src/Auth/AuthProvider.ts +33 -0
  23. package/src/Auth/Credentials.ts +56 -0
  24. package/src/Auth/Env.ts +45 -0
  25. package/src/Auth/Profile.ts +72 -0
  26. package/src/Auth/index.ts +2 -0
  27. package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
  28. package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
  29. package/src/Cloudflare/Container/ContainerApplication.ts +3 -3
  30. package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
  31. package/src/Cloudflare/Container/StartContainer.ts +1 -1
  32. package/src/Cloudflare/Providers.ts +1 -6
  33. package/src/Cloudflare/R2/R2Bucket.ts +2 -2
  34. package/src/Cloudflare/Website/StaticSite.ts +44 -15
  35. package/src/Cloudflare/Website/Vite.ts +34 -12
  36. package/src/Cloudflare/Workers/Assets.ts +170 -207
  37. package/src/Cloudflare/Workers/DurableObjectNamespace.ts +230 -370
  38. package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
  39. package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
  40. package/src/Cloudflare/Workers/InferEnv.ts +11 -7
  41. package/src/Cloudflare/Workers/Rpc.ts +13 -2
  42. package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
  43. package/src/Cloudflare/Workers/WebSocket.ts +1 -1
  44. package/src/Cloudflare/Workers/Worker.ts +261 -61
  45. package/src/Cloudflare/Workers/index.ts +3 -0
  46. package/src/Construct.ts +2 -2
  47. package/src/GitHub/Secret.ts +52 -7
  48. package/src/GitHub/Variable.ts +45 -2
  49. package/src/Kubernetes/client.ts +2 -2
  50. package/src/Output.ts +1 -1
  51. package/src/Platform.ts +10 -5
  52. package/src/Resource.ts +4 -4
  53. package/src/Test/Vitest.ts +4 -3
  54. package/src/Util/Clank.ts +77 -0
  55. package/src/Util/PlatformServices.ts +21 -0
  56. package/src/Util/dedent.ts +4 -1
  57. package/bin/alchemy-effect.js.map +0 -1
  58. package/src/Daemon/Client.ts +0 -116
  59. package/src/Daemon/Config.ts +0 -28
  60. package/src/Daemon/Errors.ts +0 -48
  61. package/src/Daemon/Lock.ts +0 -162
  62. package/src/Daemon/ProcessRegistry.ts +0 -284
  63. package/src/Daemon/RpcSchema.ts +0 -43
  64. package/src/Daemon/RpcServer.ts +0 -231
  65. package/src/Daemon/index.ts +0 -27
  66. package/src/Spawn.ts +0 -23
  67. /package/bin/{alchemy-effect.ts → alchemy.ts} +0 -0
@@ -0,0 +1,315 @@
1
+ import * as Data from "effect/Data";
2
+ import * as Effect from "effect/Effect";
3
+ import crypto from "node:crypto";
4
+ import http from "node:http";
5
+ import {
6
+ OAUTH_CLIENT_ID,
7
+ OAUTH_ENDPOINTS,
8
+ OAUTH_REDIRECT_URI,
9
+ } from "./AuthProvider.ts";
10
+
11
+ export class OAuthError extends Data.TaggedError("OAuthError")<{
12
+ error: string;
13
+ errorDescription: string;
14
+ }> {}
15
+
16
+ export interface OAuthCredentials {
17
+ type: "oauth";
18
+ access: string;
19
+ refresh: string;
20
+ expires: number;
21
+ scopes: string[];
22
+ }
23
+
24
+ export interface Authorization {
25
+ url: string;
26
+ state: string;
27
+ verifier: string;
28
+ }
29
+
30
+ function generateState(length = 32): string {
31
+ return crypto.randomBytes(length).toString("base64url");
32
+ }
33
+
34
+ function generatePKCE(length = 96): {
35
+ verifier: string;
36
+ challenge: string;
37
+ } {
38
+ const verifier = crypto.randomBytes(length).toString("base64url");
39
+ const challenge = crypto
40
+ .createHash("sha256")
41
+ .update(verifier)
42
+ .digest("base64url");
43
+ return { verifier, challenge };
44
+ }
45
+
46
+ function extractCredentials(json: {
47
+ access_token: string;
48
+ refresh_token: string;
49
+ expires_in: number;
50
+ scope: string;
51
+ }): OAuthCredentials {
52
+ return {
53
+ type: "oauth",
54
+ access: json.access_token,
55
+ refresh: json.refresh_token,
56
+ expires: Date.now() + json.expires_in * 1000,
57
+ scopes: json.scope.split(" "),
58
+ };
59
+ }
60
+
61
+ const tokenRequest = (
62
+ body: Record<string, string>,
63
+ ): Effect.Effect<OAuthCredentials, OAuthError> =>
64
+ Effect.gen(function* () {
65
+ const res = yield* Effect.tryPromise({
66
+ try: () =>
67
+ fetch(OAUTH_ENDPOINTS.token, {
68
+ method: "POST",
69
+ headers: {
70
+ Accept: "application/json",
71
+ "Content-Type": "application/x-www-form-urlencoded",
72
+ },
73
+ body: new URLSearchParams(body).toString(),
74
+ }),
75
+ catch: (err) =>
76
+ new OAuthError({
77
+ error: "network_error",
78
+ errorDescription: `Token request failed: ${err}`,
79
+ }),
80
+ });
81
+
82
+ if (!res.ok) {
83
+ const json = yield* Effect.tryPromise({
84
+ try: () =>
85
+ res.json() as Promise<{ error: string; error_description: string }>,
86
+ catch: () =>
87
+ new OAuthError({
88
+ error: "parse_error",
89
+ errorDescription: `Token endpoint returned ${res.status}`,
90
+ }),
91
+ });
92
+ return yield* new OAuthError({
93
+ error: json.error,
94
+ errorDescription: json.error_description,
95
+ });
96
+ }
97
+
98
+ const json = yield* Effect.tryPromise({
99
+ try: () =>
100
+ res.json() as Promise<{
101
+ access_token: string;
102
+ refresh_token: string;
103
+ expires_in: number;
104
+ scope: string;
105
+ }>,
106
+ catch: () =>
107
+ new OAuthError({
108
+ error: "parse_error",
109
+ errorDescription: "Failed to parse token response",
110
+ }),
111
+ });
112
+ return extractCredentials(json);
113
+ });
114
+
115
+ /**
116
+ * Generate an authorization URL with PKCE for the given scopes.
117
+ */
118
+ export function authorize(scopes: string[]): Authorization {
119
+ const state = generateState();
120
+ const pkce = generatePKCE();
121
+ const url = new URL(OAUTH_ENDPOINTS.authorize);
122
+ url.searchParams.set("client_id", OAUTH_CLIENT_ID);
123
+ url.searchParams.set("redirect_uri", OAUTH_REDIRECT_URI);
124
+ url.searchParams.set("response_type", "code");
125
+ url.searchParams.set("scope", scopes.join(" "));
126
+ url.searchParams.set("state", state);
127
+ url.searchParams.set("code_challenge", pkce.challenge);
128
+ url.searchParams.set("code_challenge_method", "S256");
129
+ return { url: url.toString(), state, verifier: pkce.verifier };
130
+ }
131
+
132
+ /**
133
+ * Exchange an authorization code for OAuth credentials.
134
+ */
135
+ export const exchange = (
136
+ code: string,
137
+ verifier: string,
138
+ ): Effect.Effect<OAuthCredentials, OAuthError> =>
139
+ tokenRequest({
140
+ grant_type: "authorization_code",
141
+ code,
142
+ code_verifier: verifier,
143
+ client_id: OAUTH_CLIENT_ID,
144
+ redirect_uri: OAUTH_REDIRECT_URI,
145
+ });
146
+
147
+ /**
148
+ * Refresh expired OAuth credentials.
149
+ */
150
+ export const refresh = (
151
+ credentials: OAuthCredentials,
152
+ ): Effect.Effect<OAuthCredentials, OAuthError> =>
153
+ tokenRequest({
154
+ grant_type: "refresh_token",
155
+ refresh_token: credentials.refresh,
156
+ client_id: OAUTH_CLIENT_ID,
157
+ redirect_uri: OAUTH_REDIRECT_URI,
158
+ });
159
+
160
+ /**
161
+ * Revoke OAuth credentials.
162
+ */
163
+ export const revoke = (
164
+ credentials: OAuthCredentials,
165
+ ): Effect.Effect<void, OAuthError> =>
166
+ Effect.gen(function* () {
167
+ yield* Effect.tryPromise({
168
+ try: () =>
169
+ fetch(OAUTH_ENDPOINTS.revoke, {
170
+ method: "POST",
171
+ headers: {
172
+ Accept: "application/json",
173
+ "Content-Type": "application/x-www-form-urlencoded",
174
+ },
175
+ body: new URLSearchParams({
176
+ refresh_token: credentials.refresh,
177
+ client_id: OAUTH_CLIENT_ID,
178
+ redirect_uri: OAUTH_REDIRECT_URI,
179
+ }).toString(),
180
+ }),
181
+ catch: (err) =>
182
+ new OAuthError({
183
+ error: "network_error",
184
+ errorDescription: `Revoke request failed: ${err}`,
185
+ }),
186
+ });
187
+ });
188
+
189
+ /**
190
+ * Start a local HTTP server to listen for the OAuth callback, exchange
191
+ * the authorization code, and return the credentials.
192
+ *
193
+ * Times out after 5 minutes.
194
+ */
195
+ export const callback = (
196
+ authorization: Authorization,
197
+ ): Effect.Effect<OAuthCredentials, OAuthError> =>
198
+ Effect.tryPromise({
199
+ try: () => callbackPromise(authorization),
200
+ catch: (err) => {
201
+ if (err instanceof OAuthError) return err;
202
+ return new OAuthError({
203
+ error: "callback_error",
204
+ errorDescription: `OAuth callback failed: ${err}`,
205
+ });
206
+ },
207
+ });
208
+
209
+ function callbackPromise(
210
+ authorization: Authorization,
211
+ ): Promise<OAuthCredentials> {
212
+ const { pathname, port } = new URL(OAUTH_REDIRECT_URI);
213
+
214
+ return new Promise<OAuthCredentials>((resolve, reject) => {
215
+ const server = http.createServer(async (req, res) => {
216
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
217
+
218
+ if (url.pathname !== pathname) {
219
+ res.statusCode = 404;
220
+ res.end("Not Found");
221
+ return;
222
+ }
223
+
224
+ const error = url.searchParams.get("error");
225
+ const errorDescription = url.searchParams.get("error_description");
226
+ if (error) {
227
+ res.writeHead(302, { Location: "https://alchemy.run/auth/error" });
228
+ res.end();
229
+ cleanup();
230
+ reject(
231
+ new OAuthError({
232
+ error,
233
+ errorDescription: errorDescription ?? "An unknown error occurred.",
234
+ }),
235
+ );
236
+ return;
237
+ }
238
+
239
+ const code = url.searchParams.get("code");
240
+ const state = url.searchParams.get("state");
241
+ if (!code || !state) {
242
+ res.writeHead(302, { Location: "https://alchemy.run/auth/error" });
243
+ res.end();
244
+ cleanup();
245
+ reject(
246
+ new OAuthError({
247
+ error: "invalid_request",
248
+ errorDescription: "Missing code or state",
249
+ }),
250
+ );
251
+ return;
252
+ }
253
+
254
+ if (state !== authorization.state) {
255
+ res.writeHead(302, { Location: "https://alchemy.run/auth/error" });
256
+ res.end();
257
+ cleanup();
258
+ reject(
259
+ new OAuthError({
260
+ error: "invalid_request",
261
+ errorDescription: "Invalid state",
262
+ }),
263
+ );
264
+ return;
265
+ }
266
+
267
+ try {
268
+ const credentials = await Effect.runPromise(
269
+ exchange(code, authorization.verifier),
270
+ );
271
+ res.writeHead(302, {
272
+ Location: "https://alchemy.run/auth/success",
273
+ });
274
+ res.end();
275
+ cleanup();
276
+ resolve(credentials);
277
+ } catch (err) {
278
+ res.writeHead(302, { Location: "https://alchemy.run/auth/error" });
279
+ res.end();
280
+ cleanup();
281
+ reject(err);
282
+ }
283
+ });
284
+
285
+ const timeout = setTimeout(
286
+ () => {
287
+ cleanup();
288
+ reject(
289
+ new OAuthError({
290
+ error: "timeout",
291
+ errorDescription: "The authorization process timed out.",
292
+ }),
293
+ );
294
+ },
295
+ 5 * 60 * 1000,
296
+ );
297
+
298
+ function cleanup() {
299
+ clearTimeout(timeout);
300
+ server.close();
301
+ }
302
+
303
+ server.on("error", (err) => {
304
+ cleanup();
305
+ reject(
306
+ new OAuthError({
307
+ error: "server_error",
308
+ errorDescription: `Failed to start callback server: ${err.message}`,
309
+ }),
310
+ );
311
+ });
312
+
313
+ server.listen(Number(port));
314
+ });
315
+ }
@@ -483,16 +483,16 @@ ${
483
483
  runtime === "bun"
484
484
  ? `
485
485
  import { BunServices } from "@effect/platform-bun";
486
- import { BunHttpServer } from "alchemy-effect/Http";
486
+ import { BunHttpServer } from "alchemy/Http";
487
487
  const HttpServer = BunHttpServer;
488
488
  `
489
489
  : `
490
490
  import { NodeServices } from "@effect/platform-node";
491
- import { NodeHttpServer } from "alchemy-effect/Http";
491
+ import { NodeHttpServer } from "alchemy/Http";
492
492
  const HttpServer = NodeHttpServer;
493
493
  `
494
494
  }
495
- import { Stack } from "alchemy-effect/Stack";
495
+ import { Stack } from "alchemy/Stack";
496
496
  import * as Effect from "effect/Effect";
497
497
  import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
498
498
  import * as Layer from "effect/Layer";
@@ -5,10 +5,8 @@ import {
5
5
  toCloudflareFetcher,
6
6
  type Fetcher,
7
7
  } from "../Fetcher.ts";
8
- import {
9
- DurableObjectNamespace,
10
- DurableObjectState,
11
- } from "../Workers/DurableObjectNamespace.ts";
8
+ import { DurableObjectNamespace } from "../Workers/DurableObjectNamespace.ts";
9
+ import { DurableObjectState } from "../Workers/DurableObjectState.ts";
12
10
  import { Worker } from "../Workers/Worker.ts";
13
11
  import type { Container } from "./Container.ts";
14
12
  import type { ContainerApplication } from "./ContainerApplication.ts";
@@ -6,7 +6,7 @@ import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
6
6
  import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
7
7
  import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
8
8
  import { type Fetcher } from "../Fetcher.ts";
9
- import { DurableObjectState } from "../Workers/DurableObjectNamespace.ts";
9
+ import { DurableObjectState } from "../Workers/DurableObjectState.ts";
10
10
  import { type Container, ContainerError } from "./Container.ts";
11
11
 
12
12
  /**
@@ -10,7 +10,6 @@ import * as Containers from "./Container/index.ts";
10
10
  import * as D1 from "./D1/index.ts";
11
11
  import * as KV from "./KV/index.ts";
12
12
  import * as R2 from "./R2/index.ts";
13
- import * as Assets from "./Workers/Assets.ts";
14
13
  import * as Workers from "./Workers/index.ts";
15
14
  import * as Workflows from "./Workers/Workflow.ts";
16
15
 
@@ -60,11 +59,7 @@ export const providers = () =>
60
59
  ),
61
60
  ),
62
61
  Layer.provideMerge(
63
- Layer.mergeAll(
64
- Assets.AssetsProvider(),
65
- Build.CommandProvider(),
66
- RandomProvider(),
67
- ),
62
+ Layer.mergeAll(Build.CommandProvider(), RandomProvider()),
68
63
  ),
69
64
  Layer.provideMerge(
70
65
  Layer.mergeAll(
@@ -6,7 +6,7 @@ import { createPhysicalName } from "../../PhysicalName.ts";
6
6
  import * as Provider from "../../Provider.ts";
7
7
  import { Resource } from "../../Resource.ts";
8
8
  import { Account } from "../Account.ts";
9
- import type { Providers } from "../Providers.ts";
9
+ import type * as Cloudflare from "../Providers.ts";
10
10
  import { R2BucketBinding } from "./R2BucketBinding.ts";
11
11
 
12
12
  export type R2BucketName = string;
@@ -44,7 +44,7 @@ export type R2Bucket = Resource<
44
44
  accountId: string;
45
45
  },
46
46
  never,
47
- Providers
47
+ Cloudflare.Providers
48
48
  >;
49
49
 
50
50
  /**
@@ -3,10 +3,17 @@ import { Command, type CommandProps } from "../../Build/Command.ts";
3
3
  import type { InputProps } from "../../Input.ts";
4
4
  import * as Namespace from "../../Namespace.ts";
5
5
  import type { AssetsConfig } from "../Workers/Assets.ts";
6
- import { Worker, type WorkerProps } from "../Workers/Worker.ts";
6
+ import {
7
+ Worker,
8
+ type WorkerAssetsConfig,
9
+ type WorkerBindingProps,
10
+ type WorkerProps,
11
+ } from "../Workers/Worker.ts";
7
12
 
8
- export interface StaticSiteProps
9
- extends Omit<WorkerProps, "assets">, Omit<CommandProps, "env"> {
13
+ export interface StaticSiteProps<Bindings extends WorkerBindingProps = {}>
14
+ extends
15
+ Omit<WorkerProps<Bindings, WorkerAssetsConfig>, "assets">,
16
+ Omit<CommandProps, "env"> {
10
17
  /**
11
18
  * Optional configuration for static asset routing behavior.
12
19
  * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc.
@@ -76,19 +83,41 @@ export type StaticSite = ReturnType<typeof StaticSite>;
76
83
  * });
77
84
  * ```
78
85
  */
79
- export const StaticSite = (id: string, props: InputProps<StaticSiteProps>) =>
86
+ export const StaticSite = <
87
+ const Bindings extends WorkerBindingProps = {},
88
+ Req = never,
89
+ >(
90
+ id: string,
91
+ propsEff:
92
+ | InputProps<StaticSiteProps<Bindings>>
93
+ | Effect.Effect<InputProps<StaticSiteProps<Bindings>>, never, Req>,
94
+ ) =>
80
95
  Effect.gen(function* () {
81
- // TODO(sam): local dev/hmr support?
82
- const build = yield* Command("Build", props);
96
+ const props = Effect.isEffect(propsEff)
97
+ ? propsEff
98
+ : Effect.succeed(propsEff);
83
99
 
84
- const worker = yield* Worker("Worker", {
85
- ...props,
86
- assets: {
87
- path: build.outdir,
88
- hash: build.hash,
89
- config: props.assetsConfig,
90
- },
91
- });
100
+ // TODO(sam): local dev/hmr support?
101
+ const build = yield* Command(
102
+ "Build",
103
+ Effect.map(props, (props) => ({
104
+ command: props.command,
105
+ cwd: props.cwd,
106
+ memo: props.memo,
107
+ outdir: props.outdir,
108
+ env: props.env,
109
+ })),
110
+ );
92
111
 
93
- return worker;
112
+ return yield* Worker<Bindings, WorkerAssetsConfig, Req>(
113
+ "Worker",
114
+ Effect.map(props, (props) => ({
115
+ ...props,
116
+ assets: {
117
+ path: build.outdir,
118
+ hash: build.hash,
119
+ config: props.assetsConfig,
120
+ },
121
+ })),
122
+ );
94
123
  }).pipe(Namespace.push(id));
@@ -1,8 +1,16 @@
1
- import type { InputProps } from "../../Input.ts";
1
+ import * as Effect from "effect/Effect";
2
2
  import type { MemoOptions } from "../../Build/Memo.ts";
3
- import { Worker, type WorkerProps } from "../Workers/Worker.ts";
3
+ import type { InputProps } from "../../Input.ts";
4
+ import {
5
+ Worker,
6
+ type WorkerAssetsConfig,
7
+ type WorkerBindingProps,
8
+ type WorkerProps,
9
+ } from "../Workers/Worker.ts";
4
10
 
5
- export interface ViteProps extends Omit<WorkerProps, "vite" | "main"> {
11
+ export interface ViteProps<
12
+ Bindings extends WorkerBindingProps = {},
13
+ > extends Omit<WorkerProps<Bindings>, "vite" | "main"> {
6
14
  /**
7
15
  * Root directory passed to Vite's `root` option.
8
16
  * Defaults to the current working directory (`process.cwd()`).
@@ -99,12 +107,26 @@ export interface ViteProps extends Omit<WorkerProps, "vite" | "main"> {
99
107
  * });
100
108
  * ```
101
109
  */
102
- export const Vite = (id: string, props: InputProps<ViteProps> = {}) =>
103
- Worker(id, {
104
- ...props,
105
- main: undefined!,
106
- vite: {
107
- rootDir: props.rootDir,
108
- memo: props.memo,
109
- },
110
- });
110
+ export const Vite = <
111
+ const Bindings extends WorkerBindingProps = {},
112
+ Req = never,
113
+ >(
114
+ id: string,
115
+ propsEff?:
116
+ | InputProps<ViteProps<Bindings>>
117
+ | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
118
+ ) =>
119
+ Worker<Bindings, WorkerAssetsConfig, Req>(
120
+ id,
121
+ Effect.map(
122
+ Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
123
+ (props) => ({
124
+ ...props,
125
+ main: undefined!,
126
+ vite: {
127
+ rootDir: props?.rootDir,
128
+ memo: props?.memo,
129
+ },
130
+ }),
131
+ ),
132
+ );