alchemy 2.0.0-beta.4 → 2.0.0-beta.test-export-fix-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.
@@ -1,392 +0,0 @@
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
- });
@@ -1,33 +0,0 @@
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
- }
@@ -1,56 +0,0 @@
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
- }
package/src/Auth/Env.ts DELETED
@@ -1,45 +0,0 @@
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
- );
@@ -1,72 +0,0 @@
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
- });
package/src/Auth/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./AuthProvider.ts";
2
- export type { AlchemyProfile, AlchemyProfiles } from "./Profile.ts";