1import * as Config from "effect/Config";
2import * as ConfigProvider from "effect/ConfigProvider";
3import * as Effect from "effect/Effect";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6import {
7 AuthError,
8 getAuthProvider,
9 presentEnvironment,
10} from "./AuthProvider.ts";
11import {
12 ALCHEMY_PROFILE,
13 DEFAULT_PROFILE_NAME,
14 ProfileError,
15 ProfileStore,
16 SuppressMissingProviderConfig,
17} from "./Profile.ts";
18import { UserFacingError } from "../UserFacingError.ts";
19import { loadConfigProvider } from "../Util/ConfigProvider.ts";
20
21/**
22 * Resolve the selected Alchemy profile after the command's dotenv provider is
23 * known. An omitted explicit override remains absent so it does not shadow
24 * `ALCHEMY_PROFILE` from `.env` / `--env-file`.
25 */
26export const resolveProfileSelection = Effect.fn(function* (
27 envFile: Option.Option<string>,
28 override: string | undefined,
29) {
30 const base = yield* loadConfigProvider(envFile);
31 const profiles = yield* ProfileStore;
32 const selected = yield* profiles.current.pipe(
33 Effect.provideService(
34 ConfigProvider.ConfigProvider,
35 withProfileOverride(base, override),
36 ),
37 );
38 return {
39 ...selected,
40 source:
41 override === undefined ? selected.source : ("command-line" as const),
42 };
43});
44
45export const resolveProfileName = Effect.fn(function* (
46 envFile: Option.Option<string>,
47 override: string | undefined,
48) {
49 return (yield* resolveProfileSelection(envFile, override)).name;
50});
51
52/**
53 * The shared preamble of every per-cloud `fromAuthProvider` /
54 * `fromEnvironment` layer. Precedence: environment credentials (process
55 * environment plus `.env` / `--env-file`) whenever the provider's declared
56 * contract is fully present — CI or not, selected profile or not — then, in
57 * CI, the provider's environment resolution alone (profiles do not exist
58 * there), otherwise the selected profile.
59 */
60export const resolveProviderConfig = <
61 C extends { method: string } = any,
62 Credentials = any,
63>(
64 providerName: string,
65) =>
66 Effect.gen(function* () {
67 const auth = yield* getAuthProvider<C, Credentials>(providerName);
68 if (auth.readEnvironment !== undefined) {
69 const used = yield* presentEnvironment(auth.environment);
70 if (used !== undefined) {
71 if (!(yield* SuppressMissingProviderConfig)) {
72 yield* auth.logEnvironmentCredentials(used);
73 }
74 return {
75 auth,
76 profileName: undefined,
77 config: undefined,
78 resolve: auth.readEnvironment,
79 source: "environment" as const,
80 };
81 }
82 }
83 const ci = yield* Config.Boolean("CI").pipe(Config.withDefault(false));
84 if (ci) {
85 if (auth.readEnvironment === undefined) {
86 return yield* Effect.fail(
87 new AuthError({
88 message: `Auth provider '${providerName}' does not support environment credentials in CI.`,
89 }),
90 );
91 }
92 return {
93 auth,
94 profileName: undefined,
95 config: undefined,
96 resolve: auth.readEnvironment,
97 source: "environment" as const,
98 };
99 }
100 const profile = yield* ProfileStore;
101 const selection = yield* profile.current;
102 const profileName = selection.name;
103 const config = yield* profile.loadProviderConfig(auth, profileName);
104 return {
105 auth,
106 profileName,
107 config,
108 resolve: auth.read(profileName, config, (updated) =>
109 profile.setProviderConfig(profileName, providerName, updated).pipe(
110 Effect.mapError(
111 (cause) =>
112 new AuthError({
113 message: `${providerName}: could not persist refreshed credentials for profile '${profileName}'.`,
114 cause,
115 }),
116 ),
117 ),
118 ),
119 source: "profile" as const,
120 };
121 });
122
123/**
124 * Defer an effect that a layer hands out as its service value. The effect
125 * runs on first use, outside the layer build, so the services it needs are
126 * captured now and provided to it. Building the layer then never runs the
127 * effect, e.g. never requires a configured profile.
128 */
129export const deferUntilFirstUse = <A, E, R>(resolve: Effect.Effect<A, E, R>) =>
130 Effect.map(Effect.context<R>(), (context) =>
131 resolve.pipe(Effect.provideContext(context)),
132 );
133
134/**
135 * A provider's credentials could not be resolved when a cloud operation first
136 * needed them: no profile, the provider is not configured in it, or the
137 * stored credentials can no longer be read or refreshed.
138 */
139export class CredentialsUnavailable extends Schema.TaggedError<CredentialsUnavailable>()(
140 "CredentialsUnavailable",
141 {
142 provider: Schema.String,
143 message: Schema.String,
144 cause: Schema.optional(Schema.Defect()),
145 },
146) {
147 readonly [UserFacingError] = true;
148}
149
150/**
151 * Credential services are `Effect<Config>` with no error channel, so a
152 * failed resolution can only leave them as a defect. Die with
153 * {@link CredentialsUnavailable} so the engine can tell it apart from a
154 * provider crash and fail the operation with `CredentialsRequired`.
155 */
156export const orDieCredentialsUnavailable =
157 (provider: string) =>
158 <A, E extends { readonly message: string }, R>(
159 self: Effect.Effect<A, E, R>,
160 ) =>
161 self.pipe(
162 Effect.mapError(
163 (cause) =>
164 new CredentialsUnavailable({
165 provider,
166 message: cause.message,
167 cause,
168 }),
169 ),
170 Effect.orDie,
171 );
172
173/** Let an explicit profile override configured selection without disturbing other keys. */
174export const withProfileOverride = (
175 base: ConfigProvider.ConfigProvider,
176 profile: string | undefined,
177): ConfigProvider.ConfigProvider => {
178 if (profile === undefined) return base;
179 const overrides: Record<string, string> = { ALCHEMY_PROFILE: profile };
180 const overrideProvider = ConfigProvider.make((path) =>
181 Effect.succeed(
182 path.length === 1 && typeof path[0] === "string" && path[0] in overrides
183 ? ConfigProvider.makeValue(overrides[path[0]]!)
184 : undefined,
185 ),
186 );
187 return ConfigProvider.orElse(base)(overrideProvider);
188};