1import * as ConfigProvider from "effect/ConfigProvider";
2import * as Effect from "effect/Effect";
3import type * as FileSystem from "effect/FileSystem";
4import type * as Layer from "effect/Layer";
5import type * as Path from "effect/Path";
6import type * as HttpClient from "effect/http/HttpClient";
7import type * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
8import { AlchemyContext } from "../AlchemyContext.ts";
9import type { AuthProviders } from "../Auth/AuthProvider.ts";
10import type { ProfileStore } from "../Auth/Profile.ts";
11import { Stack } from "../Stack.ts";
12import { Stage } from "../Stage.ts";
13
14/** What a secrets provider may depend on. */
15export type SecretsServices =
16 | Stage
17 | Stack
18 | AlchemyContext
19 | FileSystem.FileSystem
20 | Path.Path
21 | HttpClient.HttpClient
22 | ChildProcessSpawner.ChildProcessSpawner
23 | AuthProviders
24 | ProfileStore;
25
26/** What a `secrets` callback is given to decide its value. */
27export interface SecretsContext {
28 readonly stage: string;
29 readonly stack: Stack["Service"];
30 readonly alchemyContext: AlchemyContext["Service"];
31}
32
33/**
34 * A value, or a callback that picks it from the {@link SecretsContext}.
35 * Both the stack's `secrets` list and every provider's options accept this.
36 */
37export type SecretsOption<T> = T | ((context: SecretsContext) => T);
38
39/** @internal */
40export const resolveSecretsOption = <T>(
41 option: SecretsOption<T>,
42): Effect.Effect<T, never, Stage | Stack | AlchemyContext> =>
43 typeof option !== "function"
44 ? Effect.succeed(option)
45 : Effect.map(
46 Effect.all({
47 stage: Stage,
48 stack: Stack,
49 alchemyContext: AlchemyContext,
50 }),
51 option as (context: SecretsContext) => T,
52 );
53
54/**
55 * The process environment, and only that, for a secrets provider's own
56 * credentials and platform detection: `DOPPLER_TOKEN`,
57 * `INFISICAL_IDENTITY_ID`, `CI`, the OIDC variables. Nothing loaded by an
58 * earlier entry can stand in for them, and the stack's precedence is
59 * untouched, so a runner's exported variables reach the provider whatever
60 * the list says. `ALCHEMY_PROFILE` alone keeps coming from the pinned view
61 * the stack built the provider on, so `--profile` still selects the profile.
62 *
63 * @internal
64 */
65export const CredentialsConfig = ConfigProvider.layer(
66 Effect.map(ConfigProvider.ConfigProvider, (pinned) => {
67 const shell = ConfigProvider.fromEnv({ preserveEmptyStrings: true });
68 return ConfigProvider.make((path) =>
69 (path[0] === "ALCHEMY_PROFILE" ? pinned : shell).load(path),
70 );
71 }),
72);
73
74/** The layer shape every secrets provider builds: it reads the configuration assembled so far and provides the merged one. */
75export type SecretsLayer = Layer.Layer<never, unknown, SecretsServices>;
76
77/**
78 * What a `secrets` entry must satisfy. Built-in providers are
79 * `Data.TaggedClass` instances, so a `_tag` identifies the kind of provider
80 * (the stack looks for the process environment this way) and `layer` reads
81 * the configuration assembled so far and provides the merged one.
82 *
83 * ```ts
84 * class VaultProvider extends Data.TaggedClass("my-app/SecretProvider::Vault")<{
85 * readonly layer: Secrets.SecretsLayer;
86 * }> {}
87 * ```
88 */
89export interface Provider {
90 readonly _tag: string;
91 readonly layer: SecretsLayer;
92}
93
94/** A `secrets` entry: a provider, or any layer that provides a ConfigProvider. */
95export type SecretsEntry = Provider | SecretsLayer;