@konfig.ts/env 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present David Stahl-Gruber
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @konfig.ts/env
2
+
3
+ Yieldable environment entries — the shared definition layer between
4
+ konfig manifest emission and the pod's runtime Effect application.
5
+
6
+ ## Why
7
+
8
+ A pod's environment variables live in two places today:
9
+
10
+ - The konfig manifest (`secretEnv({ name: "DATABASE_URL", ... })`,
11
+ `valueEnv`, `configMapEnv`).
12
+ - The pod's runtime code (`Config.redacted("DATABASE_URL")`).
13
+
14
+ The env-var name `"DATABASE_URL"` is duplicated. This package collapses
15
+ both ends into one declaration:
16
+
17
+ ```ts
18
+ // shared/secrets.ts — imported by both sides
19
+ import { defineEnvironment, defineLiteral, defineSecret } from "@konfig.ts/env"
20
+
21
+ export const dbCreds = defineSecret({
22
+ name: "db-creds",
23
+ namespace: "prod",
24
+ env: {
25
+ url: "DATABASE_URL",
26
+ password: "DATABASE_PASSWORD"
27
+ }
28
+ })
29
+
30
+ export const port = defineLiteral({ envName: "PORT", value: 8080 })
31
+
32
+ export const apiEnv = defineEnvironment({ db: dbCreds, port })
33
+ ```
34
+
35
+ ## Entries and environments
36
+
37
+ Every `defineSecret` / `defineLiteral` / `defineDownward` call returns
38
+ an **entry** — a yieldable Effect `Config<...>` intersected with the
39
+ pure binding metadata (`name`, `namespace`, `env`/`envName`). Each
40
+ entry is its own state-management-style atom: pull it standalone, or
41
+ group it.
42
+
43
+ `defineEnvironment({...})` is a **bundle** — also a yieldable Config,
44
+ with `.members` re-exposing each named entry.
45
+
46
+ ```ts
47
+ // runtime pod code
48
+ import { Effect, Redacted } from "effect"
49
+ import { apiEnv, dbCreds } from "./secrets"
50
+
51
+ const program = Effect.gen(function*() {
52
+ const env = yield* apiEnv // { db: { url: Redacted, password: Redacted }, port: number }
53
+ const db = yield* dbCreds // { url: Redacted, password: Redacted }
54
+ const url = yield* dbCreds.fields.url // Redacted<string>
55
+ })
56
+ ```
57
+
58
+ ## Bound on the konfig side
59
+
60
+ `@konfig.ts/k8s` provides `Secret.bind({secret})` and
61
+ `Environment.bind({env})` for the manifest-side wiring. They consume
62
+ the same shared entries and produce ready-to-spread `envVars` for the
63
+ container:
64
+
65
+ ```ts
66
+ // konfig-side infra code
67
+ import { Environment, Secret, Workload } from "@konfig.ts/k8s"
68
+
69
+ const apiEnvK8s = Environment.bind({ env: apiEnv })
70
+
71
+ Workload.web({
72
+ /* ... */
73
+ deployment: {
74
+ containers: [{ name: "api", image: "...", env: apiEnvK8s.envVars }]
75
+ }
76
+ })
77
+ ```
78
+
79
+ ## Status
80
+
81
+ Phase 1 of the secret refactor — see `.docs/secret-refactoring/plan.md`.
82
+ Entries, bundles, env-var wiring only. Backends (ExternalSecrets,
83
+ SealedSecrets, Sops) and the `source` / `values` accessors land in
84
+ later phases.
85
+
86
+ ## Requirements
87
+
88
+ konfig.ts builds on [Effect](https://effect.website/), which is still in
89
+ beta. Until Effect ships a stable 4.x, you must install the exact beta
90
+ konfig is developed against:
91
+
92
+ - **`effect@4.0.0-beta.70`** — required.
93
+ - **`@effect/platform-node@4.0.0-beta.70`** — required only for `render()`
94
+ (the Node filesystem/subprocess entrypoint); manifest-only consumers can
95
+ omit it.
96
+
97
+ The peer dependency is pinned to the exact version on purpose: Effect's beta
98
+ line makes breaking changes between builds, so a looser range would surface
99
+ as `ERESOLVE` install conflicts rather than a working install. This pin will
100
+ relax to a caret range once Effect reaches a stable 4.x.
@@ -0,0 +1,2 @@
1
+ export { ChildProcess } from "effect/unstable/process";
2
+ export { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
@@ -0,0 +1,19 @@
1
+ import { Config } from "effect";
2
+ import { type EntryMarker, type HasEnvClaims } from "./entry";
3
+ export interface DownwardEntry<EnvName extends string> extends Config.Config<string>, EntryMarker<"Downward">, HasEnvClaims {
4
+ readonly envName: EnvName;
5
+ readonly fieldPath: string;
6
+ }
7
+ export interface DefineDownwardInput<EnvName extends string> {
8
+ readonly envName: EnvName;
9
+ readonly fieldPath: string;
10
+ }
11
+ export type AnyDownwardEntry = DownwardEntry<string>;
12
+ /**
13
+ * `Downward` value namespace.
14
+ *
15
+ * const podName = Downward.define({ envName: "POD_NAME", fieldPath: "metadata.name" });
16
+ */
17
+ export declare const Downward: {
18
+ define: <const EnvName extends string>(input: DefineDownwardInput<EnvName>) => DownwardEntry<EnvName>;
19
+ };
@@ -0,0 +1,27 @@
1
+ import type { Config } from "effect";
2
+ export type EntryKind = "Secret" | "Literal" | "Downward" | "Environment";
3
+ export interface EntryMarker<K extends EntryKind> {
4
+ readonly _kind: K;
5
+ }
6
+ declare const EnvNameCollision_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
7
+ readonly _tag: "EnvNameCollision";
8
+ } & Readonly<A>;
9
+ export declare class EnvNameCollision extends EnvNameCollision_base<{
10
+ readonly envName: string;
11
+ readonly claims: ReadonlyArray<string>;
12
+ }> {
13
+ get message(): string;
14
+ }
15
+ export interface EnvClaim {
16
+ readonly envName: string;
17
+ readonly label: string;
18
+ }
19
+ export interface HasEnvClaims {
20
+ readonly envClaims: ReadonlyArray<EnvClaim>;
21
+ }
22
+ export interface MakeEntryInput<C extends Config.Config<unknown>, M extends object> {
23
+ readonly config: C;
24
+ readonly metadata: M;
25
+ }
26
+ export declare const _makeEntry: <C extends Config.Config<unknown>, M extends object>(input: MakeEntryInput<C, M>) => C & M;
27
+ export {};
@@ -0,0 +1,67 @@
1
+ import { Config } from "effect";
2
+ import type { AnyDownwardEntry, DownwardEntry } from "./downward";
3
+ import { type EntryMarker, type HasEnvClaims } from "./entry";
4
+ import type { AnyLiteralEntry, LiteralEntry } from "./literal";
5
+ import type { AnySecretEntry, SecretEntry } from "./secret";
6
+ /**
7
+ * Union of every kind that can be a member of a `Environment`.
8
+ *
9
+ * Nesting: `Environment` itself is a valid `EnvMember`, so bundles can
10
+ * group related secrets / literals / downward fields into sub-records.
11
+ * The yielded record matches the nesting structure — e.g. `env.db.host`,
12
+ * `env.db.password` for a bundle `{db: Environment.define({host, password})}`.
13
+ * `Environment.bind` walks nested groups recursively when emitting
14
+ * envVars + secret manifests, and `SecretMembersOpts` / `LiteralMembersOpts`
15
+ * accept matching nested override shapes.
16
+ */
17
+ export type EnvMember = AnySecretEntry | AnyLiteralEntry | AnyDownwardEntry | AnyEnvironment;
18
+ export type MemberValue<A> = A extends Config.Config<infer T> ? T : never;
19
+ export interface Environment<M extends Readonly<Record<string, EnvMember>>> extends Config.Config<{
20
+ readonly [K in keyof M]: MemberValue<M[K]>;
21
+ }>, EntryMarker<"Environment">, HasEnvClaims {
22
+ readonly members: M;
23
+ }
24
+ export type AnyEnvironment = Environment<Readonly<Record<string, any>>>;
25
+ /**
26
+ * Compile-time envName collision check (best-effort, complements the
27
+ * runtime throw).
28
+ *
29
+ * Each member exposes a union of the envNames it claims. For every
30
+ * key K in M, we compare K's envName union against the union claimed
31
+ * by all *other* members; a non-empty intersection means a collision.
32
+ * The check stops at the top-level members of M — nested `Environment`
33
+ * members are not walked into here. The runtime `_collectClaims`
34
+ * catches every collision, including cross-nesting ones; the type
35
+ * check is just the early-warning layer.
36
+ */
37
+ type _EnvNamesOfMember<E> = E extends SecretEntry<infer _N, infer _K, infer Envs> ? Envs extends Readonly<Record<string, infer V extends string>> ? V : never : E extends LiteralEntry<infer EnvName, infer _T> ? EnvName : E extends DownwardEntry<infer EnvName> ? EnvName : never;
38
+ type _OthersEnvNames<M extends Readonly<Record<string, EnvMember>>, K extends keyof M> = {
39
+ [Other in keyof M]: Other extends K ? never : _EnvNamesOfMember<M[Other]>;
40
+ }[keyof M];
41
+ type _CollisionForKey<M extends Readonly<Record<string, EnvMember>>, K extends keyof M> = Extract<_EnvNamesOfMember<M[K]>, _OthersEnvNames<M, K>>;
42
+ type _AnyCollision<M extends Readonly<Record<string, EnvMember>>> = {
43
+ [K in keyof M]: _CollisionForKey<M, K>;
44
+ }[keyof M];
45
+ type _EnvNameCollisionError<Name extends string> = {
46
+ readonly _konfig_error: `Environment: envName "${Name}" is claimed by multiple members`;
47
+ };
48
+ type _CheckCollisions<M extends Readonly<Record<string, EnvMember>>> = [
49
+ _AnyCollision<M>
50
+ ] extends [never] ? M : _EnvNameCollisionError<Extract<_AnyCollision<M>, string>>;
51
+ /**
52
+ * `Environment` value namespace (env-contracts package).
53
+ *
54
+ * const apiEnv = Environment.define({
55
+ * db: Secret.define({ ... }),
56
+ * port: Literal.define({ ... }),
57
+ * pod: Downward.define({ ... }),
58
+ * });
59
+ *
60
+ * The k8s package re-exports this merged with its own `Environment.bind`
61
+ * / `Environment.runtime`, so importing `Environment` from `@konfig.ts/k8s`
62
+ * exposes `define` alongside the binder + runtime decoder.
63
+ */
64
+ export declare const Environment: {
65
+ define: <const M extends Readonly<Record<string, EnvMember>>>(members: M & _CheckCollisions<M>) => Environment<M>;
66
+ };
67
+ export {};
@@ -0,0 +1,8 @@
1
+ export { type AnyEnvironment, type EnvMember, Environment, type MemberValue, } from "./environment";
2
+ export { type AnyDownwardEntry, type DefineDownwardInput, Downward, type DownwardEntry, } from "./downward";
3
+ export { type AnyLiteralEntry, type DefineLiteralInput, Literal, type LiteralEntry, } from "./literal";
4
+ export { type AnySecretEntry, type DefineSecretInput, Secret, type SecretEntry, } from "./secret";
5
+ export { type EnvClaim, type EntryKind, type EntryMarker, type HasEnvClaims, EnvNameCollision, } from "./entry";
6
+ export { type FromCommandInput, type FromCommandSpec, type FromConfigInput, type LiteralInput, type ResolvedSecretValues, SecretSource, SecretSourceError, } from "./source";
7
+ export { type EnvironmentShape, environmentLayer } from "./layer";
8
+ export { runtime } from "./runtime";
package/dist/index.mjs ADDED
@@ -0,0 +1,235 @@
1
+ import { runProcessString, unsafeCoerce } from "@konfig.ts/core";
2
+ import { Config, Data, Effect, Layer, Redacted } from "effect";
3
+ import { ChildProcess } from "effect/unstable/process";
4
+ import "effect/unstable/process/ChildProcessSpawner";
5
+ //#region src/entry.ts
6
+ var EnvNameCollision = class extends Data.TaggedError("EnvNameCollision") {
7
+ get message() {
8
+ return `environment variable name "${this.envName}" is claimed by multiple entries: ${this.claims.map((c) => `"${c}"`).join(", ")}`;
9
+ }
10
+ };
11
+ const _makeEntry = (input) => unsafeCoerce(Object.assign(input.config, input.metadata), "Object.assign mutates and returns config carrying metadata's own properties, widening it to the intersection C & M");
12
+ //#endregion
13
+ //#region src/environment.ts
14
+ const _collectClaims = (members) => {
15
+ const byEnvName = /* @__PURE__ */ new Map();
16
+ const out = [];
17
+ for (const [, entry] of Object.entries(members)) for (const claim of entry.envClaims) {
18
+ const prior = byEnvName.get(claim.envName);
19
+ if (prior === void 0) byEnvName.set(claim.envName, [claim.label]);
20
+ else prior.push(claim.label);
21
+ out.push(claim);
22
+ }
23
+ for (const [envName, labels] of byEnvName) if (labels.length > 1) throw new EnvNameCollision({
24
+ envName,
25
+ claims: labels
26
+ });
27
+ return out;
28
+ };
29
+ const _define$3 = (members) => {
30
+ const envClaims = _collectClaims(members);
31
+ return _makeEntry({
32
+ config: unsafeCoerce(Config.all(members), "Config.all over the members record yields a Config of the mapped record whose values are each member's MemberValue"),
33
+ metadata: {
34
+ _kind: "Environment",
35
+ members,
36
+ envClaims
37
+ }
38
+ });
39
+ };
40
+ /**
41
+ * `Environment` value namespace (env-contracts package).
42
+ *
43
+ * const apiEnv = Environment.define({
44
+ * db: Secret.define({ ... }),
45
+ * port: Literal.define({ ... }),
46
+ * pod: Downward.define({ ... }),
47
+ * });
48
+ *
49
+ * The k8s package re-exports this merged with its own `Environment.bind`
50
+ * / `Environment.runtime`, so importing `Environment` from `@konfig.ts/k8s`
51
+ * exposes `define` alongside the binder + runtime decoder.
52
+ */
53
+ const Environment = { define: _define$3 };
54
+ //#endregion
55
+ //#region src/downward.ts
56
+ const _define$2 = (input) => {
57
+ const parser = Config.string(input.envName);
58
+ const envClaims = [{
59
+ envName: input.envName,
60
+ label: `Downward(${input.envName})`
61
+ }];
62
+ return _makeEntry({
63
+ config: parser,
64
+ metadata: {
65
+ _kind: "Downward",
66
+ envName: input.envName,
67
+ fieldPath: input.fieldPath,
68
+ envClaims
69
+ }
70
+ });
71
+ };
72
+ /**
73
+ * `Downward` value namespace.
74
+ *
75
+ * const podName = Downward.define({ envName: "POD_NAME", fieldPath: "metadata.name" });
76
+ */
77
+ const Downward = { define: _define$2 };
78
+ //#endregion
79
+ //#region src/literal.ts
80
+ const _define$1 = (input) => {
81
+ const userSerialize = input.serialize ?? ((v) => String(v));
82
+ const serialize = (value) => userSerialize(unsafeCoerce(value, "stored serialize erases its param to unknown for variance; the user-facing Literal signature guarantees the value is a T"));
83
+ const serialized = userSerialize(input.value);
84
+ const parser = input.schema !== void 0 ? input.schema : unsafeCoerce(Config.succeed(input.value), "Config.succeed of the T-typed input.value is a constant Config<T>");
85
+ const envClaims = [{
86
+ envName: input.envName,
87
+ label: `Literal(${input.envName})`
88
+ }];
89
+ return _makeEntry({
90
+ config: parser,
91
+ metadata: {
92
+ _kind: "Literal",
93
+ envName: input.envName,
94
+ value: input.value,
95
+ serialized,
96
+ serialize,
97
+ envClaims
98
+ }
99
+ });
100
+ };
101
+ /**
102
+ * `Literal` value namespace.
103
+ *
104
+ * const port = Literal.define({
105
+ * envName: "PORT", value: 8080,
106
+ * schema: Config.number("PORT").pipe(Config.withDefault(8080)),
107
+ * });
108
+ */
109
+ const Literal = { define: _define$1 };
110
+ //#endregion
111
+ //#region src/secret.ts
112
+ const _define = (input) => {
113
+ const keys = unsafeCoerce(Object.keys(input.env), "Object.keys of E returns the string keys of E, i.e. Array<keyof E & string>");
114
+ const fields = {};
115
+ for (const key of keys) fields[key] = Config.redacted(input.env[key]);
116
+ const root = unsafeCoerce(Config.all(fields), "Config.all over the per-key redacted fields yields a Config of the mapped record keyed by keyof E & string");
117
+ const envClaims = keys.map((key) => ({
118
+ envName: input.env[key],
119
+ label: `Secret(${input.name}).${key}`
120
+ }));
121
+ return _makeEntry({
122
+ config: root,
123
+ metadata: {
124
+ _kind: "Secret",
125
+ name: input.name,
126
+ namespace: input.namespace,
127
+ env: input.env,
128
+ keys,
129
+ fields: unsafeCoerce(fields, "fields record was populated for every key in keys, matching the mapped type keyed by keyof E & string"),
130
+ envClaims
131
+ }
132
+ });
133
+ };
134
+ /**
135
+ * `Secret` value namespace (env-contracts package).
136
+ *
137
+ * const dbCreds = Secret.define({
138
+ * name: "db-creds",
139
+ * namespace: "app",
140
+ * env: { url: "DATABASE_URL", password: "DATABASE_PASSWORD" },
141
+ * });
142
+ *
143
+ * The k8s package re-exports this merged with its own `Secret.make` /
144
+ * `Secret.bind`, so importing `Secret` from `@konfig.ts/k8s` exposes
145
+ * `define` alongside the manifest/binder constructors.
146
+ */
147
+ const Secret = { define: _define };
148
+ //#endregion
149
+ //#region src/source.ts
150
+ var SecretSourceError = class extends Data.TaggedError("SecretSourceError") {};
151
+ const _fromConfig = (input) => {
152
+ const envName = input.envName ?? ((k) => k);
153
+ const resolve = Effect.gen(function* () {
154
+ const out = {};
155
+ for (const key of input.keys) out[key] = yield* Config.redacted(envName(key)).pipe(Effect.mapError((cause) => new SecretSourceError({
156
+ source: "fromConfig",
157
+ key,
158
+ cause
159
+ })));
160
+ return unsafeCoerce(out, "per-key redacted record built from input.keys is the mapped type ResolvedSecretValues<K>");
161
+ });
162
+ return {
163
+ _tag: "SecretSource",
164
+ keys: input.keys,
165
+ resolve
166
+ };
167
+ };
168
+ const _literal = (input) => {
169
+ const keys = unsafeCoerce(Object.keys(input.data), "Object.keys of D returns the string keys of D, i.e. Array<keyof D & string>");
170
+ return {
171
+ _tag: "SecretSource",
172
+ keys,
173
+ resolve: Effect.sync(() => {
174
+ const out = {};
175
+ for (const k of keys) out[k] = Redacted.make(input.data[k]);
176
+ return unsafeCoerce(out, "per-key redacted record built from keys is the mapped type ResolvedSecretValues<keyof D & string>");
177
+ })
178
+ };
179
+ };
180
+ const _fromCommand = (input) => {
181
+ const resolve = Effect.gen(function* () {
182
+ const out = {};
183
+ for (const key of input.keys) {
184
+ const spec = input.run(key);
185
+ const value = (yield* runProcessString(ChildProcess.make(spec.cmd, [...spec.args]), { allowEmptyStdout: false }).pipe(Effect.mapError((cause) => new SecretSourceError({
186
+ source: "fromCommand",
187
+ key,
188
+ cause
189
+ })))).replace(/\n+$/u, "");
190
+ if (value.length === 0) return yield* Effect.fail(new SecretSourceError({
191
+ source: "fromCommand",
192
+ key,
193
+ cause: "secret command produced empty output"
194
+ }));
195
+ out[key] = Redacted.make(value);
196
+ }
197
+ return unsafeCoerce(out, "per-key redacted record built from input.keys is the mapped type ResolvedSecretValues<K>");
198
+ }).pipe(Effect.scoped);
199
+ return {
200
+ _tag: "SecretSource",
201
+ keys: input.keys,
202
+ resolve
203
+ };
204
+ };
205
+ const SecretSource = {
206
+ fromConfig: _fromConfig,
207
+ literal: _literal,
208
+ fromCommand: _fromCommand
209
+ };
210
+ //#endregion
211
+ //#region src/layer.ts
212
+ const environmentLayer = (input) => Layer.effect(input.tag, unsafeCoerce(input.env, "Environment<M> extends Config<EnvironmentShape<M>>, and Config is structurally a no-deps Effect — Layer.effect accepts it"));
213
+ //#endregion
214
+ //#region src/runtime.ts
215
+ /**
216
+ * Read every member of an `Environment<M>` from the active
217
+ * `ConfigProvider` and return the decoded record.
218
+ *
219
+ * This is the runtime half of an env contract — the same `apiEnv`
220
+ * bundle that drives `Environment.bind` in the konfig modules drives
221
+ * this decode in the running app. Add a new `Literal` /
222
+ * `Secret` to the bundle and the runtime call surfaces it
223
+ * automatically; remove one and the runtime call no longer reads it.
224
+ *
225
+ * const config = yield* Environment.runtime(apiEnv);
226
+ * const port = config.http.port;
227
+ * const password = Redacted.value(config.db.password);
228
+ */
229
+ const runtime = (env) => Effect.gen(function* () {
230
+ return unsafeCoerce(yield* env, "Environment<M> extends Config<{ [K in keyof M]: MemberValue<M[K]> }>, structurally equal to EnvironmentShape<M> by definition");
231
+ });
232
+ //#endregion
233
+ export { Downward, EnvNameCollision, Environment, Literal, Secret, SecretSource, SecretSourceError, environmentLayer, runtime };
234
+
235
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["_define","_define","_define"],"sources":["../src/entry.ts","../src/environment.ts","../src/downward.ts","../src/literal.ts","../src/secret.ts","../src/source.ts","../src/layer.ts","../src/runtime.ts"],"sourcesContent":["import { unsafeCoerce } from \"@konfig.ts/core\"\nimport type { Config } from \"effect\"\nimport { Data } from \"effect\"\n\nexport type EntryKind = \"Secret\" | \"Literal\" | \"Downward\" | \"Environment\"\n\nexport interface EntryMarker<K extends EntryKind> {\n readonly _kind: K\n}\n\nexport class EnvNameCollision extends Data.TaggedError(\"EnvNameCollision\")<{\n readonly envName: string\n readonly claims: ReadonlyArray<string>\n}> {\n get message(): string {\n return `environment variable name \"${this.envName}\" is claimed by multiple entries: ${\n this.claims\n .map((c) => `\"${c}\"`)\n .join(\", \")\n }`\n }\n}\n\nexport interface EnvClaim {\n readonly envName: string\n readonly label: string\n}\n\nexport interface HasEnvClaims {\n readonly envClaims: ReadonlyArray<EnvClaim>\n}\n\nexport interface MakeEntryInput<C extends Config.Config<unknown>, M extends object> {\n readonly config: C\n readonly metadata: M\n}\n\n// Each Secret/Literal/Downward produces a yieldable\n// Config (an \"atom\" in state-management terms) intersected with its pure\n// metadata. _makeEntry is the internal helper that does the merge.\nexport const _makeEntry = <C extends Config.Config<unknown>, M extends object>(\n input: MakeEntryInput<C, M>\n): C & M =>\n unsafeCoerce<C & M>(\n Object.assign(input.config, input.metadata),\n \"Object.assign mutates and returns config carrying metadata's own properties, widening it to the intersection C & M\"\n )\n","import { unsafeCoerce } from \"@konfig.ts/core\"\nimport { Config } from \"effect\"\nimport type { AnyDownwardEntry, DownwardEntry } from \"./downward\"\nimport { _makeEntry, type EntryMarker, type EnvClaim, EnvNameCollision, type HasEnvClaims } from \"./entry\"\nimport type { AnyLiteralEntry, LiteralEntry } from \"./literal\"\nimport type { AnySecretEntry, SecretEntry } from \"./secret\"\n\n/**\n * Union of every kind that can be a member of a `Environment`.\n *\n * Nesting: `Environment` itself is a valid `EnvMember`, so bundles can\n * group related secrets / literals / downward fields into sub-records.\n * The yielded record matches the nesting structure — e.g. `env.db.host`,\n * `env.db.password` for a bundle `{db: Environment.define({host, password})}`.\n * `Environment.bind` walks nested groups recursively when emitting\n * envVars + secret manifests, and `SecretMembersOpts` / `LiteralMembersOpts`\n * accept matching nested override shapes.\n */\nexport type EnvMember =\n | AnySecretEntry\n | AnyLiteralEntry\n | AnyDownwardEntry\n | AnyEnvironment\n\nexport type MemberValue<A> = A extends Config.Config<infer T> ? T : never\n\nexport interface Environment<M extends Readonly<Record<string, EnvMember>>>\n extends Config.Config<{ readonly [K in keyof M]: MemberValue<M[K]> }>, EntryMarker<\"Environment\">, HasEnvClaims\n{\n readonly members: M\n}\n\n// oxlint-disable-next-line app/no-explicit-any\nexport type AnyEnvironment = Environment<Readonly<Record<string, any>>>\n\nconst _collectClaims = (\n members: Readonly<Record<string, EnvMember>>\n): ReadonlyArray<EnvClaim> => {\n const byEnvName = new Map<string, string[]>()\n const out: EnvClaim[] = []\n for (const [, entry] of Object.entries(members)) {\n for (const claim of entry.envClaims) {\n const prior = byEnvName.get(claim.envName)\n if (prior === undefined) {\n byEnvName.set(claim.envName, [claim.label])\n } else {\n prior.push(claim.label)\n }\n out.push(claim)\n }\n }\n for (const [envName, labels] of byEnvName) {\n if (labels.length > 1) {\n throw new EnvNameCollision({ envName, claims: labels })\n }\n }\n return out\n}\n\n/**\n * Compile-time envName collision check (best-effort, complements the\n * runtime throw).\n *\n * Each member exposes a union of the envNames it claims. For every\n * key K in M, we compare K's envName union against the union claimed\n * by all *other* members; a non-empty intersection means a collision.\n * The check stops at the top-level members of M — nested `Environment`\n * members are not walked into here. The runtime `_collectClaims`\n * catches every collision, including cross-nesting ones; the type\n * check is just the early-warning layer.\n */\ntype _EnvNamesOfMember<E> = E extends SecretEntry<\n infer _N,\n infer _K,\n infer Envs\n> ? Envs extends Readonly<Record<string, infer V extends string>> ? V\n : never\n : E extends LiteralEntry<infer EnvName, infer _T> ? EnvName\n : E extends DownwardEntry<infer EnvName> ? EnvName\n : never\n\ntype _OthersEnvNames<\n M extends Readonly<Record<string, EnvMember>>,\n K extends keyof M\n> = {\n [Other in keyof M]: Other extends K ? never : _EnvNamesOfMember<M[Other]>\n}[keyof M]\n\ntype _CollisionForKey<\n M extends Readonly<Record<string, EnvMember>>,\n K extends keyof M\n> = Extract<_EnvNamesOfMember<M[K]>, _OthersEnvNames<M, K>>\n\ntype _AnyCollision<M extends Readonly<Record<string, EnvMember>>> = {\n [K in keyof M]: _CollisionForKey<M, K>\n}[keyof M]\n\ntype _EnvNameCollisionError<Name extends string> = {\n readonly _konfig_error: `Environment: envName \"${Name}\" is claimed by multiple members`\n}\n\ntype _CheckCollisions<M extends Readonly<Record<string, EnvMember>>> = [\n _AnyCollision<M>\n] extends [never] ? M\n : _EnvNameCollisionError<Extract<_AnyCollision<M>, string>>\n\nconst _define = <const M extends Readonly<Record<string, EnvMember>>>(\n members: M & _CheckCollisions<M>\n): Environment<M> => {\n const envClaims = _collectClaims(members)\n\n const root = unsafeCoerce<\n Config.Config<\n {\n readonly [K in keyof M]: MemberValue<M[K]>\n }\n >\n >(\n Config.all(members),\n \"Config.all over the members record yields a Config of the mapped record whose values are each member's MemberValue\"\n )\n\n return _makeEntry({\n config: root,\n metadata: {\n _kind: \"Environment\" as const,\n members,\n envClaims\n }\n })\n}\n\n/**\n * `Environment` value namespace (env-contracts package).\n *\n * const apiEnv = Environment.define({\n * db: Secret.define({ ... }),\n * port: Literal.define({ ... }),\n * pod: Downward.define({ ... }),\n * });\n *\n * The k8s package re-exports this merged with its own `Environment.bind`\n * / `Environment.runtime`, so importing `Environment` from `@konfig.ts/k8s`\n * exposes `define` alongside the binder + runtime decoder.\n */\nexport const Environment = {\n define: _define\n}\n","import { Config } from \"effect\";\nimport {\n\ttype EnvClaim,\n\ttype EntryMarker,\n\ttype HasEnvClaims,\n\t_makeEntry,\n} from \"./entry\";\n\nexport interface DownwardEntry<EnvName extends string>\n\textends Config.Config<string>,\n\t\tEntryMarker<\"Downward\">,\n\t\tHasEnvClaims {\n\treadonly envName: EnvName;\n\treadonly fieldPath: string;\n}\n\nexport interface DefineDownwardInput<EnvName extends string> {\n\treadonly envName: EnvName;\n\treadonly fieldPath: string;\n}\n\nconst _define = <const EnvName extends string>(\n\tinput: DefineDownwardInput<EnvName>,\n): DownwardEntry<EnvName> => {\n\tconst parser = Config.string(input.envName);\n\n\tconst envClaims: ReadonlyArray<EnvClaim> = [\n\t\t{ envName: input.envName, label: `Downward(${input.envName})` },\n\t];\n\n\treturn _makeEntry({\n\t\tconfig: parser,\n\t\tmetadata: {\n\t\t\t_kind: \"Downward\" as const,\n\t\t\tenvName: input.envName,\n\t\t\tfieldPath: input.fieldPath,\n\t\t\tenvClaims,\n\t\t},\n\t});\n};\n\nexport type AnyDownwardEntry = DownwardEntry<string>;\n\n/**\n * `Downward` value namespace.\n *\n * const podName = Downward.define({ envName: \"POD_NAME\", fieldPath: \"metadata.name\" });\n */\nexport const Downward = {\n\tdefine: _define,\n};\n","import { unsafeCoerce } from \"@konfig.ts/core\"\nimport { Config } from \"effect\"\nimport { _makeEntry, type EntryMarker, type EnvClaim, type HasEnvClaims } from \"./entry\"\n\nexport interface LiteralEntry<EnvName extends string, T>\n extends Config.Config<T>, EntryMarker<\"Literal\">, HasEnvClaims\n{\n readonly envName: EnvName\n readonly value: T\n readonly serialized: string\n /**\n * Stored so bind-time value overrides can re-serialize with the same\n * fn. Typed as `(unknown) => string` so that `LiteralEntry<…, T>`\n * stays assignable to `LiteralEntry<string, unknown>` in the\n * `EnvMember` union (function params are contravariant — a more\n * specific T would block the narrowing). The override site supplies\n * a value typed against `T` via `LiteralMembersOpts<M>`.\n */\n readonly serialize: (value: unknown) => string\n}\n\nexport interface DefineLiteralInput<EnvName extends string, T> {\n readonly envName: EnvName\n readonly value: T\n readonly schema?: Config.Config<T>\n readonly serialize?: (value: T) => string\n}\n\nconst _define = <const EnvName extends string, T = string>(\n input: DefineLiteralInput<EnvName, T>\n): LiteralEntry<EnvName, T> => {\n const userSerialize = input.serialize ?? ((v: T) => String(v))\n // Erase the parameter type to `unknown` for the stored function — see\n // the LiteralEntry doc for the variance rationale. Literal's\n // own type signature still enforces `T` at the user-facing call site.\n const serialize = (value: unknown): string =>\n userSerialize(\n unsafeCoerce<T>(\n value,\n \"stored serialize erases its param to unknown for variance; the user-facing Literal signature guarantees the value is a T\"\n )\n )\n const serialized = userSerialize(input.value)\n\n const parser = input.schema !== undefined\n ? input.schema\n : unsafeCoerce<Config.Config<T>>(\n Config.succeed(input.value),\n \"Config.succeed of the T-typed input.value is a constant Config<T>\"\n )\n\n const envClaims: ReadonlyArray<EnvClaim> = [\n { envName: input.envName, label: `Literal(${input.envName})` }\n ]\n\n return _makeEntry({\n config: parser,\n metadata: {\n _kind: \"Literal\" as const,\n envName: input.envName,\n value: input.value,\n serialized,\n serialize,\n envClaims\n }\n })\n}\n\nexport type AnyLiteralEntry = LiteralEntry<string, unknown>\n\n/**\n * `Literal` value namespace.\n *\n * const port = Literal.define({\n * envName: \"PORT\", value: 8080,\n * schema: Config.number(\"PORT\").pipe(Config.withDefault(8080)),\n * });\n */\nexport const Literal = {\n define: _define\n}\n","import { unsafeCoerce } from \"@konfig.ts/core\"\nimport { Config, type Redacted } from \"effect\"\nimport { _makeEntry, type EntryMarker, type EnvClaim, type HasEnvClaims } from \"./entry\"\n\nexport interface SecretEntry<\n N extends string,\n K extends string,\n E extends Readonly<Record<K, string>>\n> extends Config.Config<{ readonly [P in K]: Redacted.Redacted<string> }>, EntryMarker<\"Secret\">, HasEnvClaims {\n readonly name: N\n readonly namespace: string\n readonly env: E\n readonly keys: ReadonlyArray<K>\n readonly fields: { readonly [P in K]: Config.Config<Redacted.Redacted<string>> }\n}\n\nexport interface DefineSecretInput<\n N extends string,\n E extends Readonly<Record<string, string>>\n> {\n readonly name: N\n readonly namespace: string\n readonly env: E\n}\n\nconst _define = <\n const N extends string,\n const E extends Readonly<Record<string, string>>\n>(\n input: DefineSecretInput<N, E>\n): SecretEntry<N, keyof E & string, E> => {\n const keys = unsafeCoerce<Array<keyof E & string>>(\n Object.keys(input.env),\n \"Object.keys of E returns the string keys of E, i.e. Array<keyof E & string>\"\n )\n\n const fields: Record<string, Config.Config<Redacted.Redacted<string>>> = {}\n for (const key of keys) {\n fields[key] = Config.redacted(input.env[key])\n }\n\n const root = unsafeCoerce<\n Config.Config<\n {\n readonly [P in keyof E & string]: Redacted.Redacted<string>\n }\n >\n >(\n Config.all(fields),\n \"Config.all over the per-key redacted fields yields a Config of the mapped record keyed by keyof E & string\"\n )\n\n const envClaims: ReadonlyArray<EnvClaim> = keys.map((key) => ({\n envName: input.env[key],\n label: `Secret(${input.name}).${key}`\n }))\n\n return _makeEntry({\n config: root,\n metadata: {\n _kind: \"Secret\" as const,\n name: input.name,\n namespace: input.namespace,\n env: input.env,\n keys,\n fields: unsafeCoerce<\n {\n readonly [P in keyof E & string]: Config.Config<Redacted.Redacted<string>>\n }\n >(\n fields,\n \"fields record was populated for every key in keys, matching the mapped type keyed by keyof E & string\"\n ),\n envClaims\n }\n })\n}\n\nexport type AnySecretEntry = SecretEntry<string, string, Readonly<Record<string, string>>>\n\n/**\n * `Secret` value namespace (env-contracts package).\n *\n * const dbCreds = Secret.define({\n * name: \"db-creds\",\n * namespace: \"app\",\n * env: { url: \"DATABASE_URL\", password: \"DATABASE_PASSWORD\" },\n * });\n *\n * The k8s package re-exports this merged with its own `Secret.make` /\n * `Secret.bind`, so importing `Secret` from `@konfig.ts/k8s` exposes\n * `define` alongside the manifest/binder constructors.\n */\nexport const Secret = {\n define: _define\n}\n","import { runProcessString, unsafeCoerce } from \"@konfig.ts/core\"\nimport { Config, Data, Effect, Redacted, type Scope } from \"effect\"\nimport { ChildProcess, ChildProcessSpawner } from \"./_unstable\"\n\nexport class SecretSourceError extends Data.TaggedError(\"SecretSourceError\")<{\n readonly source: string\n readonly key: string\n readonly cause: unknown\n}> {}\n\nexport type ResolvedSecretValues<K extends string> = {\n readonly [P in K]: Redacted.Redacted<string>\n}\n\nexport interface SecretSource<K extends string, R = never> {\n readonly _tag: \"SecretSource\"\n readonly keys: ReadonlyArray<K>\n readonly resolve: Effect.Effect<ResolvedSecretValues<K>, SecretSourceError, R>\n}\n\nexport interface FromConfigInput<K extends string> {\n readonly keys: ReadonlyArray<K>\n readonly envName?: (key: K) => string\n}\n\nconst _fromConfig = <const K extends string>(input: FromConfigInput<K>): SecretSource<K> => {\n const envName = input.envName ?? ((k: K) => k)\n const resolve = Effect.gen(function*() {\n const out: Record<string, Redacted.Redacted<string>> = {}\n for (const key of input.keys) {\n const v = yield* Config.redacted(envName(key)).pipe(\n Effect.mapError(\n (cause) => new SecretSourceError({ source: \"fromConfig\", key, cause })\n )\n )\n out[key] = v\n }\n return unsafeCoerce<ResolvedSecretValues<K>>(\n out,\n \"per-key redacted record built from input.keys is the mapped type ResolvedSecretValues<K>\"\n )\n })\n return { _tag: \"SecretSource\", keys: input.keys, resolve }\n}\n\nexport interface LiteralInput<D extends Readonly<Record<string, string>>> {\n readonly data: D\n}\n\nconst _literal = <const D extends Readonly<Record<string, string>>>(\n input: LiteralInput<D>\n): SecretSource<keyof D & string> => {\n const keys = unsafeCoerce<Array<keyof D & string>>(\n Object.keys(input.data),\n \"Object.keys of D returns the string keys of D, i.e. Array<keyof D & string>\"\n )\n const resolve = Effect.sync(() => {\n const out: Record<string, Redacted.Redacted<string>> = {}\n for (const k of keys) {\n out[k] = Redacted.make(input.data[k])\n }\n return unsafeCoerce<ResolvedSecretValues<keyof D & string>>(\n out,\n \"per-key redacted record built from keys is the mapped type ResolvedSecretValues<keyof D & string>\"\n )\n })\n return { _tag: \"SecretSource\", keys, resolve }\n}\n\nexport interface FromCommandSpec {\n readonly cmd: string\n readonly args: ReadonlyArray<string>\n}\n\nexport interface FromCommandInput<K extends string> {\n readonly keys: ReadonlyArray<K>\n readonly run: (key: K) => FromCommandSpec\n}\n\nconst _fromCommand = <const K extends string>(\n input: FromCommandInput<K>\n): SecretSource<K, ChildProcessSpawner | Scope.Scope> => {\n const resolve = Effect.gen(function*() {\n const out: Record<string, Redacted.Redacted<string>> = {}\n for (const key of input.keys) {\n const spec = input.run(key)\n const proc = ChildProcess.make(spec.cmd, [...spec.args])\n const stdout = yield* runProcessString(proc, { allowEmptyStdout: false }).pipe(\n Effect.mapError(\n (cause) => new SecretSourceError({ source: \"fromCommand\", key, cause })\n )\n )\n const value = stdout.replace(/\\n+$/u, \"\")\n if (value.length === 0) {\n return yield* Effect.fail(\n new SecretSourceError({\n source: \"fromCommand\",\n key,\n cause: \"secret command produced empty output\"\n })\n )\n }\n out[key] = Redacted.make(value)\n }\n return unsafeCoerce<ResolvedSecretValues<K>>(\n out,\n \"per-key redacted record built from input.keys is the mapped type ResolvedSecretValues<K>\"\n )\n }).pipe(Effect.scoped)\n return { _tag: \"SecretSource\", keys: input.keys, resolve }\n}\n\nexport const SecretSource = {\n fromConfig: _fromConfig,\n literal: _literal,\n fromCommand: _fromCommand\n}\n","import type { Context, Effect } from \"effect\";\nimport { Layer } from \"effect\";\nimport type { EnvMember, Environment, MemberValue } from \"./environment\";\n\n/**\n * Type of the value an `Environment<M>` yields when consumed — the same\n * record type produced by `yield* env`.\n */\nexport type EnvironmentShape<M extends Readonly<Record<string, EnvMember>>> = {\n\treadonly [K in keyof M]: MemberValue<M[K]>;\n};\n\nimport { unsafeCoerce } from \"@konfig.ts/core\";\n\n/**\n * Lift an `Environment` bundle into a service `Layer` so the app's\n * graph can depend on the yielded record once and DI it everywhere.\n *\n * The bundle is a yieldable `Config`; lifting it into a `Layer.effect`\n * bound to a `Context.Service` tag means the bundle is resolved once at\n * Layer construction time, and every downstream service that yields the\n * tag reads from the resolved record — no `Effect.runSync` at module\n * scope, no per-service `Config.string` reads.\n *\n * class AppEnv extends Context.Service<AppEnv, EnvironmentShape<typeof bundleEnv.members>>()(\"AppEnv\") {}\n * export const AppEnvLive = environmentLayer({ tag: AppEnv, env: bundleEnv });\n *\n * // downstream:\n * const env = yield* AppEnv;\n * const password = Redacted.value(env.postgres.password);\n */\nexport interface EnvironmentLayerInput<Self, M extends Readonly<Record<string, EnvMember>>> {\n\treadonly tag: Context.Service<Self, EnvironmentShape<M>>;\n\treadonly env: Environment<M>;\n}\n\nexport const environmentLayer = <Self, M extends Readonly<Record<string, EnvMember>>>(\n\tinput: EnvironmentLayerInput<Self, M>,\n): Layer.Layer<Self> =>\n\tLayer.effect(\n\t\tinput.tag,\n\t\tunsafeCoerce<Effect.Effect<EnvironmentShape<M>>>(\n\t\t\tinput.env,\n\t\t\t\"Environment<M> extends Config<EnvironmentShape<M>>, and Config is structurally a no-deps Effect — Layer.effect accepts it\",\n\t\t),\n\t);\n","import { unsafeCoerce } from \"@konfig.ts/core\";\nimport { type Config, Effect } from \"effect\";\nimport type { EnvMember, Environment } from \"./environment\";\nimport type { EnvironmentShape } from \"./layer\";\n\n/**\n * Read every member of an `Environment<M>` from the active\n * `ConfigProvider` and return the decoded record.\n *\n * This is the runtime half of an env contract — the same `apiEnv`\n * bundle that drives `Environment.bind` in the konfig modules drives\n * this decode in the running app. Add a new `Literal` /\n * `Secret` to the bundle and the runtime call surfaces it\n * automatically; remove one and the runtime call no longer reads it.\n *\n * const config = yield* Environment.runtime(apiEnv);\n * const port = config.http.port;\n * const password = Redacted.value(config.db.password);\n */\nexport const runtime = <M extends Readonly<Record<string, EnvMember>>>(\n\tenv: Environment<M>,\n): Effect.Effect<EnvironmentShape<M>, Config.ConfigError> =>\n\tEffect.gen(function* () {\n\t\tconst decoded = yield* env;\n\t\treturn unsafeCoerce<EnvironmentShape<M>>(\n\t\t\tdecoded,\n\t\t\t\"Environment<M> extends Config<{ [K in keyof M]: MemberValue<M[K]> }>, structurally equal to EnvironmentShape<M> by definition\",\n\t\t);\n\t});\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { Context } from "effect";
2
+ import { Layer } from "effect";
3
+ import type { EnvMember, Environment, MemberValue } from "./environment";
4
+ /**
5
+ * Type of the value an `Environment<M>` yields when consumed — the same
6
+ * record type produced by `yield* env`.
7
+ */
8
+ export type EnvironmentShape<M extends Readonly<Record<string, EnvMember>>> = {
9
+ readonly [K in keyof M]: MemberValue<M[K]>;
10
+ };
11
+ /**
12
+ * Lift an `Environment` bundle into a service `Layer` so the app's
13
+ * graph can depend on the yielded record once and DI it everywhere.
14
+ *
15
+ * The bundle is a yieldable `Config`; lifting it into a `Layer.effect`
16
+ * bound to a `Context.Service` tag means the bundle is resolved once at
17
+ * Layer construction time, and every downstream service that yields the
18
+ * tag reads from the resolved record — no `Effect.runSync` at module
19
+ * scope, no per-service `Config.string` reads.
20
+ *
21
+ * class AppEnv extends Context.Service<AppEnv, EnvironmentShape<typeof bundleEnv.members>>()("AppEnv") {}
22
+ * export const AppEnvLive = environmentLayer({ tag: AppEnv, env: bundleEnv });
23
+ *
24
+ * // downstream:
25
+ * const env = yield* AppEnv;
26
+ * const password = Redacted.value(env.postgres.password);
27
+ */
28
+ export interface EnvironmentLayerInput<Self, M extends Readonly<Record<string, EnvMember>>> {
29
+ readonly tag: Context.Service<Self, EnvironmentShape<M>>;
30
+ readonly env: Environment<M>;
31
+ }
32
+ export declare const environmentLayer: <Self, M extends Readonly<Record<string, EnvMember>>>(input: EnvironmentLayerInput<Self, M>) => Layer.Layer<Self>;
@@ -0,0 +1,34 @@
1
+ import { Config } from "effect";
2
+ import { type EntryMarker, type HasEnvClaims } from "./entry";
3
+ export interface LiteralEntry<EnvName extends string, T> extends Config.Config<T>, EntryMarker<"Literal">, HasEnvClaims {
4
+ readonly envName: EnvName;
5
+ readonly value: T;
6
+ readonly serialized: string;
7
+ /**
8
+ * Stored so bind-time value overrides can re-serialize with the same
9
+ * fn. Typed as `(unknown) => string` so that `LiteralEntry<…, T>`
10
+ * stays assignable to `LiteralEntry<string, unknown>` in the
11
+ * `EnvMember` union (function params are contravariant — a more
12
+ * specific T would block the narrowing). The override site supplies
13
+ * a value typed against `T` via `LiteralMembersOpts<M>`.
14
+ */
15
+ readonly serialize: (value: unknown) => string;
16
+ }
17
+ export interface DefineLiteralInput<EnvName extends string, T> {
18
+ readonly envName: EnvName;
19
+ readonly value: T;
20
+ readonly schema?: Config.Config<T>;
21
+ readonly serialize?: (value: T) => string;
22
+ }
23
+ export type AnyLiteralEntry = LiteralEntry<string, unknown>;
24
+ /**
25
+ * `Literal` value namespace.
26
+ *
27
+ * const port = Literal.define({
28
+ * envName: "PORT", value: 8080,
29
+ * schema: Config.number("PORT").pipe(Config.withDefault(8080)),
30
+ * });
31
+ */
32
+ export declare const Literal: {
33
+ define: <const EnvName extends string, T = string>(input: DefineLiteralInput<EnvName, T>) => LiteralEntry<EnvName, T>;
34
+ };
@@ -0,0 +1,18 @@
1
+ import { type Config, Effect } from "effect";
2
+ import type { EnvMember, Environment } from "./environment";
3
+ import type { EnvironmentShape } from "./layer";
4
+ /**
5
+ * Read every member of an `Environment<M>` from the active
6
+ * `ConfigProvider` and return the decoded record.
7
+ *
8
+ * This is the runtime half of an env contract — the same `apiEnv`
9
+ * bundle that drives `Environment.bind` in the konfig modules drives
10
+ * this decode in the running app. Add a new `Literal` /
11
+ * `Secret` to the bundle and the runtime call surfaces it
12
+ * automatically; remove one and the runtime call no longer reads it.
13
+ *
14
+ * const config = yield* Environment.runtime(apiEnv);
15
+ * const port = config.http.port;
16
+ * const password = Redacted.value(config.db.password);
17
+ */
18
+ export declare const runtime: <M extends Readonly<Record<string, EnvMember>>>(env: Environment<M>) => Effect.Effect<EnvironmentShape<M>, Config.ConfigError>;
@@ -0,0 +1,35 @@
1
+ import { Config, type Redacted } from "effect";
2
+ import { type EntryMarker, type HasEnvClaims } from "./entry";
3
+ export interface SecretEntry<N extends string, K extends string, E extends Readonly<Record<K, string>>> extends Config.Config<{
4
+ readonly [P in K]: Redacted.Redacted<string>;
5
+ }>, EntryMarker<"Secret">, HasEnvClaims {
6
+ readonly name: N;
7
+ readonly namespace: string;
8
+ readonly env: E;
9
+ readonly keys: ReadonlyArray<K>;
10
+ readonly fields: {
11
+ readonly [P in K]: Config.Config<Redacted.Redacted<string>>;
12
+ };
13
+ }
14
+ export interface DefineSecretInput<N extends string, E extends Readonly<Record<string, string>>> {
15
+ readonly name: N;
16
+ readonly namespace: string;
17
+ readonly env: E;
18
+ }
19
+ export type AnySecretEntry = SecretEntry<string, string, Readonly<Record<string, string>>>;
20
+ /**
21
+ * `Secret` value namespace (env-contracts package).
22
+ *
23
+ * const dbCreds = Secret.define({
24
+ * name: "db-creds",
25
+ * namespace: "app",
26
+ * env: { url: "DATABASE_URL", password: "DATABASE_PASSWORD" },
27
+ * });
28
+ *
29
+ * The k8s package re-exports this merged with its own `Secret.make` /
30
+ * `Secret.bind`, so importing `Secret` from `@konfig.ts/k8s` exposes
31
+ * `define` alongside the manifest/binder constructors.
32
+ */
33
+ export declare const Secret: {
34
+ define: <const N extends string, const E extends Readonly<Record<string, string>>>(input: DefineSecretInput<N, E>) => SecretEntry<N, keyof E & string, E>;
35
+ };
@@ -0,0 +1,40 @@
1
+ import { Effect, Redacted, type Scope } from "effect";
2
+ import { ChildProcessSpawner } from "./_unstable";
3
+ declare const SecretSourceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
4
+ readonly _tag: "SecretSourceError";
5
+ } & Readonly<A>;
6
+ export declare class SecretSourceError extends SecretSourceError_base<{
7
+ readonly source: string;
8
+ readonly key: string;
9
+ readonly cause: unknown;
10
+ }> {
11
+ }
12
+ export type ResolvedSecretValues<K extends string> = {
13
+ readonly [P in K]: Redacted.Redacted<string>;
14
+ };
15
+ export interface SecretSource<K extends string, R = never> {
16
+ readonly _tag: "SecretSource";
17
+ readonly keys: ReadonlyArray<K>;
18
+ readonly resolve: Effect.Effect<ResolvedSecretValues<K>, SecretSourceError, R>;
19
+ }
20
+ export interface FromConfigInput<K extends string> {
21
+ readonly keys: ReadonlyArray<K>;
22
+ readonly envName?: (key: K) => string;
23
+ }
24
+ export interface LiteralInput<D extends Readonly<Record<string, string>>> {
25
+ readonly data: D;
26
+ }
27
+ export interface FromCommandSpec {
28
+ readonly cmd: string;
29
+ readonly args: ReadonlyArray<string>;
30
+ }
31
+ export interface FromCommandInput<K extends string> {
32
+ readonly keys: ReadonlyArray<K>;
33
+ readonly run: (key: K) => FromCommandSpec;
34
+ }
35
+ export declare const SecretSource: {
36
+ fromConfig: <const K extends string>(input: FromConfigInput<K>) => SecretSource<K>;
37
+ literal: <const D extends Readonly<Record<string, string>>>(input: LiteralInput<D>) => SecretSource<keyof D & string>;
38
+ fromCommand: <const K extends string>(input: FromCommandInput<K>) => SecretSource<K, ChildProcessSpawner | Scope.Scope>;
39
+ };
40
+ export {};
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@konfig.ts/env",
3
+ "version": "0.0.1",
4
+ "description": "Yieldable environment atoms (defineSecret, defineLiteral, defineDownward, defineEnvironment) shared between konfig manifest emission and the runtime pod application.",
5
+ "license": "MIT",
6
+ "author": "David Stahl-Gruber",
7
+ "homepage": "https://github.com/DwieDave/konfig.ts#readme",
8
+ "bugs": "https://github.com/DwieDave/konfig.ts/issues",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/DwieDave/konfig.ts.git",
12
+ "directory": "packages/env"
13
+ },
14
+ "keywords": [
15
+ "kubernetes",
16
+ "k8s",
17
+ "config",
18
+ "secret",
19
+ "effect",
20
+ "typescript"
21
+ ],
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "main": "./dist/index.mjs",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.mjs"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "tsdown ./src/index.ts --format=esm --target=node20 --sourcemap --clean --no-dts && tsc -p tsconfig.build.json",
45
+ "check": "tsc -p tsconfig.check.json --noEmit",
46
+ "test": "vitest run",
47
+ "lint": "oxlint",
48
+ "lint:apply": "oxlint --fix",
49
+ "prepublishOnly": "bun run build",
50
+ "prepack": "node ../../scripts/prepack-exports.mjs strip",
51
+ "postpack": "node ../../scripts/prepack-exports.mjs restore"
52
+ },
53
+ "dependencies": {
54
+ "@konfig.ts/core": "0.0.1"
55
+ },
56
+ "peerDependencies": {
57
+ "effect": "4.0.0-beta.70"
58
+ },
59
+ "devDependencies": {
60
+ "@effect/vitest": "catalog:",
61
+ "@types/bun": "catalog:",
62
+ "effect": "catalog:",
63
+ "fast-check": "catalog:",
64
+ "typescript": "catalog:",
65
+ "vitest": "catalog:"
66
+ }
67
+ }