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
@@ -116,25 +116,233 @@ export type FunctionShape = Main<FunctionServices>;
116
116
  * An AWS Lambda host resource that combines code bundling, IAM role
117
117
  * provisioning, and runtime binding collection.
118
118
  *
119
- * `Function` is the canonical runtime host for AWS. Resource bindings attach
120
- * environment variables and IAM statements during deployment, while the runtime
121
- * execution context collects listeners and exports from the Effect program.
119
+ * `Function` is the canonical runtime host for AWS. Alchemy automatically
120
+ * bundles your TypeScript entry module with Rolldown, creates an IAM
121
+ * execution role, and uploads the zip artifact. On subsequent deploys, the
122
+ * function is only updated when the bundle hash changes.
122
123
  *
123
- * @section Creating Functions
124
- * @example Basic Function
124
+ * There are two ways to define a Lambda Function:
125
+ *
126
+ * - **Async** — plain handler export, no Effect runtime in the bundle.
127
+ * - **Effect** — Effect implementation with typed bindings and event sources.
128
+ *
129
+ * See the {@link https://alchemy.run/guides/async-lambda | Async Lambda Guide}
130
+ * for plain handler patterns, or the
131
+ * {@link https://alchemy.run/guides/lambda | Effect Lambda Guide}
132
+ * for the full Effect-based approach with bindings, event sources, and sinks.
133
+ *
134
+ * @section Async Functions
135
+ * Point `main` at a file that exports a standard Lambda handler. No
136
+ * Effect runtime is included in the bundle. Useful when migrating
137
+ * existing Lambda functions or when you don't need Effect.
138
+ *
139
+ * @example Defining an async Lambda in your stack
125
140
  * ```typescript
126
- * const func = yield* Function("OrdersFunction", {
127
- * main: "./src/orders.ts",
141
+ * // alchemy.run.ts
142
+ * import * as AWS from "alchemy/AWS";
143
+ *
144
+ * const func = yield* AWS.Lambda.Function("ApiFunction", {
145
+ * main: "./src/handler.ts",
146
+ * url: true,
128
147
  * });
129
148
  * ```
130
149
  *
150
+ * @example Writing the async handler
151
+ * ```typescript
152
+ * // src/handler.ts
153
+ * export const handler = async (event: any) => {
154
+ * return {
155
+ * statusCode: 200,
156
+ * body: JSON.stringify({ message: "Hello from Lambda!" }),
157
+ * };
158
+ * };
159
+ * ```
160
+ *
161
+ * @section Effect Functions
162
+ * Pass the Effect implementation as the third argument. Bindings
163
+ * attach IAM permissions and environment variables at deploy time,
164
+ * while the runtime execution context collects listeners and exports.
165
+ *
166
+ * @example Effect Function with HTTP handler
167
+ * ```typescript
168
+ * export default class ApiFunction extends AWS.Lambda.Function<ApiFunction>()(
169
+ * "ApiFunction",
170
+ * { main: import.meta.filename, url: true },
171
+ * Effect.gen(function* () {
172
+ * // init: bind resources
173
+ * const getItem = yield* DynamoDB.GetItem.bind(table);
174
+ *
175
+ * return {
176
+ * // runtime: use them
177
+ * fetch: Effect.gen(function* () {
178
+ * const request = yield* HttpServerRequest;
179
+ * const url = new URL(request.url);
180
+ * const id = url.searchParams.get("id");
181
+ * const result = yield* getItem({ Key: { pk: { S: id! } } });
182
+ * return yield* HttpServerResponse.json(result.Item);
183
+ * }),
184
+ * };
185
+ * }),
186
+ * ) {}
187
+ * ```
188
+ *
189
+ * @section Configuration
131
190
  * @example Function with URL
132
191
  * ```typescript
133
- * const func = yield* Function("ApiFunction", {
134
- * main: "./src/api.ts",
192
+ * const func = yield* AWS.Lambda.Function("ApiFunction", {
193
+ * main: "./src/handler.ts",
135
194
  * url: true,
136
195
  * });
137
196
  * ```
197
+ *
198
+ * @example Function in a VPC
199
+ * ```typescript
200
+ * const func = yield* AWS.Lambda.Function("VpcFunction", {
201
+ * main: "./src/handler.ts",
202
+ * vpc: {
203
+ * subnetIds: ["subnet-abc123", "subnet-def456"],
204
+ * securityGroupIds: ["sg-xyz789"],
205
+ * },
206
+ * });
207
+ * ```
208
+ *
209
+ * @section S3 Bindings
210
+ * Bind S3 operations in the init phase to give the function IAM
211
+ * permissions and inject the bucket name as an environment variable.
212
+ *
213
+ * @example Read and write S3 objects
214
+ * ```typescript
215
+ * // init
216
+ * const getObject = yield* S3.GetObject.bind(bucket);
217
+ * const putObject = yield* S3.PutObject.bind(bucket);
218
+ *
219
+ * return {
220
+ * fetch: Effect.gen(function* () {
221
+ * // runtime
222
+ * yield* putObject({ Key: "hello.txt", Body: "Hello!" });
223
+ * const obj = yield* getObject({ Key: "hello.txt" });
224
+ * return HttpServerResponse.text("OK");
225
+ * }),
226
+ * };
227
+ * ```
228
+ *
229
+ * @section DynamoDB Bindings
230
+ * Bind DynamoDB operations in the init phase to grant table-scoped
231
+ * IAM permissions.
232
+ *
233
+ * @example Get and put items
234
+ * ```typescript
235
+ * // init
236
+ * const getItem = yield* DynamoDB.GetItem.bind(table);
237
+ * const putItem = yield* DynamoDB.PutItem.bind(table);
238
+ *
239
+ * return {
240
+ * fetch: Effect.gen(function* () {
241
+ * // runtime
242
+ * yield* putItem({ Item: { pk: { S: "user#1" }, name: { S: "Alice" } } });
243
+ * const result = yield* getItem({ Key: { pk: { S: "user#1" } } });
244
+ * return yield* HttpServerResponse.json(result.Item);
245
+ * }),
246
+ * };
247
+ * ```
248
+ *
249
+ * @section SQS Bindings
250
+ * Bind SQS operations in the init phase to send messages to a queue.
251
+ *
252
+ * @example Send a message
253
+ * ```typescript
254
+ * // init
255
+ * const sendMessage = yield* SQS.SendMessage.bind(queue);
256
+ *
257
+ * return {
258
+ * fetch: Effect.gen(function* () {
259
+ * // runtime
260
+ * yield* sendMessage({
261
+ * MessageBody: JSON.stringify({ orderId: "123" }),
262
+ * });
263
+ * return HttpServerResponse.text("Queued");
264
+ * }),
265
+ * };
266
+ * ```
267
+ *
268
+ * @section SNS Bindings
269
+ * Bind SNS operations in the init phase to publish messages to a
270
+ * topic.
271
+ *
272
+ * @example Publish a notification
273
+ * ```typescript
274
+ * // init
275
+ * const publish = yield* SNS.Publish.bind(topic);
276
+ *
277
+ * return {
278
+ * fetch: Effect.gen(function* () {
279
+ * // runtime
280
+ * yield* publish({
281
+ * Message: JSON.stringify({ event: "order.created" }),
282
+ * Subject: "OrderCreated",
283
+ * });
284
+ * return HttpServerResponse.text("Published");
285
+ * }),
286
+ * };
287
+ * ```
288
+ *
289
+ * @section Kinesis Bindings
290
+ * Bind Kinesis operations in the init phase to put records into a
291
+ * stream.
292
+ *
293
+ * @example Put a record
294
+ * ```typescript
295
+ * // init
296
+ * const putRecord = yield* Kinesis.PutRecord.bind(stream);
297
+ *
298
+ * return {
299
+ * fetch: Effect.gen(function* () {
300
+ * // runtime
301
+ * yield* putRecord({
302
+ * PartitionKey: "order-123",
303
+ * Data: new TextEncoder().encode(JSON.stringify({ orderId: "123" })),
304
+ * });
305
+ * return HttpServerResponse.text("Sent");
306
+ * }),
307
+ * };
308
+ * ```
309
+ *
310
+ * @section Event Sources
311
+ * Lambda functions can be triggered by event sources like SQS queues,
312
+ * DynamoDB streams, S3 notifications, SNS topics, and Kinesis streams.
313
+ *
314
+ * @example Process SQS messages
315
+ * ```typescript
316
+ * yield* SQS.messages(queue).process(
317
+ * Effect.fn(function* (message) {
318
+ * yield* Effect.log(`Received: ${message.body}`);
319
+ * }),
320
+ * );
321
+ * ```
322
+ *
323
+ * @example Process DynamoDB stream changes
324
+ * ```typescript
325
+ * yield* DynamoDB.streams(table, {
326
+ * StreamViewType: "NEW_AND_OLD_IMAGES",
327
+ * }).process(
328
+ * Effect.fn(function* (record) {
329
+ * yield* Effect.log(`Change: ${record.eventName}`);
330
+ * }),
331
+ * );
332
+ * ```
333
+ *
334
+ * @example Process S3 notifications
335
+ * ```typescript
336
+ * yield* S3.notifications(bucket, {
337
+ * events: ["s3:ObjectCreated:*"],
338
+ * }).subscribe((stream) =>
339
+ * stream.pipe(
340
+ * Stream.runForEach((event) =>
341
+ * Effect.log(`New object: ${event.key}`),
342
+ * ),
343
+ * ),
344
+ * );
345
+ * ```
138
346
  */
139
347
  export const Function: Platform<
140
348
  Function,
@@ -433,7 +641,7 @@ export const FunctionProvider = () =>
433
641
  virtualEntryPlugin(
434
642
  (importPath) => `
435
643
  import { NodeServices } from "@effect/platform-node";
436
- import { Stack } from "alchemy-effect/Stack";
644
+ import { Stack } from "alchemy/Stack";
437
645
  import * as Config from "effect/Config";
438
646
  import * as ConfigProvider from "effect/ConfigProvider";
439
647
  import * as Credentials from "@distilled.cloud/aws/Credentials";
@@ -91,7 +91,7 @@ export interface Bucket extends Resource<
91
91
  * @section Creating a Bucket
92
92
  * @example Basic Bucket
93
93
  * ```typescript
94
- * import * as S3 from "alchemy-effect/AWS/S3";
94
+ * import * as S3 from "alchemy/AWS/S3";
95
95
  *
96
96
  * const bucket = yield* S3.Bucket("my-bucket", {});
97
97
  * ```
@@ -110,37 +110,44 @@ export interface Bucket extends Resource<
110
110
  * });
111
111
  * ```
112
112
  *
113
- * @section Reading Objects
114
- * @example Get an object from a bucket
115
- * ```typescript
116
- * const getObject = yield* S3.GetObject.bind(bucket);
117
- *
118
- * const response = yield* getObject({ Key: "hello.txt" });
119
- * ```
113
+ * @section Runtime Operations
114
+ * Bind S3 operations in the init phase and use them in runtime
115
+ * handlers. Bindings inject the bucket name and grant scoped IAM
116
+ * permissions automatically.
120
117
  *
121
- * @section Writing Objects
122
- * @example Put an object into a bucket
118
+ * @example Read and write objects
123
119
  * ```typescript
120
+ * // init
121
+ * const getObject = yield* S3.GetObject.bind(bucket);
124
122
  * const putObject = yield* S3.PutObject.bind(bucket);
125
123
  *
126
- * yield* putObject({
127
- * Key: "hello.txt",
128
- * Body: "Hello, World!",
129
- * ContentType: "text/plain",
130
- * });
124
+ * return {
125
+ * fetch: Effect.gen(function* () {
126
+ * // runtime
127
+ * yield* putObject({
128
+ * Key: "hello.txt",
129
+ * Body: "Hello, World!",
130
+ * ContentType: "text/plain",
131
+ * });
132
+ * const response = yield* getObject({ Key: "hello.txt" });
133
+ * return HttpServerResponse.text("OK");
134
+ * }),
135
+ * };
131
136
  * ```
132
137
  *
133
- * @section Deleting Objects
134
- * @example Delete an object from a bucket
138
+ * @example Delete an object
135
139
  * ```typescript
140
+ * // init
136
141
  * const deleteObject = yield* S3.DeleteObject.bind(bucket);
137
- *
138
- * yield* deleteObject({ Key: "hello.txt" });
139
142
  * ```
140
143
  *
141
144
  * @section Event Notifications
145
+ * Subscribe to bucket events from the init phase. The subscription
146
+ * and Lambda invoke permissions are created automatically.
147
+ *
142
148
  * @example Process object creation events
143
149
  * ```typescript
150
+ * // init
144
151
  * yield* S3.notifications(bucket, {
145
152
  * events: ["s3:ObjectCreated:*"],
146
153
  * }).subscribe((stream) =>
@@ -35,7 +35,7 @@ export interface NotificationsProps<Events extends S3EventType[]> {
35
35
  * @section Subscribing to Events
36
36
  * @example Process all object creation events
37
37
  * ```typescript
38
- * import * as S3 from "alchemy-effect/AWS/S3";
38
+ * import * as S3 from "alchemy/AWS/S3";
39
39
  *
40
40
  * yield* S3.notifications(bucket, {
41
41
  * events: ["s3:ObjectCreated:*"],
@@ -60,20 +60,30 @@ export interface Topic extends Resource<
60
60
  * An Amazon SNS topic for fan-out messaging and notifications.
61
61
  *
62
62
  * `Topic` owns the SNS topic lifecycle while raw AWS topic attributes remain
63
- * available through the `attributes` prop so the full core pub/sub surface can be
64
- * configured without waiting on additional typed wrappers.
63
+ * available through the `attributes` prop so the full core pub/sub surface can
64
+ * be configured without waiting on additional typed wrappers. A topic name is
65
+ * auto-generated unless you provide one explicitly.
65
66
  *
66
67
  * @section Creating Topics
67
68
  * @example Standard Topic
68
69
  * ```typescript
69
- * const topic = yield* Topic("OrdersTopic", {
70
- * topicName: "orders-events",
70
+ * import * as SNS from "alchemy/AWS/SNS";
71
+ *
72
+ * const topic = yield* SNS.Topic("OrdersTopic");
73
+ * ```
74
+ *
75
+ * @example Topic with Display Name
76
+ * ```typescript
77
+ * const topic = yield* SNS.Topic("NotificationsTopic", {
78
+ * attributes: {
79
+ * DisplayName: "App Notifications",
80
+ * },
71
81
  * });
72
82
  * ```
73
83
  *
74
84
  * @example FIFO Topic
75
85
  * ```typescript
76
- * const topic = yield* Topic("OrdersFifoTopic", {
86
+ * const topic = yield* SNS.Topic("OrdersFifoTopic", {
77
87
  * fifo: true,
78
88
  * attributes: {
79
89
  * ContentBasedDeduplication: "true",
@@ -82,14 +92,41 @@ export interface Topic extends Resource<
82
92
  * ```
83
93
  *
84
94
  * @section Runtime Publishing
85
- * @example Publish from a Lambda Function
95
+ * Bind publish operations in the init phase and use them in runtime
96
+ * handlers.
97
+ *
98
+ * @example Publish from a handler
86
99
  * ```typescript
100
+ * // init
87
101
  * const publish = yield* SNS.Publish.bind(topic);
88
102
  *
89
- * yield* publish({
90
- * Message: JSON.stringify({ orderId: "123" }),
91
- * Subject: "OrderCreated",
92
- * });
103
+ * return {
104
+ * fetch: Effect.gen(function* () {
105
+ * // runtime
106
+ * yield* publish({
107
+ * Message: JSON.stringify({ orderId: "123" }),
108
+ * Subject: "OrderCreated",
109
+ * });
110
+ * return HttpServerResponse.text("Published");
111
+ * }),
112
+ * };
113
+ * ```
114
+ *
115
+ * @section Subscriptions
116
+ * Subscribe a Lambda function to process messages published to the
117
+ * topic. The subscription and invoke permissions are created
118
+ * automatically.
119
+ *
120
+ * @example Process topic notifications
121
+ * ```typescript
122
+ * // init
123
+ * yield* SNS.notifications(topic).subscribe((stream) =>
124
+ * stream.pipe(
125
+ * Stream.runForEach((message) =>
126
+ * Effect.log(`Received: ${message.Message}`),
127
+ * ),
128
+ * ),
129
+ * );
93
130
  * ```
94
131
  */
95
132
  export const Topic = Resource<Topic>("AWS.SNS.Topic");
@@ -87,6 +87,72 @@ export interface Queue extends Resource<
87
87
  Providers
88
88
  > {}
89
89
 
90
+ /**
91
+ * An Amazon SQS queue for reliable, decoupled message processing.
92
+ *
93
+ * `Queue` owns the lifecycle of a standard or FIFO SQS queue. A queue name
94
+ * is auto-generated from the app, stage, and logical ID unless you provide
95
+ * one explicitly. FIFO queues automatically append the `.fifo` suffix.
96
+ *
97
+ * @section Creating Queues
98
+ * @example Standard Queue
99
+ * ```typescript
100
+ * import * as SQS from "alchemy/AWS/SQS";
101
+ *
102
+ * const queue = yield* SQS.Queue("OrdersQueue");
103
+ * ```
104
+ *
105
+ * @example FIFO Queue
106
+ * ```typescript
107
+ * const queue = yield* SQS.Queue("OrdersFifoQueue", {
108
+ * fifo: true,
109
+ * contentBasedDeduplication: true,
110
+ * });
111
+ * ```
112
+ *
113
+ * @example Queue with Custom Settings
114
+ * ```typescript
115
+ * const queue = yield* SQS.Queue("ProcessingQueue", {
116
+ * visibilityTimeout: 120,
117
+ * messageRetentionPeriod: 86400,
118
+ * receiveMessageWaitTimeSeconds: 20,
119
+ * });
120
+ * ```
121
+ *
122
+ * @section Sending Messages
123
+ * Bind send operations in the init phase and use them in runtime
124
+ * handlers.
125
+ *
126
+ * @example Send a message from a handler
127
+ * ```typescript
128
+ * // init
129
+ * const sendMessage = yield* SQS.SendMessage.bind(queue);
130
+ *
131
+ * return {
132
+ * fetch: Effect.gen(function* () {
133
+ * // runtime
134
+ * yield* sendMessage({
135
+ * MessageBody: JSON.stringify({ orderId: "123" }),
136
+ * });
137
+ * return HttpServerResponse.text("Queued");
138
+ * }),
139
+ * };
140
+ * ```
141
+ *
142
+ * @section Event Sources
143
+ * Process messages from a queue using a Lambda event source mapping.
144
+ * Messages are automatically deleted after successful processing.
145
+ *
146
+ * @example Process queue messages
147
+ * ```typescript
148
+ * // init
149
+ * yield* SQS.messages(queue).process(
150
+ * Effect.fn(function* (message) {
151
+ * yield* Effect.log(`Received: ${message.body}`);
152
+ * }),
153
+ * );
154
+ * ```
155
+ */
90
156
  export const Queue = Resource<Queue>("AWS.SQS.Queue");
91
157
 
92
158
  export const QueueProvider = () =>
@@ -0,0 +1,33 @@
1
+ import type * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+
4
+ export class AuthError extends Schema.TaggedErrorClass<AuthError>()(
5
+ "AuthError",
6
+ {
7
+ message: Schema.String,
8
+ cause: Schema.optional(Schema.Defect),
9
+ },
10
+ ) {}
11
+
12
+ export interface AuthProvider<Config extends { method: string }, Credentials> {
13
+ readonly name: string;
14
+
15
+ configure(profileName: string): Effect.Effect<Config, AuthError>;
16
+
17
+ login(
18
+ profileName: string,
19
+ config: Config,
20
+ ): Effect.Effect<void, AuthError, never>;
21
+
22
+ logout(
23
+ profileName: string,
24
+ config: Config,
25
+ ): Effect.Effect<void, AuthError, never>;
26
+
27
+ prettyPrint(profileName: string, config: Config): Effect.Effect<void>;
28
+
29
+ read(
30
+ profileName: string,
31
+ config: Config,
32
+ ): Effect.Effect<Credentials, AuthError, never>;
33
+ }
@@ -0,0 +1,56 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Redacted from "effect/Redacted";
4
+ import path from "pathe";
5
+ import { rootDir } from "./Profile.ts";
6
+
7
+ const credentialsDirPath = path.join(rootDir, "credentials");
8
+
9
+ export const credentialsFilePath = (profile: string, provider: string) =>
10
+ path.join(credentialsDirPath, profile, `${provider}.json`);
11
+
12
+ export const readCredentials = Effect.fnUntraced(function* <T>(
13
+ profile: string,
14
+ provider: string,
15
+ ) {
16
+ const fs = yield* FileSystem.FileSystem;
17
+ const data = yield* fs
18
+ .readFileString(credentialsFilePath(profile, provider))
19
+ .pipe(Effect.catch(() => Effect.succeed(undefined)));
20
+ if (data === undefined) return undefined as T | undefined;
21
+ try {
22
+ return JSON.parse(data) as T;
23
+ } catch {
24
+ return undefined as T | undefined;
25
+ }
26
+ });
27
+
28
+ export const writeCredentials = Effect.fnUntraced(function* <T>(
29
+ profile: string,
30
+ provider: string,
31
+ credentials: T,
32
+ ) {
33
+ const fs = yield* FileSystem.FileSystem;
34
+ const filePath = credentialsFilePath(profile, provider);
35
+ yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
36
+ yield* fs.writeFileString(filePath, JSON.stringify(credentials, null, 2));
37
+ });
38
+
39
+ export const deleteCredentials = Effect.fnUntraced(function* (
40
+ profile: string,
41
+ provider: string,
42
+ ) {
43
+ const fs = yield* FileSystem.FileSystem;
44
+ yield* fs
45
+ .remove(credentialsFilePath(profile, provider))
46
+ .pipe(Effect.catch(() => Effect.void));
47
+ });
48
+
49
+ export function displayRedacted(
50
+ r: Redacted.Redacted<string>,
51
+ visibleChars = 4,
52
+ ): string {
53
+ const raw = Redacted.value(r);
54
+ if (raw.length <= visibleChars) return "****";
55
+ return `${raw.slice(0, visibleChars)}****`;
56
+ }
@@ -0,0 +1,45 @@
1
+ import * as Config from "effect/Config";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Clank from "../Util/Clank.ts";
4
+ import { AuthError } from "./AuthProvider.ts";
5
+
6
+ export const getEnv = (key: string) =>
7
+ Config.string(key)
8
+ .asEffect()
9
+ .pipe(Effect.orElseSucceed(() => undefined));
10
+
11
+ export const getEnvRequired = (key: string) =>
12
+ Config.string(key)
13
+ .asEffect()
14
+ .pipe(
15
+ Effect.catch(() =>
16
+ Effect.fail(new AuthError({ message: `Missing required env: ${key}` })),
17
+ ),
18
+ );
19
+
20
+ export const getEnvRedacted = (key: string) =>
21
+ Config.redacted(key)
22
+ .asEffect()
23
+ .pipe(Effect.orElseSucceed(() => undefined));
24
+
25
+ export const getEnvRedactedRequired = (key: string) =>
26
+ Config.redacted(key)
27
+ .asEffect()
28
+ .pipe(
29
+ Effect.catch(() =>
30
+ Effect.fail(new AuthError({ message: `Missing required env: ${key}` })),
31
+ ),
32
+ );
33
+
34
+ export const retryOnce = <A, R>(
35
+ self: Effect.Effect<A, Clank.PromptCancelled, R>,
36
+ ) =>
37
+ self.pipe(
38
+ Effect.retry({
39
+ times: 1,
40
+ while: (e) => e instanceof Clank.PromptCancelled,
41
+ }),
42
+ Effect.mapError(
43
+ (e) => new AuthError({ message: "User cancelled prompt", cause: e }),
44
+ ),
45
+ );
@@ -0,0 +1,72 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import type { PlatformError } from "effect/PlatformError";
4
+ import os from "node:os";
5
+ import path from "pathe";
6
+
7
+ export const rootDir = path.join(os.homedir(), ".alchemy");
8
+ export const configFilePath = path.join(rootDir, "profiles.json");
9
+
10
+ export const CONFIG_VERSION = 2;
11
+
12
+ export interface AlchemyProfiles {
13
+ version: typeof CONFIG_VERSION;
14
+ profiles: Record<string, AlchemyProfile>;
15
+ }
16
+
17
+ export type AlchemyProfile = Record<string, { method: string }>;
18
+
19
+ const emptyConfig = (): AlchemyProfiles => ({
20
+ version: CONFIG_VERSION,
21
+ profiles: {},
22
+ });
23
+
24
+ export const readConfig: Effect.Effect<
25
+ AlchemyProfiles,
26
+ never,
27
+ FileSystem.FileSystem
28
+ > = Effect.gen(function* () {
29
+ const fs = yield* FileSystem.FileSystem;
30
+ const data = yield* fs
31
+ .readFileString(configFilePath)
32
+ .pipe(Effect.orElseSucceed(() => undefined));
33
+ if (data === undefined) return emptyConfig();
34
+ try {
35
+ const parsed = JSON.parse(data);
36
+ if (parsed?.version !== CONFIG_VERSION) {
37
+ return emptyConfig();
38
+ }
39
+ return parsed as AlchemyProfiles;
40
+ } catch {
41
+ return emptyConfig();
42
+ }
43
+ });
44
+
45
+ export const writeConfig = (
46
+ config: AlchemyProfiles,
47
+ ): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
48
+ Effect.gen(function* () {
49
+ const fs = yield* FileSystem.FileSystem;
50
+ yield* fs.makeDirectory(path.dirname(configFilePath), {
51
+ recursive: true,
52
+ });
53
+ yield* fs.writeFileString(configFilePath, JSON.stringify(config, null, 2));
54
+ });
55
+
56
+ export const getProfile = (
57
+ name: string,
58
+ ): Effect.Effect<AlchemyProfile | undefined, never, FileSystem.FileSystem> =>
59
+ Effect.gen(function* () {
60
+ const config = yield* readConfig;
61
+ return config.profiles[name];
62
+ });
63
+
64
+ export const setProfile = (
65
+ name: string,
66
+ profile: AlchemyProfile,
67
+ ): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
68
+ Effect.gen(function* () {
69
+ const config = yield* readConfig;
70
+ config.profiles[name] = profile;
71
+ yield* writeConfig(config);
72
+ });