| 1 | import * as Config from "effect/Config"; |
| 2 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Option from "effect/Option"; |
| 5 | import * as Schema from "effect/Schema"; |
| 6 | import { |
| 7 | AuthError, |
| 8 | getAuthProvider, |
| 9 | presentEnvironment, |
| 10 | } from "./AuthProvider.ts"; |
| 11 | import { |
| 12 | ALCHEMY_PROFILE, |
| 13 | DEFAULT_PROFILE_NAME, |
| 14 | ProfileError, |
| 15 | ProfileStore, |
| 16 | SuppressMissingProviderConfig, |
| 17 | } from "./Profile.ts"; |
| 18 | import { UserFacingError } from "../UserFacingError.ts"; |
| 19 | import { 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 | */ |
| 26 | export 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 | |
| 45 | export 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 | */ |
| 60 | export 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 | */ |
| 129 | export 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 | */ |
| 139 | export 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 | */ |
| 156 | export 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. */ |
| 174 | export 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 | }; |