1import * as ConfigProvider from "effect/ConfigProvider";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { envKeys } from "./Log.ts";
5import {
6 resolveSecretsOption,
7 type SecretsLayer,
8 type SecretsOption,
9} from "./Provider.ts";
10
11export interface ProcessEnvOptions {
12 /**
13 * Leave the process environment out of this stack entirely. The entry
14 * stays in the list but contributes nothing.
15 * @default false
16 */
17 disabled?: boolean;
18}
19
20/** A `secrets` entry that loads the process environment. */
21export class ProcessEnvProvider extends Data.TaggedClass(
22 "alchemy/SecretProvider::ProcessEnv",
23)<{ readonly layer: SecretsLayer }> {}
24
25/**
26 * The process environment as a secrets provider.
27 *
28 * It is implicit: every stack ends its `secrets` list with the shell, so a
29 * variable exported in the terminal or by CI overrides every provider. List
30 * it yourself to put it elsewhere, typically first so it becomes a fallback
31 * that a secrets manager overrides, or pass `disabled: true` to leave it out:
32 *
33 * ```ts
34 * secrets: [Secrets.ProcessEnv(), Doppler.Secrets({ project: "app", config: "prd" })]
35 * secrets: [Doppler.Secrets({ project: "app", config: "prd" }), Secrets.ProcessEnv({ disabled: true })]
36 * ```
37 */
38export const ProcessEnv = (options: SecretsOption<ProcessEnvOptions> = {}) =>
39 new ProcessEnvProvider({
40 layer: ConfigProvider.layerAdd(
41 Effect.gen(function* () {
42 const resolved = yield* resolveSecretsOption(options);
43 if (resolved.disabled) return ConfigProvider.fromEnv({ env: {} });
44 const environment = ConfigProvider.fromEnv({
45 preserveEmptyStrings: true,
46 });
47 // A shell has far too many variables to list; the count is enough.
48 const keys = yield* envKeys(environment);
49 yield* Effect.logDebug(
50 `Loaded ${keys.length} secrets from the process environment`,
51 );
52 return environment;
53 }),
54 { asPrimary: true },
55 ),
56 });