1import { ConfigError } from "@distilled.cloud/core/errors";
2import { Credentials, CredentialsFromEnv } from "@distilled.cloud/fly-io";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Redacted from "effect/Redacted";
7import {
8 deferUntilFirstUse,
9 orDieCredentialsUnavailable,
10 resolveProviderConfig,
11} from "../Auth/Resolve.ts";
12import * as Output from "../Output.ts";
13import {
14 FLY_AUTH_PROVIDER_NAME,
15 type FlyAuthConfig,
16 type FlyResolvedCredentials,
17} from "./AuthProvider.ts";
18
19export {
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 */
33export 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 */
56export 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 */
74export 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 );