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.
- package/bin/{alchemy-effect.js → alchemy.js} +170 -170
- 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 +38 -38
- 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/ContainerApplication.ts +3 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +2 -2
- package/src/Cloudflare/Website/StaticSite.ts +44 -15
- package/src/Cloudflare/Website/Vite.ts +34 -12
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +230 -370
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- 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 +261 -61
- package/src/Cloudflare/Workers/index.ts +3 -0
- package/src/Construct.ts +2 -2
- package/src/GitHub/Secret.ts +52 -7
- package/src/GitHub/Variable.ts +45 -2
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- 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 +4 -1
- package/bin/alchemy-effect.js.map +0 -1
- 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,392 @@
|
|
|
1
|
+
import * as DistilledAuth from "@distilled.cloud/aws/Auth";
|
|
2
|
+
import * as Console from "effect/Console";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as FileSystem from "effect/FileSystem";
|
|
5
|
+
import * as Match from "effect/Match";
|
|
6
|
+
import * as Path from "effect/Path";
|
|
7
|
+
import * as Redacted from "effect/Redacted";
|
|
8
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
9
|
+
import {
|
|
10
|
+
ChildProcess,
|
|
11
|
+
type ChildProcessSpawner,
|
|
12
|
+
} from "effect/unstable/process";
|
|
13
|
+
import * as NodeCrypto from "node:crypto";
|
|
14
|
+
import * as NodeOs from "node:os";
|
|
15
|
+
import { AuthError, type AuthProvider } from "../Auth/AuthProvider.ts";
|
|
16
|
+
import {
|
|
17
|
+
deleteCredentials,
|
|
18
|
+
displayRedacted,
|
|
19
|
+
readCredentials,
|
|
20
|
+
writeCredentials,
|
|
21
|
+
} from "../Auth/Credentials.ts";
|
|
22
|
+
import {
|
|
23
|
+
getEnv,
|
|
24
|
+
getEnvRedacted,
|
|
25
|
+
getEnvRedactedRequired,
|
|
26
|
+
retryOnce,
|
|
27
|
+
} from "../Auth/Env.ts";
|
|
28
|
+
import * as Clank from "../Util/Clank.ts";
|
|
29
|
+
|
|
30
|
+
export type AwsAuthConfig =
|
|
31
|
+
| { method: "sso"; ssoProfile: string }
|
|
32
|
+
| { method: "stored" }
|
|
33
|
+
| { method: "env" };
|
|
34
|
+
|
|
35
|
+
const options: Array<{
|
|
36
|
+
value: AwsAuthConfig["method"];
|
|
37
|
+
label: string;
|
|
38
|
+
hint?: string;
|
|
39
|
+
}> = [
|
|
40
|
+
{
|
|
41
|
+
value: "sso",
|
|
42
|
+
label: "SSO",
|
|
43
|
+
hint: "aws sso login — credentials loaded from AWS SSO cache",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
value: "env",
|
|
47
|
+
label: "Environment Variables",
|
|
48
|
+
hint: "AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: "stored",
|
|
52
|
+
label: "Stored",
|
|
53
|
+
hint: "stored in ~/.alchemy/credentials",
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export interface AwsStoredCredentials {
|
|
58
|
+
accessKeyId: string;
|
|
59
|
+
secretAccessKey: string;
|
|
60
|
+
sessionToken?: string;
|
|
61
|
+
region?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AwsResolvedCredentials {
|
|
65
|
+
accessKeyId: Redacted.Redacted<string>;
|
|
66
|
+
secretAccessKey: Redacted.Redacted<string>;
|
|
67
|
+
sessionToken?: Redacted.Redacted<string>;
|
|
68
|
+
region?: string;
|
|
69
|
+
source: { type: AwsAuthConfig["method"]; details?: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const AwsAuth = Effect.gen(function* () {
|
|
73
|
+
const context = yield* Effect.context<
|
|
74
|
+
| FileSystem.FileSystem
|
|
75
|
+
| Path.Path
|
|
76
|
+
| HttpClient.HttpClient
|
|
77
|
+
| ChildProcessSpawner.ChildProcessSpawner
|
|
78
|
+
>();
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name: "AWS",
|
|
82
|
+
configure: (profileName: string) =>
|
|
83
|
+
configureCredentials(profileName).pipe(Effect.provide(context)),
|
|
84
|
+
|
|
85
|
+
logout: (profileName, config) =>
|
|
86
|
+
logout(profileName, config).pipe(Effect.provide(context)),
|
|
87
|
+
|
|
88
|
+
login: (profileName, config) =>
|
|
89
|
+
login(profileName, config).pipe(Effect.provide(context)),
|
|
90
|
+
|
|
91
|
+
prettyPrint: (profileName, config) =>
|
|
92
|
+
prettyPrint(profileName, config).pipe(Effect.provide(context)),
|
|
93
|
+
|
|
94
|
+
read: (profileName, config) =>
|
|
95
|
+
resolveCredentials(profileName, config).pipe(Effect.provide(context)),
|
|
96
|
+
} satisfies AuthProvider<AwsAuthConfig, AwsResolvedCredentials>;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const runSsoCommand = (command: "login" | "logout", ssoProfile: string) =>
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
const handle = yield* ChildProcess.make(
|
|
102
|
+
"aws",
|
|
103
|
+
["sso", command, "--profile", ssoProfile],
|
|
104
|
+
{
|
|
105
|
+
shell: false,
|
|
106
|
+
stdin: "inherit",
|
|
107
|
+
stdout: "inherit",
|
|
108
|
+
stderr: "inherit",
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
const exit = yield* handle.exitCode;
|
|
112
|
+
if (exit !== 0) {
|
|
113
|
+
yield* Effect.fail(
|
|
114
|
+
new AuthError({
|
|
115
|
+
message: `aws sso ${command} exited with code ${exit}`,
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}).pipe(Effect.scoped);
|
|
120
|
+
|
|
121
|
+
const resolveCredentials = (profileName: string, config: AwsAuthConfig) =>
|
|
122
|
+
Match.value(config)
|
|
123
|
+
.pipe(
|
|
124
|
+
Match.when(
|
|
125
|
+
{ method: "env" },
|
|
126
|
+
Effect.fnUntraced(function* () {
|
|
127
|
+
const accessKeyId =
|
|
128
|
+
yield* getEnvRedactedRequired("AWS_ACCESS_KEY_ID");
|
|
129
|
+
const secretAccessKey = yield* getEnvRedactedRequired(
|
|
130
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
131
|
+
);
|
|
132
|
+
const sessionToken = yield* getEnvRedacted("AWS_SESSION_TOKEN");
|
|
133
|
+
const region = yield* (
|
|
134
|
+
getEnv("AWS_REGION") ?? getEnv("AWS_DEFAULT_REGION") ?? undefined
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
accessKeyId,
|
|
138
|
+
secretAccessKey,
|
|
139
|
+
sessionToken,
|
|
140
|
+
region,
|
|
141
|
+
source: { type: "env" as const },
|
|
142
|
+
};
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
Match.when({ method: "stored" }, () =>
|
|
146
|
+
readCredentials<AwsStoredCredentials>(profileName, "aws").pipe(
|
|
147
|
+
Effect.flatMap((creds) =>
|
|
148
|
+
creds == null
|
|
149
|
+
? Effect.fail(
|
|
150
|
+
new AuthError({
|
|
151
|
+
message:
|
|
152
|
+
"AWS stored credentials not found. Run: alchemy-effect login --configure",
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
: Effect.succeed({
|
|
156
|
+
accessKeyId: Redacted.make(creds.accessKeyId),
|
|
157
|
+
secretAccessKey: Redacted.make(creds.secretAccessKey),
|
|
158
|
+
sessionToken: creds.sessionToken
|
|
159
|
+
? Redacted.make(creds.sessionToken)
|
|
160
|
+
: undefined,
|
|
161
|
+
region: creds.region,
|
|
162
|
+
source: { type: "stored" as const },
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
Match.when({ method: "sso" }, (config) =>
|
|
168
|
+
Effect.gen(function* () {
|
|
169
|
+
const auth = yield* DistilledAuth.Default;
|
|
170
|
+
const creds = yield* auth
|
|
171
|
+
.loadProfileCredentials(config.ssoProfile)
|
|
172
|
+
.pipe(
|
|
173
|
+
Effect.mapError(
|
|
174
|
+
(e) =>
|
|
175
|
+
new AuthError({
|
|
176
|
+
message: "failed to load credentials",
|
|
177
|
+
cause: e,
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
const profile = yield* auth
|
|
182
|
+
.loadProfile(config.ssoProfile)
|
|
183
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
184
|
+
return {
|
|
185
|
+
accessKeyId: creds.accessKeyId,
|
|
186
|
+
secretAccessKey: creds.secretAccessKey,
|
|
187
|
+
sessionToken: creds.sessionToken,
|
|
188
|
+
region: profile?.region,
|
|
189
|
+
source: { type: "sso" as const, details: config.ssoProfile },
|
|
190
|
+
};
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
Match.exhaustive,
|
|
194
|
+
)
|
|
195
|
+
.pipe(
|
|
196
|
+
Effect.mapError(
|
|
197
|
+
(e) => new AuthError({ message: "login failed", cause: e }),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const prettyPrint = (profileName: string, config: AwsAuthConfig) =>
|
|
202
|
+
resolveCredentials(profileName, config).pipe(
|
|
203
|
+
Effect.tap((creds) =>
|
|
204
|
+
Effect.all([
|
|
205
|
+
Console.log(` accessKeyId: ${displayRedacted(creds.accessKeyId)}`),
|
|
206
|
+
Console.log(
|
|
207
|
+
` secretAccessKey: ${displayRedacted(creds.secretAccessKey)}`,
|
|
208
|
+
),
|
|
209
|
+
creds.sessionToken
|
|
210
|
+
? Console.log(
|
|
211
|
+
` sessionToken: ${displayRedacted(creds.sessionToken)}`,
|
|
212
|
+
)
|
|
213
|
+
: Effect.void,
|
|
214
|
+
creds.region
|
|
215
|
+
? Console.log(` region: ${creds.region}`)
|
|
216
|
+
: Effect.void,
|
|
217
|
+
Console.log(
|
|
218
|
+
//@ts-expect-error
|
|
219
|
+
` source: ${creds.source.details ? `${creds.source.type} - ${creds.source.details}` : creds.source.type}`,
|
|
220
|
+
),
|
|
221
|
+
]),
|
|
222
|
+
),
|
|
223
|
+
Effect.catch((e) =>
|
|
224
|
+
Console.error(` Failed to retrieve credentials: ${e}`),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* `aws sso logout` only clears AWS CLI's own caches — it does not know about the
|
|
230
|
+
* `<sha1(sso_session)>.credentials.json` file that `@distilled.cloud/aws`
|
|
231
|
+
* writes alongside the SSO token. Without this cleanup, `loadProfileCredentials`
|
|
232
|
+
* short-circuits on the stale distilled cache file after logout and appears to
|
|
233
|
+
* stay logged in until the role creds hit their TTL.
|
|
234
|
+
*/
|
|
235
|
+
const clearDistilledSsoCache = (ssoProfile: string) =>
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
const auth = yield* DistilledAuth.Default;
|
|
238
|
+
const profile = yield* auth.loadProfile(ssoProfile);
|
|
239
|
+
const ssoSession = (profile as { sso_session?: string }).sso_session;
|
|
240
|
+
if (!ssoSession) return;
|
|
241
|
+
const fs = yield* FileSystem.FileSystem;
|
|
242
|
+
const path = yield* Path.Path;
|
|
243
|
+
const hash = NodeCrypto.createHash("sha1").update(ssoSession).digest("hex");
|
|
244
|
+
const cacheFile = path.join(
|
|
245
|
+
NodeOs.homedir(),
|
|
246
|
+
".aws",
|
|
247
|
+
"sso",
|
|
248
|
+
"cache",
|
|
249
|
+
`${hash}.credentials.json`,
|
|
250
|
+
);
|
|
251
|
+
yield* fs.remove(cacheFile).pipe(Effect.catch(() => Effect.void));
|
|
252
|
+
}).pipe(Effect.catch(() => Effect.void));
|
|
253
|
+
|
|
254
|
+
const logout = (profileName: string, config: AwsAuthConfig) =>
|
|
255
|
+
Match.value(config).pipe(
|
|
256
|
+
Match.when({ method: "env" }, () => Effect.void),
|
|
257
|
+
Match.when({ method: "sso" }, (config) =>
|
|
258
|
+
Clank.info(
|
|
259
|
+
`AWS: running 'aws sso logout --profile ${config.ssoProfile}'...`,
|
|
260
|
+
).pipe(
|
|
261
|
+
Effect.zip(runSsoCommand("logout", config.ssoProfile)),
|
|
262
|
+
Effect.zip(clearDistilledSsoCache(config.ssoProfile)),
|
|
263
|
+
Effect.match({
|
|
264
|
+
onSuccess: () => Clank.success("AWS: SSO logout complete"),
|
|
265
|
+
onFailure: (e) =>
|
|
266
|
+
Clank.warn(`AWS: SSO logout failed: \`${e.message}\``),
|
|
267
|
+
}),
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
Match.when({ method: "stored" }, () =>
|
|
271
|
+
deleteCredentials(profileName, "aws").pipe(
|
|
272
|
+
Effect.andThen(Clank.success("AWS: stored credentials removed")),
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
Match.exhaustive,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const login = (profileName: string, config: AwsAuthConfig) =>
|
|
279
|
+
Match.value(config)
|
|
280
|
+
.pipe(
|
|
281
|
+
Match.when({ method: "env" }, () => Effect.void),
|
|
282
|
+
Match.when({ method: "sso" }, (config) =>
|
|
283
|
+
DistilledAuth.loadProfileCredentials(config.ssoProfile).pipe(
|
|
284
|
+
Effect.matchEffect({
|
|
285
|
+
onSuccess: () =>
|
|
286
|
+
Clank.info(
|
|
287
|
+
`AWS: SSO profile '${config.ssoProfile}' already has valid credentials`,
|
|
288
|
+
),
|
|
289
|
+
onFailure: () => loginSSO(config),
|
|
290
|
+
}),
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
Match.when({ method: "stored" }, () =>
|
|
294
|
+
readCredentials<AwsStoredCredentials>(profileName, "aws").pipe(
|
|
295
|
+
Effect.flatMap((creds) =>
|
|
296
|
+
creds == null ? loginStored(profileName) : Effect.void,
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
),
|
|
300
|
+
Match.exhaustive,
|
|
301
|
+
)
|
|
302
|
+
.pipe(
|
|
303
|
+
Effect.mapError(
|
|
304
|
+
(e) => new AuthError({ message: "login failed", cause: e }),
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const configureCredentials = (profileName: string) =>
|
|
309
|
+
Clank.select({
|
|
310
|
+
message: "AWS authentication method",
|
|
311
|
+
options,
|
|
312
|
+
})
|
|
313
|
+
.pipe(
|
|
314
|
+
Effect.flatMap((method) =>
|
|
315
|
+
Match.value(method).pipe(
|
|
316
|
+
Match.when("env", () => Effect.succeed({ method: "env" as const })),
|
|
317
|
+
Match.when("sso", () =>
|
|
318
|
+
Effect.gen(function* () {
|
|
319
|
+
const ssoProfile = yield* Clank.text({
|
|
320
|
+
message: "AWS profile name (from ~/.aws/config)",
|
|
321
|
+
placeholder: "default",
|
|
322
|
+
defaultValue: "default",
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const config = {
|
|
326
|
+
method: "sso" as const,
|
|
327
|
+
ssoProfile: ssoProfile ?? "default",
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
yield* loginSSO(config);
|
|
331
|
+
|
|
332
|
+
return config;
|
|
333
|
+
}),
|
|
334
|
+
),
|
|
335
|
+
Match.when("stored", () => loginStored(profileName)),
|
|
336
|
+
Match.exhaustive,
|
|
337
|
+
),
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
.pipe(
|
|
341
|
+
Effect.mapError(
|
|
342
|
+
(e) =>
|
|
343
|
+
new AuthError({
|
|
344
|
+
message: "failed to configure credentials",
|
|
345
|
+
cause: e,
|
|
346
|
+
}),
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const loginSSO = (config: Extract<AwsAuthConfig, { method: "sso" }>) =>
|
|
351
|
+
Clank.info(
|
|
352
|
+
`AWS SSO: running 'aws sso login --profile ${config.ssoProfile}'...`,
|
|
353
|
+
).pipe(
|
|
354
|
+
Effect.andThen(runSsoCommand("login", config.ssoProfile)),
|
|
355
|
+
Effect.matchEffect({
|
|
356
|
+
onSuccess: () => Clank.success("AWS SSO: login complete"),
|
|
357
|
+
onFailure: (e) => Clank.warn(`AWS SSO: login faield: \`${e.message}\``),
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const loginStored = Effect.fnUntraced(function* (profileName: string) {
|
|
362
|
+
const accessKeyId = yield* Clank.text({
|
|
363
|
+
message: "AWS Access Key ID",
|
|
364
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
365
|
+
}).pipe(retryOnce);
|
|
366
|
+
|
|
367
|
+
const secretAccessKey = yield* Clank.password({
|
|
368
|
+
message: "AWS Secret Access Key",
|
|
369
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
370
|
+
}).pipe(retryOnce);
|
|
371
|
+
|
|
372
|
+
const sessionToken = yield* Clank.text({
|
|
373
|
+
message: "AWS Session Token (optional — press Enter or Esc to skip)",
|
|
374
|
+
placeholder: "(none)",
|
|
375
|
+
}).pipe(Effect.catch(() => Effect.succeed("")));
|
|
376
|
+
|
|
377
|
+
const region = yield* Clank.text({
|
|
378
|
+
message: "AWS Region",
|
|
379
|
+
placeholder: "us-east-1",
|
|
380
|
+
defaultValue: "us-east-1",
|
|
381
|
+
}).pipe(retryOnce);
|
|
382
|
+
|
|
383
|
+
yield* writeCredentials<AwsStoredCredentials>(profileName, "aws", {
|
|
384
|
+
accessKeyId,
|
|
385
|
+
secretAccessKey,
|
|
386
|
+
sessionToken,
|
|
387
|
+
region,
|
|
388
|
+
});
|
|
389
|
+
yield* Clank.success("AWS credentials saved.");
|
|
390
|
+
|
|
391
|
+
return { method: "stored" as const };
|
|
392
|
+
});
|
package/src/AWS/Credentials.ts
CHANGED
|
@@ -7,7 +7,6 @@ import * as Effect from "effect/Effect";
|
|
|
7
7
|
import * as Layer from "effect/Layer";
|
|
8
8
|
import * as Option from "effect/Option";
|
|
9
9
|
import { Profile } from "./Profile.ts";
|
|
10
|
-
|
|
11
10
|
import { StageConfig } from "./StageConfig.ts";
|
|
12
11
|
|
|
13
12
|
export { Credentials } from "@distilled.cloud/aws/Credentials";
|
|
@@ -115,7 +115,9 @@ export interface Table extends Resource<
|
|
|
115
115
|
* @section Creating Tables
|
|
116
116
|
* @example Basic Table
|
|
117
117
|
* ```typescript
|
|
118
|
-
*
|
|
118
|
+
* import * as DynamoDB from "alchemy/AWS/DynamoDB";
|
|
119
|
+
*
|
|
120
|
+
* const table = yield* DynamoDB.Table("UsersTable", {
|
|
119
121
|
* partitionKey: "pk",
|
|
120
122
|
* attributes: {
|
|
121
123
|
* pk: "S",
|
|
@@ -125,7 +127,7 @@ export interface Table extends Resource<
|
|
|
125
127
|
*
|
|
126
128
|
* @example Table with Sort Key and TTL
|
|
127
129
|
* ```typescript
|
|
128
|
-
* const table = yield* Table("SessionsTable", {
|
|
130
|
+
* const table = yield* DynamoDB.Table("SessionsTable", {
|
|
129
131
|
* partitionKey: "userId",
|
|
130
132
|
* sortKey: "sessionId",
|
|
131
133
|
* attributes: {
|
|
@@ -140,18 +142,69 @@ export interface Table extends Resource<
|
|
|
140
142
|
* });
|
|
141
143
|
* ```
|
|
142
144
|
*
|
|
143
|
-
* @
|
|
144
|
-
* @example Read an Item
|
|
145
|
+
* @example Table with Global Secondary Index
|
|
145
146
|
* ```typescript
|
|
146
|
-
* const
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* pk:
|
|
147
|
+
* const table = yield* DynamoDB.Table("OrdersTable", {
|
|
148
|
+
* partitionKey: "pk",
|
|
149
|
+
* sortKey: "sk",
|
|
150
|
+
* attributes: {
|
|
151
|
+
* pk: "S",
|
|
152
|
+
* sk: "S",
|
|
153
|
+
* gsi1pk: "S",
|
|
154
|
+
* gsi1sk: "S",
|
|
151
155
|
* },
|
|
152
|
-
*
|
|
156
|
+
* globalSecondaryIndexes: [{
|
|
157
|
+
* IndexName: "GSI1",
|
|
158
|
+
* KeySchema: [
|
|
159
|
+
* { AttributeName: "gsi1pk", KeyType: "HASH" },
|
|
160
|
+
* { AttributeName: "gsi1sk", KeyType: "RANGE" },
|
|
161
|
+
* ],
|
|
162
|
+
* Projection: { ProjectionType: "ALL" },
|
|
163
|
+
* }],
|
|
153
164
|
* });
|
|
154
165
|
* ```
|
|
166
|
+
*
|
|
167
|
+
* @section Runtime Operations
|
|
168
|
+
* Bind DynamoDB operations in the init phase and use them in runtime
|
|
169
|
+
* handlers. Bindings inject the table name and grant scoped IAM
|
|
170
|
+
* permissions automatically.
|
|
171
|
+
*
|
|
172
|
+
* @example Read and write items
|
|
173
|
+
* ```typescript
|
|
174
|
+
* // init
|
|
175
|
+
* const getItem = yield* DynamoDB.GetItem.bind(table);
|
|
176
|
+
* const putItem = yield* DynamoDB.PutItem.bind(table);
|
|
177
|
+
*
|
|
178
|
+
* return {
|
|
179
|
+
* fetch: Effect.gen(function* () {
|
|
180
|
+
* // runtime
|
|
181
|
+
* yield* putItem({
|
|
182
|
+
* Item: { pk: { S: "user#123" }, name: { S: "Alice" } },
|
|
183
|
+
* });
|
|
184
|
+
* const result = yield* getItem({
|
|
185
|
+
* Key: { pk: { S: "user#123" } },
|
|
186
|
+
* });
|
|
187
|
+
* return yield* HttpServerResponse.json(result.Item);
|
|
188
|
+
* }),
|
|
189
|
+
* };
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* @section DynamoDB Streams
|
|
193
|
+
* Process change data capture events from a DynamoDB table using a
|
|
194
|
+
* Lambda event source mapping. The stream is enabled automatically
|
|
195
|
+
* through the binding contract.
|
|
196
|
+
*
|
|
197
|
+
* @example Process table changes
|
|
198
|
+
* ```typescript
|
|
199
|
+
* // init
|
|
200
|
+
* yield* DynamoDB.streams(table, {
|
|
201
|
+
* StreamViewType: "NEW_AND_OLD_IMAGES",
|
|
202
|
+
* }).process(
|
|
203
|
+
* Effect.fn(function* (record) {
|
|
204
|
+
* yield* Effect.log(`${record.eventName}: ${JSON.stringify(record.dynamodb)}`);
|
|
205
|
+
* }),
|
|
206
|
+
* );
|
|
207
|
+
* ```
|
|
155
208
|
*/
|
|
156
209
|
export const Table = Resource<Table>("AWS.DynamoDB.Table");
|
|
157
210
|
|
|
@@ -9,10 +9,10 @@ import type { ScopedPlanStatusSession } from "../../Cli/Cli.ts";
|
|
|
9
9
|
import { isResolved } from "../../Diff.ts";
|
|
10
10
|
import * as Provider from "../../Provider.ts";
|
|
11
11
|
import { Resource } from "../../Resource.ts";
|
|
12
|
-
import type { Providers } from "../Providers.ts";
|
|
13
12
|
import { createInternalTags, createTagsList, diffTags } from "../../Tags.ts";
|
|
14
13
|
import type { AccountID } from "../Account.ts";
|
|
15
14
|
import { Account } from "../Account.ts";
|
|
15
|
+
import type { Providers } from "../Providers.ts";
|
|
16
16
|
import type { RegionID } from "../Region.ts";
|
|
17
17
|
import type { RouteTableId } from "./RouteTable.ts";
|
|
18
18
|
import type { SecurityGroupId } from "./SecurityGroup.ts";
|
|
@@ -595,11 +595,11 @@ const waitForVpcEndpointAvailable = (
|
|
|
595
595
|
return yield* new VpcEndpointNotFound({ vpcEndpointId });
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
-
if (ep.State === "
|
|
598
|
+
if (ep.State === "available") {
|
|
599
599
|
return ep;
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
if (ep.State === "
|
|
602
|
+
if (ep.State === "failed" || ep.State === "rejected") {
|
|
603
603
|
return yield* new VpcEndpointFailed({
|
|
604
604
|
vpcEndpointId,
|
|
605
605
|
errorCode: ep.LastError?.Code,
|
|
@@ -641,7 +641,7 @@ const waitForVpcEndpointDeleted = (
|
|
|
641
641
|
|
|
642
642
|
const ep = result.VpcEndpoints?.[0];
|
|
643
643
|
|
|
644
|
-
if (!ep || ep.State === "
|
|
644
|
+
if (!ep || ep.State === "deleted") {
|
|
645
645
|
return; // Successfully deleted
|
|
646
646
|
}
|
|
647
647
|
|
package/src/AWS/EC2/hosted.ts
CHANGED
|
@@ -223,7 +223,7 @@ export const createEc2HostedSupport = ({
|
|
|
223
223
|
virtualEntryPlugin(
|
|
224
224
|
(importPath) => `
|
|
225
225
|
import { NodeServices } from "@effect/platform-node";
|
|
226
|
-
import { Stack } from "alchemy
|
|
226
|
+
import { Stack } from "alchemy/Stack";
|
|
227
227
|
import * as Config from "effect/Config";
|
|
228
228
|
import * as ConfigProvider from "effect/ConfigProvider";
|
|
229
229
|
import * as Credentials from "@distilled.cloud/aws/Credentials";
|
package/src/AWS/ECS/Task.ts
CHANGED
|
@@ -486,7 +486,7 @@ export const TaskProvider = () =>
|
|
|
486
486
|
virtualEntryPlugin(
|
|
487
487
|
(importPath) => `
|
|
488
488
|
import { NodeServices } from "@effect/platform-node";
|
|
489
|
-
import { Stack } from "alchemy
|
|
489
|
+
import { Stack } from "alchemy/Stack";
|
|
490
490
|
import * as Config from "effect/Config";
|
|
491
491
|
import * as ConfigProvider from "effect/ConfigProvider";
|
|
492
492
|
import * as Credentials from "@distilled.cloud/aws/Credentials";
|
|
@@ -170,32 +170,68 @@ export interface Stream extends Resource<
|
|
|
170
170
|
*
|
|
171
171
|
* `Stream` owns the stream's lifecycle and mutable control-plane configuration,
|
|
172
172
|
* including retention, encryption, monitoring, warm throughput, record size, tags,
|
|
173
|
-
* and stream resource policy.
|
|
173
|
+
* and stream resource policy. A stream name is auto-generated from the app,
|
|
174
|
+
* stage, and logical ID unless you provide one explicitly.
|
|
174
175
|
*
|
|
175
176
|
* @section Creating Streams
|
|
176
177
|
* @example On-Demand Stream
|
|
177
178
|
* ```typescript
|
|
178
|
-
*
|
|
179
|
+
* import * as Kinesis from "alchemy/AWS/Kinesis";
|
|
180
|
+
*
|
|
181
|
+
* const stream = yield* Kinesis.Stream("OrdersStream");
|
|
179
182
|
* ```
|
|
180
183
|
*
|
|
181
184
|
* @example Provisioned Stream
|
|
182
185
|
* ```typescript
|
|
183
|
-
* const stream = yield* Stream("AnalyticsStream", {
|
|
186
|
+
* const stream = yield* Kinesis.Stream("AnalyticsStream", {
|
|
184
187
|
* streamMode: "PROVISIONED",
|
|
185
188
|
* shardCount: 2,
|
|
186
189
|
* retentionPeriodHours: 48,
|
|
187
190
|
* });
|
|
188
191
|
* ```
|
|
189
192
|
*
|
|
193
|
+
* @example Encrypted Stream
|
|
194
|
+
* ```typescript
|
|
195
|
+
* const stream = yield* Kinesis.Stream("SecureStream", {
|
|
196
|
+
* encryption: true,
|
|
197
|
+
* kmsKeyId: "alias/my-key",
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
190
201
|
* @section Runtime Producers
|
|
191
|
-
*
|
|
202
|
+
* Bind producer operations in the init phase and use them in runtime
|
|
203
|
+
* handlers.
|
|
204
|
+
*
|
|
205
|
+
* @example Put a record from a handler
|
|
192
206
|
* ```typescript
|
|
207
|
+
* // init
|
|
193
208
|
* const putRecord = yield* Kinesis.PutRecord.bind(stream);
|
|
194
209
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
210
|
+
* return {
|
|
211
|
+
* fetch: Effect.gen(function* () {
|
|
212
|
+
* // runtime
|
|
213
|
+
* yield* putRecord({
|
|
214
|
+
* PartitionKey: "order-123",
|
|
215
|
+
* Data: new TextEncoder().encode(JSON.stringify({ orderId: "123" })),
|
|
216
|
+
* });
|
|
217
|
+
* return HttpServerResponse.text("Sent");
|
|
218
|
+
* }),
|
|
219
|
+
* };
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @section Event Sources
|
|
223
|
+
* Process records from a Kinesis stream using a Lambda event source
|
|
224
|
+
* mapping.
|
|
225
|
+
*
|
|
226
|
+
* @example Process stream records
|
|
227
|
+
* ```typescript
|
|
228
|
+
* // init
|
|
229
|
+
* yield* Kinesis.records(stream).process(
|
|
230
|
+
* Effect.fn(function* (record) {
|
|
231
|
+
* const data = new TextDecoder().decode(record.data);
|
|
232
|
+
* yield* Effect.log(`Received: ${data}`);
|
|
233
|
+
* }),
|
|
234
|
+
* );
|
|
199
235
|
* ```
|
|
200
236
|
*/
|
|
201
237
|
export const Stream = Resource<Stream>("AWS.Kinesis.Stream");
|