@konfig.ts/env 0.0.8 → 0.0.10

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/dist/entry.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface EnvClaim {
19
19
  export interface HasEnvClaims {
20
20
  readonly envClaims: ReadonlyArray<EnvClaim>;
21
21
  }
22
+ /** Builds a single `EnvClaim` — shared by Secret/Literal/Downward entries. */
23
+ export declare const _envClaim: ({ envName, label }: EnvClaim) => EnvClaim;
22
24
  export interface MakeEntryInput<C extends Config.Config<unknown>, M extends object> {
23
25
  readonly config: C;
24
26
  readonly metadata: M;
package/dist/index.mjs CHANGED
@@ -8,15 +8,20 @@ var EnvNameCollision = class extends Data.TaggedError("EnvNameCollision") {
8
8
  return `environment variable name "${this.envName}" is claimed by multiple entries: ${this.claims.map((c) => `"${c}"`).join(", ")}`;
9
9
  }
10
10
  };
11
+ /** Builds a single `EnvClaim` — shared by Secret/Literal/Downward entries. */
12
+ const _envClaim = ({ envName, label }) => ({
13
+ envName,
14
+ label
15
+ });
11
16
  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
17
  //#endregion
13
18
  //#region src/downward.ts
14
19
  const _define$3 = (input) => {
15
20
  const parser = Config.string(input.envName);
16
- const envClaims = [{
21
+ const envClaims = [_envClaim({
17
22
  envName: input.envName,
18
23
  label: `Downward(${input.envName})`
19
- }];
24
+ })];
20
25
  return _makeEntry({
21
26
  config: parser,
22
27
  metadata: {
@@ -35,6 +40,21 @@ const _define$3 = (input) => {
35
40
  const Downward = { define: _define$3 };
36
41
  //#endregion
37
42
  //#region src/environment.ts
43
+ /**
44
+ * Flattens every member's `envClaims` and cross-checks for envName
45
+ * collisions.
46
+ *
47
+ * Throws `EnvNameCollision` (a `Data.TaggedError`) synchronously on
48
+ * collision. `Environment.define` is a synchronous, user-facing config
49
+ * builder — collisions are an authoring mistake caught at definition
50
+ * time (typically module load), the same moment `_CheckCollisions`
51
+ * already flags most of them at compile time. There is no Effect
52
+ * context to run this in, so surfacing it as `Effect<A, EnvNameCollision>`
53
+ * would just push callers to unwrap it immediately; a plain throw
54
+ * (mirroring the type-level check it complements) keeps the API a
55
+ * direct `M -> Environment<M>` function. Add an Either/Effect variant
56
+ * only if a caller actually needs to recover from this at runtime.
57
+ */
38
58
  const _collectClaims = (members) => {
39
59
  const byEnvName = /* @__PURE__ */ new Map();
40
60
  const out = [];
@@ -85,10 +105,10 @@ const _define$1 = (input) => {
85
105
  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"));
86
106
  const serialized = userSerialize(input.value);
87
107
  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>");
88
- const envClaims = [{
108
+ const envClaims = [_envClaim({
89
109
  envName: input.envName,
90
110
  label: `Literal(${input.envName})`
91
- }];
111
+ })];
92
112
  return _makeEntry({
93
113
  config: parser,
94
114
  metadata: {
@@ -136,7 +156,7 @@ const _define = (input) => {
136
156
  const fields = {};
137
157
  for (const key of keys) fields[key] = Config.redacted(input.env[key]);
138
158
  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");
139
- const envClaims = keys.map((key) => ({
159
+ const envClaims = keys.map((key) => _envClaim({
140
160
  envName: input.env[key],
141
161
  label: `Secret(${input.name}).${key}`
142
162
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["_define","_define","_define"],"sources":["../src/entry.ts","../src/downward.ts","../src/environment.ts","../src/layer.ts","../src/literal.ts","../src/runtime.ts","../src/secret.ts","../src/source.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 { Config } from \"effect\"\nimport { _makeEntry, type EntryMarker, type EnvClaim, type HasEnvClaims } from \"./entry\"\n\nexport interface DownwardEntry<EnvName extends string>\n extends Config.Config<string>, EntryMarker<\"Downward\">, HasEnvClaims\n{\n readonly envName: EnvName\n readonly fieldPath: string\n}\n\nexport interface DefineDownwardInput<EnvName extends string> {\n readonly envName: EnvName\n readonly fieldPath: string\n}\n\nconst _define = <const EnvName extends string>(\n input: DefineDownwardInput<EnvName>\n): DownwardEntry<EnvName> => {\n const parser = Config.string(input.envName)\n\n const envClaims: ReadonlyArray<EnvClaim> = [\n { envName: input.envName, label: `Downward(${input.envName})` }\n ]\n\n return _makeEntry({\n config: parser,\n metadata: {\n _kind: \"Downward\" as const,\n envName: input.envName,\n fieldPath: input.fieldPath,\n envClaims\n }\n })\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 define: _define\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 type { Context, Effect } from \"effect\"\nimport { Layer } from \"effect\"\nimport type { Environment, EnvMember, 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 readonly [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 readonly tag: Context.Service<Self, EnvironmentShape<M>>\n readonly env: Environment<M>\n}\n\nexport const environmentLayer = <Self, M extends Readonly<Record<string, EnvMember>>>(\n input: EnvironmentLayerInput<Self, M>\n): Layer.Layer<Self> =>\n Layer.effect(\n input.tag,\n unsafeCoerce<Effect.Effect<EnvironmentShape<M>>>(\n input.env,\n \"Environment<M> extends Config<EnvironmentShape<M>>, and Config is structurally a no-deps Effect — Layer.effect accepts it\"\n )\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 { type Config, Effect } from \"effect\"\nimport type { Environment, EnvMember } 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 env: Environment<M>\n): Effect.Effect<EnvironmentShape<M>, Config.ConfigError> =>\n Effect.gen(function*() {\n const decoded = yield* env\n return unsafeCoerce<EnvironmentShape<M>>(\n decoded,\n \"Environment<M> extends Config<{ [K in keyof M]: MemberValue<M[K]> }>, structurally equal to EnvironmentShape<M> by definition\"\n )\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* new SecretSourceError({\n source: \"fromCommand\",\n key,\n cause: \"secret command produced empty output\"\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"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["_define","_define","_define"],"sources":["../src/entry.ts","../src/downward.ts","../src/environment.ts","../src/layer.ts","../src/literal.ts","../src/runtime.ts","../src/secret.ts","../src/source.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\n/** Builds a single `EnvClaim` — shared by Secret/Literal/Downward entries. */\nexport const _envClaim = ({ envName, label }: EnvClaim): EnvClaim => ({ envName, label })\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 { Config } from \"effect\"\nimport { _envClaim, _makeEntry, type EntryMarker, type EnvClaim, type HasEnvClaims } from \"./entry\"\n\nexport interface DownwardEntry<EnvName extends string>\n extends Config.Config<string>, EntryMarker<\"Downward\">, HasEnvClaims\n{\n readonly envName: EnvName\n readonly fieldPath: string\n}\n\nexport interface DefineDownwardInput<EnvName extends string> {\n readonly envName: EnvName\n readonly fieldPath: string\n}\n\nconst _define = <const EnvName extends string>(\n input: DefineDownwardInput<EnvName>\n): DownwardEntry<EnvName> => {\n const parser = Config.string(input.envName)\n\n const envClaims: ReadonlyArray<EnvClaim> = [\n _envClaim({ envName: input.envName, label: `Downward(${input.envName})` })\n ]\n\n return _makeEntry({\n config: parser,\n metadata: {\n _kind: \"Downward\" as const,\n envName: input.envName,\n fieldPath: input.fieldPath,\n envClaims\n }\n })\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 define: _define\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\n/**\n * Flattens every member's `envClaims` and cross-checks for envName\n * collisions.\n *\n * Throws `EnvNameCollision` (a `Data.TaggedError`) synchronously on\n * collision. `Environment.define` is a synchronous, user-facing config\n * builder — collisions are an authoring mistake caught at definition\n * time (typically module load), the same moment `_CheckCollisions`\n * already flags most of them at compile time. There is no Effect\n * context to run this in, so surfacing it as `Effect<A, EnvNameCollision>`\n * would just push callers to unwrap it immediately; a plain throw\n * (mirroring the type-level check it complements) keeps the API a\n * direct `M -> Environment<M>` function. Add an Either/Effect variant\n * only if a caller actually needs to recover from this at runtime.\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 type { Context, Effect } from \"effect\"\nimport { Layer } from \"effect\"\nimport type { Environment, EnvMember, 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 readonly [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 readonly tag: Context.Service<Self, EnvironmentShape<M>>\n readonly env: Environment<M>\n}\n\nexport const environmentLayer = <Self, M extends Readonly<Record<string, EnvMember>>>(\n input: EnvironmentLayerInput<Self, M>\n): Layer.Layer<Self> =>\n Layer.effect(\n input.tag,\n unsafeCoerce<Effect.Effect<EnvironmentShape<M>>>(\n input.env,\n \"Environment<M> extends Config<EnvironmentShape<M>>, and Config is structurally a no-deps Effect — Layer.effect accepts it\"\n )\n )\n","import { unsafeCoerce } from \"@konfig.ts/core\"\nimport { Config } from \"effect\"\nimport { _envClaim, _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\n/** Types the default `String(v)` serializer can stringify meaningfully. */\ntype _Primitive = string | number | boolean | bigint\n\n/**\n * `serialize` is optional only for primitive `T` (the default `String(v)`\n * serializer is meaningful there). For any other `T` — objects, arrays,\n * etc. — `serialize` is required, so a caller can't silently end up with\n * `\"[object Object]\"` as the env value.\n */\nexport type DefineLiteralInput<EnvName extends string, T> =\n & {\n readonly envName: EnvName\n readonly value: T\n readonly schema?: Config.Config<T>\n }\n & (T extends _Primitive ? { readonly serialize?: (value: T) => string }\n : { readonly serialize: (value: T) => string })\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 _envClaim({ 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 { type Config, Effect } from \"effect\"\nimport type { Environment, EnvMember } 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 env: Environment<M>\n): Effect.Effect<EnvironmentShape<M>, Config.ConfigError> =>\n Effect.gen(function*() {\n const decoded = yield* env\n return unsafeCoerce<EnvironmentShape<M>>(\n decoded,\n \"Environment<M> extends Config<{ [K in keyof M]: MemberValue<M[K]> }>, structurally equal to EnvironmentShape<M> by definition\"\n )\n })\n","import { unsafeCoerce } from \"@konfig.ts/core\"\nimport { Config, type Redacted } from \"effect\"\nimport { _envClaim, _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 _envClaim({ envName: input.env[key], 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* new SecretSourceError({\n source: \"fromCommand\",\n key,\n cause: \"secret command produced empty output\"\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"],"mappings":"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"}
package/dist/literal.d.ts CHANGED
@@ -14,12 +14,23 @@ export interface LiteralEntry<EnvName extends string, T> extends Config.Config<T
14
14
  */
15
15
  readonly serialize: (value: unknown) => string;
16
16
  }
17
- export interface DefineLiteralInput<EnvName extends string, T> {
17
+ /** Types the default `String(v)` serializer can stringify meaningfully. */
18
+ type _Primitive = string | number | boolean | bigint;
19
+ /**
20
+ * `serialize` is optional only for primitive `T` (the default `String(v)`
21
+ * serializer is meaningful there). For any other `T` — objects, arrays,
22
+ * etc. — `serialize` is required, so a caller can't silently end up with
23
+ * `"[object Object]"` as the env value.
24
+ */
25
+ export type DefineLiteralInput<EnvName extends string, T> = {
18
26
  readonly envName: EnvName;
19
27
  readonly value: T;
20
28
  readonly schema?: Config.Config<T>;
29
+ } & (T extends _Primitive ? {
21
30
  readonly serialize?: (value: T) => string;
22
- }
31
+ } : {
32
+ readonly serialize: (value: T) => string;
33
+ });
23
34
  export type AnyLiteralEntry = LiteralEntry<string, unknown>;
24
35
  /**
25
36
  * `Literal` value namespace.
@@ -32,3 +43,4 @@ export type AnyLiteralEntry = LiteralEntry<string, unknown>;
32
43
  export declare const Literal: {
33
44
  define: <const EnvName extends string, T = string>(input: DefineLiteralInput<EnvName, T>) => LiteralEntry<EnvName, T>;
34
45
  };
46
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konfig.ts/env",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Yieldable environment atoms (defineSecret, defineLiteral, defineDownward, defineEnvironment) shared between konfig manifest emission and the runtime pod application.",
5
5
  "license": "MIT",
6
6
  "author": "David Stahl-Gruber",
@@ -51,7 +51,7 @@
51
51
  "postpack": "node ../../scripts/prepack-exports.mjs restore"
52
52
  },
53
53
  "dependencies": {
54
- "@konfig.ts/core": "0.0.8"
54
+ "@konfig.ts/core": "0.0.10"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "effect": "4.0.0-beta.98"