| 1 | import { ConfigError } from "@distilled.cloud/core/errors"; |
| 2 | import { Credentials, CredentialsFromEnv } from "@distilled.cloud/fly-io"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Option from "effect/Option"; |
| 6 | import * as Redacted from "effect/Redacted"; |
| 7 | import { |
| 8 | deferUntilFirstUse, |
| 9 | orDieCredentialsUnavailable, |
| 10 | resolveProviderConfig, |
| 11 | } from "../Auth/Resolve.ts"; |
| 12 | import * as Output from "../Output.ts"; |
| 13 | import { |
| 14 | FLY_AUTH_PROVIDER_NAME, |
| 15 | type FlyAuthConfig, |
| 16 | type FlyResolvedCredentials, |
| 17 | } from "./AuthProvider.ts"; |
| 18 | |
| 19 | export { |
| 20 | Credentials, |
| 21 | CredentialsFromEnv, |
| 22 | credentials, |
| 23 | DEFAULT_API_BASE_URL, |
| 24 | normalizeApiBaseUrl, |
| 25 | type Config as CredentialsConfig, |
| 26 | } from "@distilled.cloud/fly-io"; |
| 27 | |
| 28 | /** |
| 29 | * Capture the deployment's org token as a named runtime Output. |
| 30 | * Resolve it during binding initialization so plan/diff retains |
| 31 | * profile-backed credentials rather than falling back to process env. |
| 32 | */ |
| 33 | export const bindFlyApiToken = (): Effect.Effect<void, never, Credentials> => |
| 34 | Effect.gen(function* () { |
| 35 | const token = globalThis.__ALCHEMY_RUNTIME__ |
| 36 | ? "" |
| 37 | : yield* Credentials.pipe( |
| 38 | Effect.flatMap((resolve) => resolve), |
| 39 | Effect.map((cfg) => Redacted.value(cfg.apiKey)), |
| 40 | ); |
| 41 | yield* Output.named(Output.asOutput(token), "FLY_API_TOKEN"); |
| 42 | }); |
| 43 | |
| 44 | /** |
| 45 | * `Credentials` for the HTTP binding layers (`GetSecretHttp`, `ExecHttp`, …). |
| 46 | * |
| 47 | * Those layers are built in two places. Inside a stack (plan/deploy, or an |
| 48 | * Action) `providers()` has already resolved the profile-backed |
| 49 | * `Credentials`, and the binding must use them — a laptop deploy has no |
| 50 | * `FLY_API_TOKEN` in its env once the token lives in the Alchemy profile. |
| 51 | * Inside a deployed Machine there is no profile; `bindFlyApiToken` |
| 52 | * has already `RuntimeContext.set` the org token, which Platform copies |
| 53 | * into Machine env as `FLY_API_TOKEN`. So: reuse the ambient |
| 54 | * `Credentials` when present, otherwise read the env. |
| 55 | */ |
| 56 | export const CredentialsFromAmbientOrEnv: Layer.Layer<Credentials> = |
| 57 | Layer.effect( |
| 58 | Credentials, |
| 59 | Effect.gen(function* () { |
| 60 | const ambient = yield* Effect.serviceOption(Credentials); |
| 61 | if (Option.isSome(ambient)) return ambient.value; |
| 62 | return yield* Credentials.pipe(Effect.provide(CredentialsFromEnv)); |
| 63 | }), |
| 64 | ); |
| 65 | |
| 66 | /** |
| 67 | * Build a `Credentials` layer that resolves Fly credentials via the current |
| 68 | * Alchemy profile, or directly from environment variables in CI. |
| 69 | * |
| 70 | * Maps onto `@distilled.cloud/fly-io`'s `{ apiKey, apiBaseUrl }` shape. |
| 71 | * Distilled's own `CredentialsFromEnv` also accepts `FLY_IO_API_KEY` as a |
| 72 | * fallback — Alchemy itself only reads `FLY_API_TOKEN`. |
| 73 | */ |
| 74 | export const fromAuthProvider = () => |
| 75 | Layer.effect( |
| 76 | Credentials, |
| 77 | Effect.gen(function* () { |
| 78 | // Defer profile lookup and credential resolution until first use, so |
| 79 | // building the provider layers never requires a configured profile. |
| 80 | const resolve = yield* resolveProviderConfig< |
| 81 | FlyAuthConfig, |
| 82 | FlyResolvedCredentials |
| 83 | >(FLY_AUTH_PROVIDER_NAME).pipe( |
| 84 | Effect.flatMap(({ profileName, resolve }) => |
| 85 | resolve.pipe( |
| 86 | Effect.map((creds) => ({ |
| 87 | apiKey: creds.apiKey, |
| 88 | apiBaseUrl: creds.apiBaseUrl, |
| 89 | })), |
| 90 | Effect.mapError( |
| 91 | (e) => |
| 92 | new ConfigError({ |
| 93 | message: `Failed to resolve Fly credentials from ${profileName === undefined ? "the CI environment" : `profile '${profileName}'`}: ${(e as { message?: string }).message ?? String(e)}`, |
| 94 | }), |
| 95 | ), |
| 96 | ), |
| 97 | ), |
| 98 | deferUntilFirstUse, |
| 99 | ); |
| 100 | return yield* resolve.pipe( |
| 101 | orDieCredentialsUnavailable(FLY_AUTH_PROVIDER_NAME), |
| 102 | Effect.cached, |
| 103 | ); |
| 104 | }), |
| 105 | ); |