@konfig.ts/k8s 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,154 @@
1
+ # @konfig.ts/k8s
2
+
3
+ Kubernetes resource constructors with branded references on top of
4
+ `@konfig.ts/core`'s `Manifest<A>` carrier. The brands are where the
5
+ package earns its keep: cross-resource invariants (env-var → Secret,
6
+ volume → ConfigMap, Ingress TLS → Secret) are checked at the type level,
7
+ and the `Dep.*` tracking propagates needs/provides through Effect
8
+ `Layer`s.
9
+
10
+ ## Branded references
11
+
12
+ ```ts
13
+ import { ConfigMapRef, SecretRef, ServiceAccountRef } from "@konfig.ts/k8s"
14
+
15
+ const apiCreds = SecretRef.of("api-creds") // SecretRef<"api-creds">
16
+ const cfg = ConfigMapRef.of("oauth-templates") // ConfigMapRef<"oauth-templates">
17
+ const sa = ServiceAccountRef.of("api") // ServiceAccountRef<"api">
18
+ ```
19
+
20
+ Each ref carries the resource _name_ in its type parameter. The
21
+ enforcement points (env var `secretKeyRef` / `configMapKeyRef`, volume
22
+ secret, volume configMap, `imagePullSecret`, Ingress TLS) all accept the
23
+ branded type and reject raw strings.
24
+
25
+ ## Identity constructors expose `.ref`
26
+
27
+ ```ts
28
+ const apiSecret = Secret.make({ name: "api-creds", namespace: "prod", stringData: {...} });
29
+ // apiSecret.ref is a SecretRef<"api-creds">
30
+ ```
31
+
32
+ The `.ref` accessor means consumers wire the brand through without
33
+ restating the name:
34
+
35
+ ```ts
36
+ const env = secretEnv("DATABASE_URL", { ref: apiSecret.ref, key: "url" })
37
+ ```
38
+
39
+ ## Env-var helpers
40
+
41
+ | Helper | Rejects raw string for |
42
+ | ------------------------------------------- | ---------------------- |
43
+ | `valueEnv(name, value)` | n/a |
44
+ | `secretEnv(name, {ref, key, optional?})` | `ref` |
45
+ | `configMapEnv(name, {ref, key, optional?})` | `ref` |
46
+ | `rawEnv({name, value?, valueFrom?})` | n/a (escape hatch) |
47
+
48
+ ## Volume helpers
49
+
50
+ | Helper |
51
+ | ----------------------------------- |
52
+ | `volumeFromSecret(name, ref)` |
53
+ | `volumeFromConfigMap(name, ref)` |
54
+ | `emptyDirVolume(name, opts?)` |
55
+ | `pvcVolume(name, claimName, opts?)` |
56
+
57
+ ## Constructors
58
+
59
+ Workload-tier (`Deployment.make`, `StatefulSet.make`, `Job.make`,
60
+ `CronJob.make`) accept typed containers, `Volume`s,
61
+ `imagePullSecrets: { name: SecretRef<N> }[]`, and
62
+ `serviceAccountName: ServiceAccountRef | string`.
63
+
64
+ Network-tier (`Service.make`, `Ingress.make`). Ingress TLS uses the
65
+ `ingressTLS(secretName, hosts?)` helper for branded refs.
66
+
67
+ Identity-tier (`Namespace.make`, `ServiceAccount.make`, `ConfigMap.make`,
68
+ `Secret.make`) return constructors whose returned record carries the
69
+ typed `.ref`.
70
+
71
+ Policy-tier (`PersistentVolume.make`, `PersistentVolumeClaim.make`,
72
+ `NetworkPolicy.make`, `ClusterRole.make`, `ClusterRoleBinding.make`,
73
+ `Role.make`, `RoleBinding.make`) — simple constructors with no extra
74
+ dependencies.
75
+
76
+ ## Higher-level: `Workload.web` and `Workload.cron`
77
+
78
+ Built for the workload shapes consumers need today — not speculative.
79
+
80
+ `Workload.web(...)` composes Deployment + Service + (optional) Ingress
81
+ with derived labels (`app: <name>`). `Workload.cron(...)` composes
82
+ CronJob + ServiceAccount (the SA is private to the cron).
83
+
84
+ ## Secret rotation — build-time hash vs runtime Reloader
85
+
86
+ konfig provides two complementary stories for restarting pods when a
87
+ Secret or ConfigMap they consume changes:
88
+
89
+ - **Build time** (`hashSecretValues` / `pod-hash` annotation). A SHA of
90
+ the secret material lives on the pod spec; re-rendering after a
91
+ rotation produces a new hash, so the Deployment's pod template
92
+ changes and Kubernetes rolls. Fast feedback, deterministic — but only
93
+ fires when konfig re-renders.
94
+
95
+ - **Runtime** (`Workload.web({ reloader: "stakater" })`). Emits
96
+ `reloader.stakater.com/auto: "true"` on the Deployment so
97
+ [Stakater Reloader](https://github.com/stakater/Reloader) watches
98
+ every referenced Secret/ConfigMap and patches the workload when the
99
+ in-cluster object changes. Pair with `ExternalSecrets` or a
100
+ controller that updates Secrets out-of-band.
101
+
102
+ Pick build-time hashes for stateless render → apply pipelines; pick
103
+ Reloader when secrets rotate independently of CI. They compose — a
104
+ config-managed Secret with `reloader: "stakater"` rolls on both
105
+ re-render and on out-of-band updates.
106
+
107
+ ## Secret backends — `SecretBackend<N, K>`
108
+
109
+ `SecretBackend` is the contract Sops / SealedSecrets / ExternalSecrets
110
+ implement. `Secret.bind({ secret, backend, source? })` ties an env-bundle
111
+ secret to a backend emission. `backend.requiresSource` is `true` for
112
+ Sops and SealedSecrets (they need plaintext at render time) and `false`
113
+ for ExternalSecrets (it just emits an `ExternalSecret` CR pointing at a
114
+ remote store).
115
+
116
+ ## k8s 1.30 types
117
+
118
+ Re-exported from `kubernetes-types@1.30.0` under
119
+ `src/.generated/k8s-types/`. We pin to the cluster minor and ship the
120
+ re-export rather than rolling our own OpenAPI codegen.
121
+
122
+ ## Layout
123
+
124
+ ```
125
+ src/
126
+ ├── index.ts barrel
127
+ ├── .generated/k8s-types/ thin re-export from kubernetes-types
128
+ ├── refs.ts SecretRef, ConfigMapRef, ServiceAccountRef
129
+ ├── env.ts secretEnv, configMapEnv, valueEnv, rawEnv
130
+ ├── volume.ts volumeFromSecret, volumeFromConfigMap, ...
131
+ ├── container.ts ContainerInput, PodSpecInput, imagePullSecret
132
+ ├── identity.ts Namespace, ServiceAccount, ConfigMap, Secret
133
+ ├── workload.ts Deployment, StatefulSet, Job, CronJob
134
+ ├── network.ts Service, Ingress, ingressTLS
135
+ ├── policy.ts PV, PVC, NetworkPolicy, RBAC
136
+ ├── workloadHelpers.ts Workload.web, Workload.cron
137
+ └── backend.ts SecretBackend contract + Secret.bind
138
+ ```
139
+
140
+ ## Requirements
141
+
142
+ konfig.ts builds on [Effect](https://effect.website/), which is still in
143
+ beta. Until Effect ships a stable 4.x, you must install the exact beta
144
+ konfig is developed against:
145
+
146
+ - **`effect@4.0.0-beta.70`** — required.
147
+ - **`@effect/platform-node@4.0.0-beta.70`** — required only for `render()`
148
+ (the Node filesystem/subprocess entrypoint); manifest-only consumers can
149
+ omit it.
150
+
151
+ The peer dependency is pinned to the exact version on purpose: Effect's beta
152
+ line makes breaking changes between builds, so a looser range would surface
153
+ as `ERESOLVE` install conflicts rather than a working install. This pin will
154
+ relax to a caret range once Effect reaches a stable 4.x.
@@ -0,0 +1,6 @@
1
+ export type { Deployment, DeploymentSpec, StatefulSet, StatefulSetSpec, } from "kubernetes-types/apps/v1";
2
+ export type { CronJob, Job, JobSpec, } from "kubernetes-types/batch/v1";
3
+ export type { ConfigMap, Container, ContainerPort, EnvVar, EnvVarSource, LocalObjectReference, Namespace, PersistentVolume, PersistentVolumeClaim, PodSpec, PodTemplateSpec, Secret, Service, ServiceAccount, ServicePort, Volume, VolumeMount, } from "kubernetes-types/core/v1";
4
+ export type { ObjectMeta } from "kubernetes-types/meta/v1";
5
+ export type { Ingress, IngressRule, IngressSpec, IngressTLS, NetworkPolicy, } from "kubernetes-types/networking/v1";
6
+ export type { ClusterRole, ClusterRoleBinding, Role, RoleBinding, } from "kubernetes-types/rbac/v1";
@@ -0,0 +1,26 @@
1
+ import type { Manifest } from "@konfig.ts/core";
2
+ import type { SecretSource } from "@konfig.ts/env";
3
+ export type BackendTag = "Sops" | "Sops.passthrough" | "SealedSecrets" | "ExternalSecrets" | "NativeSecret";
4
+ export interface BackendEmitInput<N extends string, K extends string> {
5
+ readonly name: N;
6
+ readonly namespace: string;
7
+ readonly keys: ReadonlyArray<K>;
8
+ readonly labels?: Readonly<Record<string, string>>;
9
+ readonly annotations?: Readonly<Record<string, string>>;
10
+ readonly source?: SecretSource<K, Manifest.RenderServices>;
11
+ }
12
+ export interface SecretBackend<N extends string, K extends string, RequiresSource extends boolean = boolean> {
13
+ readonly _tag: BackendTag;
14
+ readonly requiresSource: RequiresSource;
15
+ readonly emit: (input: BackendEmitInput<N, K>) => Manifest.Manifest<unknown>;
16
+ }
17
+ declare const BackendSourceMissing_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 & {
18
+ readonly _tag: "BackendSourceMissing";
19
+ } & Readonly<A>;
20
+ export declare class BackendSourceMissing extends BackendSourceMissing_base<{
21
+ readonly backend: BackendTag;
22
+ readonly secret: string;
23
+ }> {
24
+ get message(): string;
25
+ }
26
+ export {};
@@ -0,0 +1,190 @@
1
+ import type { BuiltImageRef, SecretRef, ServiceAccountRef } from "@konfig.ts/core";
2
+ import type { Container as K8sContainer, PodSpec as K8sPodSpec } from "./.generated/k8s-types";
3
+ import type { EnvVar } from "./env";
4
+ import type { ContainerPort, NamesOf, ProbeTarget } from "./ports";
5
+ import type { Volume, VolumeMount, VolumeNamesOf } from "./volume";
6
+ /**
7
+ * Container input — extends the full K8s Container API. Konfig's
8
+ * branded helpers (`EnvVar.value`, `EnvVar.fromSecret`,
9
+ * `EnvVar.fromConfigMap`) produce `EnvVar` instances whose runtime
10
+ * shape matches `K8sEnvVar`; the override
11
+ * here narrows the field to readonly + the branded helper output so
12
+ * `secretKeyRef.name` carries its `SecretRef<N>` brand at construction
13
+ * time.
14
+ *
15
+ * `image` is overridden as required (K8s lets it be optional for
16
+ * higher-level controllers that default it; konfig wants every
17
+ * container to have an explicit image).
18
+ *
19
+ * `ports[i].name` accepts the branded `PortName<string>` from `Port.make(...)`
20
+ * as well as a raw string; the typed builder `Container` is the
21
+ * link that captures the literal name union for cross-reference checks.
22
+ */
23
+ export interface ContainerInput extends Omit<K8sContainer, "env" | "image" | "ports" | "readinessProbe" | "livenessProbe" | "startupProbe" | "volumeMounts"> {
24
+ /**
25
+ * Container image. Accepts a raw string (escape hatch for vendor
26
+ * images: `ghcr.io/bitnami/postgresql:16.0.0`) or a `BuiltImageRef<App>`
27
+ * produced by `Dep.builtImageRef` / `Dep.provideImage`. The branded
28
+ * path ties the workload into the dep graph — a workload referencing
29
+ * an image whose build module isn't in the composition fails at
30
+ * `AppOfApps.entrypoint`.
31
+ */
32
+ readonly image: string | BuiltImageRef<string>;
33
+ readonly env?: ReadonlyArray<EnvVar>;
34
+ readonly ports?: ReadonlyArray<ContainerPort | {
35
+ readonly containerPort: number;
36
+ readonly name?: string;
37
+ readonly protocol?: "TCP" | "UDP" | "SCTP";
38
+ }>;
39
+ /**
40
+ * Probes accept either the upstream K8s shape or konfig's branded
41
+ * `ProbeTarget<string>` (from `Container`). The two are
42
+ * structurally close — the union lets a `ContainerSpec` flow
43
+ * through `Workload.web`'s containers slot without a cast.
44
+ */
45
+ readonly readinessProbe?: K8sContainer["readinessProbe"] | ProbeTarget<string>;
46
+ readonly livenessProbe?: K8sContainer["livenessProbe"] | ProbeTarget<string>;
47
+ readonly startupProbe?: K8sContainer["startupProbe"] | ProbeTarget<string>;
48
+ /**
49
+ * Volume mounts accept the upstream K8s shape or konfig's branded
50
+ * `VolumeMount<string>` (from `Container` / `mountRef`). Same
51
+ * rationale as the probe widening above.
52
+ */
53
+ readonly volumeMounts?: K8sContainer["volumeMounts"] | ReadonlyArray<VolumeMount<string>>;
54
+ }
55
+ /**
56
+ * Strongly-typed container builder. Captures the union of named ports
57
+ * declared via `port({ name, containerPort })` as a phantom type
58
+ * parameter, then constrains every probe's `port` field to that union.
59
+ *
60
+ * Pair with `Service.fromContainer({ forContainer })` in `network.ts`
61
+ * to link a Service's `targetPort` to the same union — a typo or
62
+ * undeclared port name is a compile error.
63
+ */
64
+ export interface ContainerSpec<Ports extends string = string, Mounts extends string = string> extends Omit<ContainerInput, "ports" | "readinessProbe" | "livenessProbe" | "startupProbe" | "volumeMounts"> {
65
+ readonly name: string;
66
+ readonly image: string;
67
+ readonly ports: ReadonlyArray<ContainerPort<Ports>>;
68
+ readonly readinessProbe?: ProbeTarget<Ports>;
69
+ readonly livenessProbe?: ProbeTarget<Ports>;
70
+ readonly startupProbe?: ProbeTarget<Ports>;
71
+ readonly volumeMounts?: ReadonlyArray<VolumeMount<Mounts>>;
72
+ readonly __portNames?: Ports;
73
+ readonly __mountNames?: Mounts;
74
+ }
75
+ export interface DefineContainerInput<Ports extends ReadonlyArray<ContainerPort<string>>, Mounts extends ReadonlyArray<VolumeMount<string>>, Envs extends ReadonlyArray<EnvVar<string>>> extends Omit<ContainerInput, "ports" | "readinessProbe" | "livenessProbe" | "startupProbe" | "volumeMounts" | "env"> {
76
+ readonly name: string;
77
+ readonly image: string;
78
+ readonly ports: Ports;
79
+ readonly readinessProbe?: ProbeTarget<NamesOf<Ports>>;
80
+ readonly livenessProbe?: ProbeTarget<NamesOf<Ports>>;
81
+ readonly startupProbe?: ProbeTarget<NamesOf<Ports>>;
82
+ readonly volumeMounts?: Mounts;
83
+ readonly env?: Envs & EnvDupCheck<Envs>;
84
+ }
85
+ type MountNamesOf<M extends ReadonlyArray<VolumeMount<string>>> = {
86
+ readonly [I in keyof M]: M[I] extends VolumeMount<infer N> ? N : never;
87
+ }[number];
88
+ type _EnvNameOf<X> = X extends EnvVar<infer N> ? N : never;
89
+ /**
90
+ * Union of any literal env-var names that appear more than once in `Envs`.
91
+ * Computes the intersection of pairwise names — if `Envs[I]` and `Envs[J]`
92
+ * share a literal name (i ≠ j), that name leaks into the union. Empty
93
+ * when every name is unique.
94
+ */
95
+ type DuplicateEnvNames<Envs extends ReadonlyArray<EnvVar<string>>> = {
96
+ [I in keyof Envs]: {
97
+ [J in keyof Envs]: J extends I ? never : _EnvNameOf<Envs[I]> & _EnvNameOf<Envs[J]> extends never ? never : _EnvNameOf<Envs[I]> & _EnvNameOf<Envs[J]>;
98
+ }[number];
99
+ }[number];
100
+ /**
101
+ * Empty (`unknown`) when `Envs` has no duplicate names; otherwise an
102
+ * inline object type whose required `_konfig_duplicate_env_names`
103
+ * property carries a template-literal hint sentence. Intersected with
104
+ * the `env` slot, an unmet property surfaces the sentence inline in the
105
+ * TS error — far more discoverable than a runtime "last-wins" surprise.
106
+ */
107
+ type EnvDupCheck<Envs extends ReadonlyArray<EnvVar<string>>> = [
108
+ DuplicateEnvNames<Envs>
109
+ ] extends [never] ? unknown : {
110
+ readonly _konfig_duplicate_env_names: `Duplicate env name(s): "${DuplicateEnvNames<Envs>}". K8s silently last-wins; rename one of the colliding entries or remove the manual valueEnv that shadows another producer.`;
111
+ };
112
+ /**
113
+ * `Container` value namespace.
114
+ *
115
+ * const apiContainer = Container.define({
116
+ * name: "api",
117
+ * image: apiImage,
118
+ * ports: [Port.make({ name: "http", containerPort: 8080 })],
119
+ * readinessProbe: { httpGet: { port: Port.ref("http") } },
120
+ * env: [...],
121
+ * });
122
+ *
123
+ * `Container.define` captures the union of named ports (from
124
+ * `Port.make`) as `Ports`, the union of mounted volume names (from
125
+ * `Volume.mountRef`) as `Mounts`, and validates that the `env` list
126
+ * has no duplicate env-var names. The first two phantoms travel on
127
+ * `ContainerSpec`; `Pod.define` checks the container's Mounts against
128
+ * the pod's declared volume names.
129
+ *
130
+ * Duplicate env names are caught at the call site via a template-literal
131
+ * error message — see `EnvDupCheck`. With no volumeMounts and no env,
132
+ * all phantoms collapse to `never` so the container slots into any pod.
133
+ */
134
+ export declare const Container: {
135
+ define: <const Ports extends ReadonlyArray<ContainerPort<string>>, const Mounts extends ReadonlyArray<VolumeMount<string>> = readonly [], const Envs extends ReadonlyArray<EnvVar<string>> = readonly []>(input: DefineContainerInput<Ports, Mounts, Envs>) => ContainerSpec<NamesOf<Ports>, MountNamesOf<Mounts>>;
136
+ };
137
+ /**
138
+ * `Pod.define` input. The pod declares a tuple of `Volume`s and lists
139
+ * containers whose `Mounts` phantom is checked against the declared
140
+ * volume names via `NoInfer`. A container referencing an undeclared
141
+ * volume — or a typo — fails at the call site rather than at pod-startup
142
+ * time with "container references volume not found."
143
+ */
144
+ export interface DefinePodInput<V extends ReadonlyArray<Volume<string>>> {
145
+ readonly volumes: V;
146
+ readonly containers: ReadonlyArray<ContainerSpec<string, NoInfer<VolumeNamesOf<V>>>>;
147
+ readonly initContainers?: ReadonlyArray<ContainerSpec<string, NoInfer<VolumeNamesOf<V>>>>;
148
+ }
149
+ export interface DefinedPod<MountNames extends string> {
150
+ readonly volumes: ReadonlyArray<Volume<MountNames>>;
151
+ readonly containers: ReadonlyArray<ContainerSpec<string, MountNames>>;
152
+ readonly initContainers?: ReadonlyArray<ContainerSpec<string, MountNames>>;
153
+ }
154
+ /**
155
+ * Pod spec input — extends K8s PodSpec but tightens the fields where
156
+ * konfig adds brand checking: `imagePullSecrets`, `serviceAccountName`,
157
+ * and `volumes` (the helpers in `volume.ts` produce konfig `Volume`
158
+ * objects that lower to K8s `Volume`).
159
+ */
160
+ export interface PodSpecInput extends Omit<K8sPodSpec, "containers" | "initContainers" | "volumes" | "imagePullSecrets" | "serviceAccountName"> {
161
+ readonly containers: ReadonlyArray<ContainerInput>;
162
+ readonly initContainers?: ReadonlyArray<ContainerInput>;
163
+ readonly volumes?: ReadonlyArray<Volume>;
164
+ readonly imagePullSecrets?: ReadonlyArray<{
165
+ readonly name: SecretRef<string>;
166
+ }>;
167
+ readonly serviceAccountName?: ServiceAccountRef<string> | string;
168
+ }
169
+ /**
170
+ * `Pod` value namespace.
171
+ *
172
+ * const pod = Pod.define({
173
+ * volumes: [Volume.empty({ name: "config" })],
174
+ * containers: [Container.define({ ..., volumeMounts: [...] })],
175
+ * });
176
+ *
177
+ * imagePullSecrets: [Pod.imagePullSecret(ghcrRef)],
178
+ *
179
+ * - `Pod.define(input)` ties container `volumeMounts[i].name` to the
180
+ * pod's declared volume names via `NoInfer`.
181
+ * - `Pod.imagePullSecret(ref)` returns the `{ name: SecretRef }` entry
182
+ * consumed by Deployment / StatefulSet / Job / CronJob workloads.
183
+ */
184
+ export declare const Pod: {
185
+ define: <const V extends ReadonlyArray<Volume<string>>>(input: DefinePodInput<V>) => DefinedPod<VolumeNamesOf<V>>;
186
+ imagePullSecret: (ref: SecretRef<string>) => {
187
+ readonly name: SecretRef<string>;
188
+ };
189
+ };
190
+ export {};
package/dist/env.d.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { ConfigMapRef, SecretRef } from "@konfig.ts/core";
2
+ export interface EnvVarSource {
3
+ readonly secretKeyRef?: {
4
+ readonly name: string;
5
+ readonly key: string;
6
+ readonly optional?: boolean;
7
+ };
8
+ readonly configMapKeyRef?: {
9
+ readonly name: string;
10
+ readonly key: string;
11
+ readonly optional?: boolean;
12
+ };
13
+ readonly fieldRef?: {
14
+ readonly fieldPath: string;
15
+ readonly apiVersion?: string;
16
+ };
17
+ readonly resourceFieldRef?: {
18
+ readonly containerName?: string;
19
+ readonly resource: string;
20
+ };
21
+ }
22
+ /**
23
+ * Container env-var entry. The phantom `N` records the literal env-var
24
+ * name at the type level so `Container` can detect duplicates
25
+ * across the entry list. The default `N = string` preserves loose-typed
26
+ * call sites.
27
+ */
28
+ export interface EnvVarShape<N extends string = string> {
29
+ readonly name: N;
30
+ readonly value?: string;
31
+ readonly valueFrom?: EnvVarSource;
32
+ }
33
+ export interface ValueEnvInput<N extends string> {
34
+ readonly name: N;
35
+ readonly value: string;
36
+ }
37
+ export interface SecretEnvInput<EnvName extends string, N extends string, K extends string> {
38
+ readonly name: EnvName;
39
+ readonly ref: SecretRef<N, K>;
40
+ /**
41
+ * Constrained to the keys carried by `ref`. `NoInfer` locks `K` to
42
+ * whatever the ref declares, so the typo `key: "passowrd"` fails at
43
+ * compile time when the ref's K is `"url" | "username" | "password"`.
44
+ * Refs constructed with the default `K = string` accept any string.
45
+ */
46
+ readonly key: NoInfer<K>;
47
+ readonly optional?: boolean;
48
+ }
49
+ export interface SecretEnvForPodInput<EnvName extends string, N extends string, K extends string, Ns extends string> {
50
+ readonly name: EnvName;
51
+ /**
52
+ * Ref whose namespace slot must match `podNamespace`. Pass refs that
53
+ * came from `Secret.make({ namespace })` in the same namespace as
54
+ * the consuming pod. For legitimate cross-namespace cases, widen
55
+ * via `SecretRef.unsafeReNamespace(ref)` first.
56
+ */
57
+ readonly ref: SecretRef<N, K, NoInfer<Ns>>;
58
+ readonly key: NoInfer<K>;
59
+ readonly podNamespace: Ns;
60
+ readonly optional?: boolean;
61
+ }
62
+ export interface ConfigMapEnvInput<EnvName extends string, N extends string, K extends string> {
63
+ readonly name: EnvName;
64
+ readonly ref: ConfigMapRef<N, K>;
65
+ /**
66
+ * Constrained to the keys carried by `ref`. `NoInfer` locks `K` to
67
+ * whatever the ref declares — typos like `key: "passwrod"` fail at
68
+ * compile time when the ref's K is `"HOST" | "PORT" | "LOG_LEVEL"`.
69
+ * Refs constructed with the default `K = string` accept any string.
70
+ */
71
+ readonly key: NoInfer<K>;
72
+ readonly optional?: boolean;
73
+ }
74
+ export interface RawEnvInput<N extends string> {
75
+ readonly name: N;
76
+ readonly value?: string;
77
+ readonly valueFrom?: EnvVarSource;
78
+ }
79
+ /**
80
+ * `EnvVar` value namespace.
81
+ *
82
+ * env: [
83
+ * EnvVar.value({ name: "PORT", value: "8080" }),
84
+ * EnvVar.fromSecret({ name: "DB_URL", ref: dbCreds.ref, key: "url" }),
85
+ * EnvVar.fromSecretForPod({
86
+ * name: "DB_URL_PRIMARY", ref: dbCreds.ref, key: "url",
87
+ * podNamespace: "app",
88
+ * }),
89
+ * EnvVar.fromConfigMap({ name: "NEW_UI", ref: flags.ref, key: "NEW_UI" }),
90
+ * EnvVar.raw({ name: "POD_NAME", valueFrom: { fieldRef: ... } }),
91
+ * ]
92
+ *
93
+ * Each constructor captures the literal env-var name (`N`) so
94
+ * `Container` can detect duplicate entries across the env list.
95
+ */
96
+ export declare const EnvVar: {
97
+ value: <const N extends string>(input: ValueEnvInput<N>) => EnvVar<N>;
98
+ fromSecret: <const EnvName extends string, N extends string, K extends string = string>(input: SecretEnvInput<EnvName, N, K>) => EnvVar<EnvName>;
99
+ /**
100
+ * Namespace-checked variant. The ref's namespace slot (`Ns`) must
101
+ * match `podNamespace`. Catches "pod in namespace A references a
102
+ * Secret in namespace B" at compile time — kube-apiserver only
103
+ * resolves `valueFrom.secretKeyRef` against the pod's own namespace,
104
+ * so cross-namespace refs are runtime errors.
105
+ */
106
+ fromSecretForPod: <const EnvName extends string, N extends string, K extends string, const Ns extends string>(input: SecretEnvForPodInput<EnvName, N, K, Ns>) => EnvVar<EnvName>;
107
+ fromConfigMap: <const EnvName extends string, N extends string, K extends string = string>(input: ConfigMapEnvInput<EnvName, N, K>) => EnvVar<EnvName>;
108
+ raw: <const N extends string>(input: RawEnvInput<N>) => EnvVar<N>;
109
+ };
110
+ export type EnvVar<N extends string = string> = EnvVarShape<N>;
@@ -0,0 +1,133 @@
1
+ import { type Manifest, type RenderError } from "@konfig.ts/core";
2
+ import type { DownwardEntry, EnvMember, Environment, LiteralEntry, SecretEntry, SecretSource } from "@konfig.ts/env";
3
+ import { Layer } from "effect";
4
+ import type { SecretBackend } from "./backend";
5
+ import type { EnvVar } from "./env";
6
+ import { type DeclaredSecret } from "./secretBind";
7
+ export interface DeclaredLiteral<EnvName extends string, T> {
8
+ readonly envName: EnvName;
9
+ readonly value: T;
10
+ readonly envVar: EnvVar;
11
+ }
12
+ export interface DeclaredDownward<EnvName extends string> {
13
+ readonly envName: EnvName;
14
+ readonly fieldPath: string;
15
+ readonly envVar: EnvVar;
16
+ }
17
+ /**
18
+ * Per-member declared shape produced by `Environment.bind`. Mirrors the
19
+ * structure of the bundle: secrets give `DeclaredSecret`, literals
20
+ * `DeclaredLiteral`, downwards `DeclaredDownward`, and nested
21
+ * `Environment` members give a `members` sub-record with the same
22
+ * recursive shape.
23
+ */
24
+ export type DeclaredMember<A extends EnvMember, Ns extends string = string> = A extends SecretEntry<infer N, infer K, infer _E> ? [N, K] extends [string, string] ? DeclaredSecret<N, K, Ns> : never : A extends LiteralEntry<infer EnvName, infer T> ? [EnvName] extends [string] ? DeclaredLiteral<EnvName, T> : never : A extends DownwardEntry<infer EnvName> ? [EnvName] extends [string] ? DeclaredDownward<EnvName> : never : A extends Environment<infer SubM> ? {
25
+ readonly [K in keyof SubM]: DeclaredMember<SubM[K], Ns>;
26
+ } : never;
27
+ export interface DeclaredEnvironment<M extends Readonly<Record<string, EnvMember>>, Ns extends string = string> {
28
+ readonly envVars: ReadonlyArray<EnvVar>;
29
+ readonly manifests: ReadonlyArray<Manifest.Manifest<unknown>>;
30
+ readonly members: {
31
+ readonly [K in keyof M]: DeclaredMember<M[K], Ns>;
32
+ };
33
+ readonly valuesLayer: Layer.Layer<unknown, RenderError, Manifest.RenderServices>;
34
+ }
35
+ interface _SecretMemberOptionsBase {
36
+ readonly labels?: Readonly<Record<string, string>>;
37
+ readonly annotations?: Readonly<Record<string, string>>;
38
+ }
39
+ interface _SecretMemberBackendRequiresSource<N extends string, K extends string> extends _SecretMemberOptionsBase {
40
+ readonly backend: SecretBackend<N, K, true>;
41
+ readonly source: SecretSource<K, Manifest.RenderServices>;
42
+ }
43
+ interface _SecretMemberBackendOptionalSource<N extends string, K extends string> extends _SecretMemberOptionsBase {
44
+ readonly backend: SecretBackend<N, K, false>;
45
+ readonly source?: SecretSource<K, Manifest.RenderServices>;
46
+ }
47
+ interface _SecretMemberSourceOnly<_N extends string, K extends string> extends _SecretMemberOptionsBase {
48
+ readonly backend?: undefined;
49
+ readonly source: SecretSource<K, Manifest.RenderServices>;
50
+ }
51
+ /**
52
+ * Per-secret-member options at bind time. Backends with `RequiresSource: true`
53
+ * (`Sops.backend`, `SealedSecrets.backend`, `NativeSecret.backend`) make
54
+ * `source` mandatory at the type level. Backends with `false`
55
+ * (`ExternalSecrets.backend`, `Sops.passthrough`) make it optional. With no
56
+ * backend, `source` becomes mandatory — used to feed the in-process
57
+ * `SecretValues` layer for tests / local renders.
58
+ */
59
+ export type SecretMemberOptions<N extends string, K extends string> = _SecretMemberBackendRequiresSource<N, K> | _SecretMemberBackendOptionalSource<N, K> | _SecretMemberSourceOnly<N, K>;
60
+ export type SecretMemberOptionsFor<A> = A extends SecretEntry<infer N, infer K, infer _E> ? [N, K] extends [string, string] ? SecretMemberOptions<N, K> : never : never;
61
+ /**
62
+ * True iff `M` (recursively, via nested `Environment` members)
63
+ * contains at least one `SecretEntry`. Used to flip `secrets` between
64
+ * required (any secrets present) and optional (literals/downwards
65
+ * only) on `BindEnvironmentInput`.
66
+ */
67
+ export type HasSecrets<M extends Readonly<Record<string, EnvMember>>> = true extends {
68
+ readonly [K in keyof M]: M[K] extends SecretEntry<infer _N, infer _K, infer _E> ? true : M[K] extends Environment<infer Sub> ? HasSecrets<Sub> : false;
69
+ }[keyof M] ? true : false;
70
+ /**
71
+ * Recursive per-member secret options. For a secret member, the option
72
+ * matches its `SecretMemberOptionsFor` and is required. For a nested
73
+ * `Environment` member that itself contains secrets, the option is
74
+ * `SecretMembersOpts<SubM>` and is also required. Nested environments
75
+ * with no secrets are omitted entirely (no key needed).
76
+ *
77
+ * Together with `HasSecrets`, this enforces at compile time that every
78
+ * `Secret` reachable from the bundle is acknowledged at bind
79
+ * time — adding a new secret to the env contract forces every call
80
+ * site to update.
81
+ */
82
+ export type SecretMembersOpts<M extends Readonly<Record<string, EnvMember>>> = {
83
+ readonly [K in keyof M as M[K] extends SecretEntry<infer _N, infer _K, infer _E> ? K : M[K] extends Environment<infer SubM> ? HasSecrets<SubM> extends true ? K : never : never]: M[K] extends SecretEntry<infer _N, infer _K, infer _E> ? SecretMemberOptionsFor<M[K]> : M[K] extends Environment<infer SubM> ? SecretMembersOpts<SubM> : never;
84
+ };
85
+ /**
86
+ * Per-literal value override for bind time. Keyed by member name, typed
87
+ * to each literal's declared `T`. Useful when a `Literal` is a
88
+ * runtime contract (carries a `schema: Config.string(envName)` for app
89
+ * code to yield) but the manifest's emitted value differs per env —
90
+ * e.g. `CLIENT_URL`, `S3_ENDPOINT`, host/URL literals.
91
+ *
92
+ * Recurses on nested `Environment` members — pass `{group: {host: "x"}}`
93
+ * to override a literal nested inside a sub-bundle.
94
+ */
95
+ export type LiteralMembersOpts<M extends Readonly<Record<string, EnvMember>>> = {
96
+ readonly [K in keyof M as M[K] extends LiteralEntry<infer _EnvName, infer _T> ? K : M[K] extends Environment<infer _SubM> ? K : never]?: M[K] extends LiteralEntry<infer _EnvName, infer T> ? T : M[K] extends Environment<infer SubM> ? LiteralMembersOpts<SubM> : never;
97
+ };
98
+ interface _BindEnvironmentInputBase<M extends Readonly<Record<string, EnvMember>>, Ns extends string = string> {
99
+ readonly env: Environment<M>;
100
+ /**
101
+ * Per-literal value override for the manifest's emitted env var. The
102
+ * runtime read (via `entry.schema`, if provided) is unchanged —
103
+ * overrides only affect what the konfig module writes into the
104
+ * Deployment's `env` array.
105
+ */
106
+ readonly literals?: LiteralMembersOpts<M>;
107
+ /**
108
+ * Override every secret member's namespace for this bind. Useful when
109
+ * the bundle is consumed across multiple k8s namespaces (e.g. prod /
110
+ * staging / local) without redeclaring each contract. Recurses into
111
+ * nested `Environment` members.
112
+ *
113
+ * When passed as a string literal, the literal flows into each
114
+ * `DeclaredSecret.ref`'s brand so `secretEnvForPod` can enforce
115
+ * cross-namespace coherence on hand-spliced env entries.
116
+ */
117
+ readonly namespace?: Ns;
118
+ }
119
+ /**
120
+ * `secrets` flips between required and optional based on whether `M`
121
+ * actually contains any secrets. A bundle of literals/downwards only
122
+ * has nothing to bind, so `secrets` stays optional. A bundle with even
123
+ * one `Secret` (directly or nested) forces the caller to supply
124
+ * every secret member — and for each member, either a `backend` or a
125
+ * `source` (see `SecretMemberOptions`).
126
+ */
127
+ export type BindEnvironmentInput<M extends Readonly<Record<string, EnvMember>>, Ns extends string = string> = _BindEnvironmentInputBase<M, Ns> & (HasSecrets<M> extends true ? {
128
+ readonly secrets: SecretMembersOpts<M>;
129
+ } : {
130
+ readonly secrets?: SecretMembersOpts<M>;
131
+ });
132
+ export declare const bindEnvironment: <const M extends Readonly<Record<string, EnvMember>>, const Ns extends string = string>(input: BindEnvironmentInput<M, Ns>) => DeclaredEnvironment<M, Ns>;
133
+ export {};