alchemy 2.0.0-beta.2 → 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.
- package/bin/{alchemy-effect.js → alchemy.js} +171 -171
- package/bin/alchemy.js.map +1 -0
- package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +167 -167
- package/lib/cli/index.js.map +1 -1
- package/package.json +54 -45
- package/src/AWS/AGENTS.md +10 -10
- package/src/AWS/Assets.ts +1 -1
- package/src/AWS/AuthProvider.ts +392 -0
- package/src/AWS/Credentials.ts +0 -1
- package/src/AWS/DynamoDB/Table.ts +63 -10
- package/src/AWS/EC2/VpcEndpoint.ts +4 -4
- package/src/AWS/EC2/hosted.ts +1 -1
- package/src/AWS/ECS/Task.ts +1 -1
- package/src/AWS/Kinesis/Stream.ts +44 -8
- package/src/AWS/Lambda/Function.ts +218 -10
- package/src/AWS/S3/Bucket.ts +26 -19
- package/src/AWS/S3/BucketNotifications.ts +1 -1
- package/src/AWS/SNS/Topic.ts +47 -10
- package/src/AWS/SQS/Queue.ts +66 -0
- package/src/Auth/AuthProvider.ts +33 -0
- package/src/Auth/Credentials.ts +56 -0
- package/src/Auth/Env.ts +45 -0
- package/src/Auth/Profile.ts +72 -0
- package/src/Auth/index.ts +2 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
- package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
- package/src/Cloudflare/Container/Container.ts +122 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +6 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/D1/D1Database.ts +23 -4
- package/src/Cloudflare/KV/KVNamespace.ts +25 -0
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +45 -2
- package/src/Cloudflare/Website/StaticSite.ts +101 -15
- package/src/Cloudflare/Website/Vite.ts +86 -20
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +629 -0
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
- package/src/Cloudflare/Workers/InferEnv.ts +11 -7
- package/src/Cloudflare/Workers/Rpc.ts +13 -2
- package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
- package/src/Cloudflare/Workers/WebSocket.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +572 -67
- package/src/Cloudflare/Workers/Workflow.ts +53 -11
- package/src/Cloudflare/Workers/index.ts +4 -1
- package/src/Construct.ts +2 -2
- package/src/GitHub/Comment.ts +224 -0
- package/src/GitHub/Secret.ts +257 -0
- package/src/GitHub/Variable.ts +166 -0
- package/src/GitHub/index.ts +3 -0
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- package/src/Provider.ts +1 -1
- package/src/Resource.ts +4 -4
- package/src/Test/Vitest.ts +4 -3
- package/src/Util/Clank.ts +77 -0
- package/src/Util/PlatformServices.ts +21 -0
- package/src/Util/dedent.ts +59 -0
- package/src/Util/index.ts +1 -0
- package/bin/alchemy-effect.js.map +0 -1
- package/src/Cloudflare/Workers/DurableObject.ts +0 -527
- package/src/Daemon/Client.ts +0 -116
- package/src/Daemon/Config.ts +0 -28
- package/src/Daemon/Errors.ts +0 -48
- package/src/Daemon/Lock.ts +0 -162
- package/src/Daemon/ProcessRegistry.ts +0 -284
- package/src/Daemon/RpcSchema.ts +0 -43
- package/src/Daemon/RpcServer.ts +0 -231
- package/src/Daemon/index.ts +0 -27
- package/src/Spawn.ts +0 -23
- /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
|
+
}
|
|
@@ -48,6 +48,128 @@ export type Container = {
|
|
|
48
48
|
interceptAllOutboundHttp(binding: Fetcher): Effect.Effect<void>;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A Cloudflare Container that runs a long-lived process alongside a
|
|
53
|
+
* Durable Object.
|
|
54
|
+
*
|
|
55
|
+
* Containers always use the **Container Layer** pattern — the class
|
|
56
|
+
* and `.make()` must live in separate files. A Container must be
|
|
57
|
+
* bound to a Durable Object, and the DO imports the class to get a
|
|
58
|
+
* typed handle. If the class and `.make()` lived in the same file,
|
|
59
|
+
* the DO's bundle would pull in all of the container's runtime
|
|
60
|
+
* dependencies (process spawners, Node APIs, SDKs, etc.), which
|
|
61
|
+
* would bloat the bundle and likely break the Cloudflare Workers
|
|
62
|
+
* runtime. Keeping them separate ensures the bundler only includes
|
|
63
|
+
* the tiny class in the DO's output.
|
|
64
|
+
*
|
|
65
|
+
* See the {@link https://alchemy.run/concepts/platform | Platform
|
|
66
|
+
* concept} page for how this fits into the async / effect / layer
|
|
67
|
+
* progression.
|
|
68
|
+
*
|
|
69
|
+
* @section Container Layer
|
|
70
|
+
* Define the class and `.make()` in separate files. The class
|
|
71
|
+
* declares the container's identity, configuration, and typed
|
|
72
|
+
* shape. `.make()` provides the runtime implementation as a
|
|
73
|
+
* default export. Use `Container.of` to construct the typed
|
|
74
|
+
* shape — it ensures your implementation matches the methods
|
|
75
|
+
* declared on the class.
|
|
76
|
+
*
|
|
77
|
+
* @example Container class
|
|
78
|
+
* ```typescript
|
|
79
|
+
* // src/Sandbox.ts
|
|
80
|
+
* export class Sandbox extends Cloudflare.Container<
|
|
81
|
+
* Sandbox,
|
|
82
|
+
* {
|
|
83
|
+
* exec: (cmd: string) => Effect.Effect<{
|
|
84
|
+
* exitCode: number;
|
|
85
|
+
* stdout: string;
|
|
86
|
+
* stderr: string;
|
|
87
|
+
* }>;
|
|
88
|
+
* }
|
|
89
|
+
* >()(
|
|
90
|
+
* "Sandbox",
|
|
91
|
+
* { main: import.meta.filename },
|
|
92
|
+
* ) {}
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @example Container .make()
|
|
96
|
+
* ```typescript
|
|
97
|
+
* // src/Sandbox.runtime.ts
|
|
98
|
+
* export default Sandbox.make(
|
|
99
|
+
* Effect.gen(function* () {
|
|
100
|
+
* const cp = yield* ChildProcessSpawner;
|
|
101
|
+
*
|
|
102
|
+
* return Sandbox.of({
|
|
103
|
+
* exec: (cmd) =>
|
|
104
|
+
* cp.spawn(ChildProcess.make(cmd, { shell: true })).pipe(
|
|
105
|
+
* Effect.map(({ exitCode, stdout, stderr }) => ({
|
|
106
|
+
* exitCode, stdout, stderr,
|
|
107
|
+
* })),
|
|
108
|
+
* Effect.scoped,
|
|
109
|
+
* ),
|
|
110
|
+
* fetch: Effect.succeed(
|
|
111
|
+
* HttpServerResponse.text("Hello from container!"),
|
|
112
|
+
* ),
|
|
113
|
+
* });
|
|
114
|
+
* }),
|
|
115
|
+
* );
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @section Configuration
|
|
119
|
+
* The props object accepts `main` (entrypoint file), `instanceType`
|
|
120
|
+
* (compute size), `runtime` (`"bun"` or `"node"`), and
|
|
121
|
+
* `observability` settings. Use `Stack.useSync` to vary config by
|
|
122
|
+
* stage.
|
|
123
|
+
*
|
|
124
|
+
* @example Stage-dependent configuration
|
|
125
|
+
* ```typescript
|
|
126
|
+
* export class Sandbox extends Cloudflare.Container<Sandbox>()(
|
|
127
|
+
* "Sandbox",
|
|
128
|
+
* Stack.useSync((stack) => ({
|
|
129
|
+
* main: import.meta.filename,
|
|
130
|
+
* instanceType: stack.stage === "prod" ? "standard-1" : "dev",
|
|
131
|
+
* observability: { logs: { enabled: true } },
|
|
132
|
+
* })),
|
|
133
|
+
* ) {}
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* @section Starting from a Durable Object
|
|
137
|
+
* Use `Cloudflare.Container.bind` in the outer init phase to bind
|
|
138
|
+
* the container class, then `Cloudflare.start` in the inner
|
|
139
|
+
* per-instance phase to start it. Because the DO only imports the
|
|
140
|
+
* class, the runtime implementation is completely excluded from the
|
|
141
|
+
* DO's bundle.
|
|
142
|
+
*
|
|
143
|
+
* @example Binding and starting a container
|
|
144
|
+
* ```typescript
|
|
145
|
+
* // init (outer Effect) — only imports the class
|
|
146
|
+
* const sandbox = yield* Cloudflare.Container.bind(Sandbox);
|
|
147
|
+
*
|
|
148
|
+
* // per-instance (inner Effect)
|
|
149
|
+
* return Effect.gen(function* () {
|
|
150
|
+
* const container = yield* Cloudflare.start(sandbox);
|
|
151
|
+
*
|
|
152
|
+
* return {
|
|
153
|
+
* exec: (cmd: string) => container.exec(cmd),
|
|
154
|
+
* };
|
|
155
|
+
* });
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @section HTTP Requests to Container Ports
|
|
159
|
+
* Use `getTcpPort` to get a `fetch` handle for a specific port on
|
|
160
|
+
* the running container. This lets you make HTTP requests to
|
|
161
|
+
* servers running inside the container process.
|
|
162
|
+
*
|
|
163
|
+
* @example Fetching from a container port
|
|
164
|
+
* ```typescript
|
|
165
|
+
* const container = yield* Cloudflare.start(sandbox);
|
|
166
|
+
* const { fetch } = yield* container.getTcpPort(3000);
|
|
167
|
+
*
|
|
168
|
+
* const response = yield* fetch(
|
|
169
|
+
* HttpClientRequest.get("http://container/health"),
|
|
170
|
+
* );
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
51
173
|
export const Container: Platform<
|
|
52
174
|
ContainerApplication,
|
|
53
175
|
ContainerServices,
|
|
@@ -223,6 +223,9 @@ export type ContainerServices =
|
|
|
223
223
|
|
|
224
224
|
export type ContainerShape = Main<ContainerServices>;
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* @internal
|
|
228
|
+
*/
|
|
226
229
|
export interface ContainerApplication<Shape = unknown> extends Resource<
|
|
227
230
|
ContainerTypeId,
|
|
228
231
|
ContainerApplicationProps,
|
|
@@ -480,16 +483,16 @@ ${
|
|
|
480
483
|
runtime === "bun"
|
|
481
484
|
? `
|
|
482
485
|
import { BunServices } from "@effect/platform-bun";
|
|
483
|
-
import { BunHttpServer } from "alchemy
|
|
486
|
+
import { BunHttpServer } from "alchemy/Http";
|
|
484
487
|
const HttpServer = BunHttpServer;
|
|
485
488
|
`
|
|
486
489
|
: `
|
|
487
490
|
import { NodeServices } from "@effect/platform-node";
|
|
488
|
-
import { NodeHttpServer } from "alchemy
|
|
491
|
+
import { NodeHttpServer } from "alchemy/Http";
|
|
489
492
|
const HttpServer = NodeHttpServer;
|
|
490
493
|
`
|
|
491
494
|
}
|
|
492
|
-
import { Stack } from "alchemy
|
|
495
|
+
import { Stack } from "alchemy/Stack";
|
|
493
496
|
import * as Effect from "effect/Effect";
|
|
494
497
|
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
495
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
|
-
|
|
10
|
-
DurableObjectState,
|
|
11
|
-
} from "../Workers/DurableObject.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/
|
|
9
|
+
import { DurableObjectState } from "../Workers/DurableObjectState.ts";
|
|
10
10
|
import { type Container, ContainerError } from "./Container.ts";
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -57,18 +57,37 @@ export type D1Database = Resource<
|
|
|
57
57
|
/**
|
|
58
58
|
* A Cloudflare D1 serverless SQL database built on SQLite.
|
|
59
59
|
*
|
|
60
|
+
* D1 is a serverless relational database that runs at the edge. Create a
|
|
61
|
+
* database as a resource, then bind it to a Worker to run SQL queries.
|
|
62
|
+
*
|
|
60
63
|
* @section Creating a Database
|
|
61
|
-
* @example Basic
|
|
64
|
+
* @example Basic database
|
|
62
65
|
* ```typescript
|
|
63
|
-
* const db = yield*
|
|
66
|
+
* const db = yield* Cloudflare.D1Database("my-db");
|
|
64
67
|
* ```
|
|
65
68
|
*
|
|
66
|
-
* @example Database with
|
|
69
|
+
* @example Database with location hint
|
|
67
70
|
* ```typescript
|
|
68
|
-
* const db = yield*
|
|
71
|
+
* const db = yield* Cloudflare.D1Database("my-db", {
|
|
69
72
|
* primaryLocationHint: "wnam",
|
|
70
73
|
* });
|
|
71
74
|
* ```
|
|
75
|
+
*
|
|
76
|
+
* @section Binding to a Worker
|
|
77
|
+
* @example Using D1 inside a Worker
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const db = yield* Cloudflare.D1Connection.bind(MyDB);
|
|
80
|
+
*
|
|
81
|
+
* // Run a query
|
|
82
|
+
* const results = yield* db.prepare("SELECT * FROM users WHERE id = ?")
|
|
83
|
+
* .bind(userId)
|
|
84
|
+
* .all();
|
|
85
|
+
*
|
|
86
|
+
* // Execute a mutation
|
|
87
|
+
* yield* db.prepare("INSERT INTO users (id, name) VALUES (?, ?)")
|
|
88
|
+
* .bind(newId, name)
|
|
89
|
+
* .run();
|
|
90
|
+
* ```
|
|
72
91
|
*/
|
|
73
92
|
export const D1Database = Resource<D1Database>("Cloudflare.D1Database");
|
|
74
93
|
|
|
@@ -30,6 +30,31 @@ export type KVNamespace = Resource<
|
|
|
30
30
|
Providers
|
|
31
31
|
>;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A Cloudflare Workers KV namespace for key-value storage at the edge.
|
|
35
|
+
*
|
|
36
|
+
* KV provides eventually-consistent, low-latency reads with global
|
|
37
|
+
* replication. Create a namespace as a resource, then bind it to a Worker
|
|
38
|
+
* to get/put values at runtime.
|
|
39
|
+
*
|
|
40
|
+
* @section Creating a Namespace
|
|
41
|
+
* @example Basic KV namespace
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const kv = yield* Cloudflare.KVNamespace("MyKV");
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @section Binding to a Worker
|
|
47
|
+
* @example Using KV inside a Worker
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const kv = yield* Cloudflare.KVNamespace.bind(MyKV);
|
|
50
|
+
*
|
|
51
|
+
* // Read a value
|
|
52
|
+
* const value = yield* kv.get("my-key");
|
|
53
|
+
*
|
|
54
|
+
* // Write a value
|
|
55
|
+
* yield* kv.put("my-key", "hello world");
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
33
58
|
export const KVNamespace = Resource<KVNamespace>("Cloudflare.KVNamespace")({
|
|
34
59
|
bind: KVNamespaceBinding.bind,
|
|
35
60
|
});
|
|
@@ -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
|
|
9
|
+
import type * as Cloudflare from "../Providers.ts";
|
|
10
10
|
import { R2BucketBinding } from "./R2BucketBinding.ts";
|
|
11
11
|
|
|
12
12
|
export type R2BucketName = string;
|
|
@@ -44,9 +44,52 @@ export type R2Bucket = Resource<
|
|
|
44
44
|
accountId: string;
|
|
45
45
|
},
|
|
46
46
|
never,
|
|
47
|
-
Providers
|
|
47
|
+
Cloudflare.Providers
|
|
48
48
|
>;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A Cloudflare R2 object storage bucket with S3-compatible API.
|
|
52
|
+
*
|
|
53
|
+
* R2 provides zero-egress-fee object storage. Create a bucket as a resource,
|
|
54
|
+
* then bind it to a Worker to read and write objects at runtime.
|
|
55
|
+
*
|
|
56
|
+
* @section Creating a Bucket
|
|
57
|
+
* @example Basic R2 bucket
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const bucket = yield* Cloudflare.R2Bucket("MyBucket");
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @example Bucket with location hint
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
|
|
65
|
+
* locationHint: "wnam",
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @section Binding to a Worker
|
|
70
|
+
* @example Reading and writing objects
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const bucket = yield* Cloudflare.R2Bucket.bind(MyBucket);
|
|
73
|
+
*
|
|
74
|
+
* // Write an object
|
|
75
|
+
* yield* bucket.put("hello.txt", "Hello, World!");
|
|
76
|
+
*
|
|
77
|
+
* // Read an object
|
|
78
|
+
* const object = yield* bucket.get("hello.txt");
|
|
79
|
+
* if (object) {
|
|
80
|
+
* const text = yield* object.text();
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example Streaming upload with content length
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const bucket = yield* Cloudflare.R2Bucket.bind(MyBucket);
|
|
87
|
+
*
|
|
88
|
+
* yield* bucket.put("upload.bin", request.stream, {
|
|
89
|
+
* contentLength: Number(request.headers["content-length"] ?? 0),
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
50
93
|
export const R2Bucket = Resource<R2Bucket>("Cloudflare.R2Bucket")({
|
|
51
94
|
bind: R2BucketBinding.bind,
|
|
52
95
|
});
|