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.
Files changed (77) hide show
  1. package/bin/{alchemy-effect.js → alchemy.js} +171 -171
  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 +54 -45
  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/Container.ts +122 -0
  30. package/src/Cloudflare/Container/ContainerApplication.ts +6 -3
  31. package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
  32. package/src/Cloudflare/Container/StartContainer.ts +1 -1
  33. package/src/Cloudflare/D1/D1Database.ts +23 -4
  34. package/src/Cloudflare/KV/KVNamespace.ts +25 -0
  35. package/src/Cloudflare/Providers.ts +1 -6
  36. package/src/Cloudflare/R2/R2Bucket.ts +45 -2
  37. package/src/Cloudflare/Website/StaticSite.ts +101 -15
  38. package/src/Cloudflare/Website/Vite.ts +86 -20
  39. package/src/Cloudflare/Workers/Assets.ts +170 -207
  40. package/src/Cloudflare/Workers/DurableObjectNamespace.ts +629 -0
  41. package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
  42. package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
  43. package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
  44. package/src/Cloudflare/Workers/InferEnv.ts +11 -7
  45. package/src/Cloudflare/Workers/Rpc.ts +13 -2
  46. package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
  47. package/src/Cloudflare/Workers/WebSocket.ts +1 -1
  48. package/src/Cloudflare/Workers/Worker.ts +572 -67
  49. package/src/Cloudflare/Workers/Workflow.ts +53 -11
  50. package/src/Cloudflare/Workers/index.ts +4 -1
  51. package/src/Construct.ts +2 -2
  52. package/src/GitHub/Comment.ts +224 -0
  53. package/src/GitHub/Secret.ts +257 -0
  54. package/src/GitHub/Variable.ts +166 -0
  55. package/src/GitHub/index.ts +3 -0
  56. package/src/Kubernetes/client.ts +2 -2
  57. package/src/Output.ts +1 -1
  58. package/src/Platform.ts +10 -5
  59. package/src/Provider.ts +1 -1
  60. package/src/Resource.ts +4 -4
  61. package/src/Test/Vitest.ts +4 -3
  62. package/src/Util/Clank.ts +77 -0
  63. package/src/Util/PlatformServices.ts +21 -0
  64. package/src/Util/dedent.ts +59 -0
  65. package/src/Util/index.ts +1 -0
  66. package/bin/alchemy-effect.js.map +0 -1
  67. package/src/Cloudflare/Workers/DurableObject.ts +0 -527
  68. package/src/Daemon/Client.ts +0 -116
  69. package/src/Daemon/Config.ts +0 -28
  70. package/src/Daemon/Errors.ts +0 -48
  71. package/src/Daemon/Lock.ts +0 -162
  72. package/src/Daemon/ProcessRegistry.ts +0 -284
  73. package/src/Daemon/RpcSchema.ts +0 -43
  74. package/src/Daemon/RpcServer.ts +0 -231
  75. package/src/Daemon/index.ts +0 -27
  76. package/src/Spawn.ts +0 -23
  77. /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-effect/Http";
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-effect/Http";
491
+ import { NodeHttpServer } from "alchemy/Http";
489
492
  const HttpServer = NodeHttpServer;
490
493
  `
491
494
  }
492
- import { Stack } from "alchemy-effect/Stack";
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
- DurableObjectNamespace,
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/DurableObject.ts";
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 Database
64
+ * @example Basic database
62
65
  * ```typescript
63
- * const db = yield* Database("my-db", {});
66
+ * const db = yield* Cloudflare.D1Database("my-db");
64
67
  * ```
65
68
  *
66
- * @example Database with Location Hint
69
+ * @example Database with location hint
67
70
  * ```typescript
68
- * const db = yield* Database("my-db", {
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 { 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,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
  });