@evinvest/settings 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -66,7 +66,45 @@ invalid settings (2 problems)
66
66
  ```
67
67
 
68
68
  Validators: `str`, `num`, `int`, `port`, `bool`, `url`, `list`, `oneOf` —
69
- refined by `optional(v)`, `withDefault(v, 'literal')`, `secret(v)`.
69
+ refined by `optional(v)`, `withDefault(v, 'literal')`, `secret(v)`,
70
+ `requiredIn(v, 'production')`.
71
+
72
+ ### Optional here, mandatory in production
73
+
74
+ The setting that hurts is not the missing required one — that already stops the
75
+ boot. It is the optional whose absence is a *silent* no-op: no `SMTP_HOST` means
76
+ mail is logged instead of sent, no `SENTRY_DSN` means the alerts never arrive.
77
+ `requiredIn` names the profiles where that convenience ends:
78
+
79
+ ```ts
80
+ SMTP_HOST: requiredIn(optional(str()), 'production'),
81
+ PUBLIC_ORIGIN: requiredIn(withDefault(url(), 'http://localhost:3000'), 'production', 'staging'),
82
+ ```
83
+
84
+ ```text
85
+ invalid settings (1 problem)
86
+ - SMTP_HOST: missing (required when APP_ENV=production)
87
+ ```
88
+
89
+ The profile is `APP_ENV` from `runtimeEnv` (empty counts as unset, default
90
+ `development`). In a Next.js app pass `profile: process.env.NODE_ENV` instead of
91
+ introducing a second, drifting name — and pass it explicitly whenever a `client`
92
+ setting uses `requiredIn`, since the browser bundle carries no `APP_ENV`. With an
93
+ explicit `profile` the message drops the variable name (`…required when the
94
+ active profile is production`), so it never points at a variable that had no say.
95
+
96
+ ### Failing the boot with a useful exit code
97
+
98
+ `orExit(() => …)` prints the aggregate message and exits `78` (`EX_CONFIG`) on a
99
+ `SettingsError`, rethrowing anything else. Exit 1 is indistinguishable from "a
100
+ dependency blinked", which a restart fixes; 78 says a restart cannot.
101
+
102
+ ```ts
103
+ // instrumentation.ts — before the server accepts traffic
104
+ export function register() {
105
+ orExit(() => assertConfig());
106
+ }
107
+ ```
70
108
 
71
109
  ## Rust ↔ TS parity
72
110
 
@@ -87,13 +125,21 @@ shared rules:
87
125
  `±(2^53 - 1)` — use `str()` for 64-bit ids.
88
126
  - errors aggregate into one `SettingsError` (message shape shared with the
89
127
  Rust `Display` impl); `secret(v)` redacts values in error output.
128
+ - `requiredIn(v, …)` ↔ `#[required_in(…)]`, matched against the same canonical
129
+ `APP_ENV` with the same `development` fallback, checked **before** a default
130
+ applies, and rejected on a setting that is already required everywhere (a
131
+ compile error on the Rust side, a thrown declaration error here). The profile
132
+ semantics are pinned by mirrored tests:
133
+ [`test/profile.node.test.ts`](./test/profile.node.test.ts) ↔
134
+ `rust/src/settings/tests.rs` (`required_in_*`).
90
135
  - the contract is pinned by mirrored vectors:
91
136
  [`test/contract.node.test.ts`](./test/contract.node.test.ts) ↔
92
137
  `rust/src/settings/tests.rs` (`mod contract`). Change both sides or neither.
93
138
 
94
139
  TS-only (browser-bundler concerns, no Rust equivalent): the `server`/`client`
95
- split with `clientPrefix`, the explicit `runtimeEnv` destructure, and the
96
- `NEXT_PUBLIC_*` client presets.
140
+ split with `clientPrefix`, the explicit `runtimeEnv` destructure, the `profile`
141
+ override, and the `NEXT_PUBLIC_*` client presets. Rust-only: `drift`, a backend
142
+ concern (a browser bundle has no environment to drift from).
97
143
 
98
144
  ## Limitations
99
145
 
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * ```
11
11
  */
12
12
  /** What went wrong with one variable. */
13
- type SettingsIssueKind = 'missing' | 'invalid';
13
+ type SettingsIssueKind = 'missing' | 'missing-in-profile' | 'invalid';
14
14
  /** One problem with one variable. */
15
15
  interface SettingsIssue {
16
16
  /** The env var name as it was looked up. */
@@ -20,6 +20,13 @@ interface SettingsIssue {
20
20
  readonly detail?: string;
21
21
  /** The offending raw value. Omitted for `secret(...)` settings. */
22
22
  readonly value?: string;
23
+ /** For `missing-in-profile` issues: the active profile that demanded it. */
24
+ readonly profile?: string;
25
+ /**
26
+ * Whether the profile came from {@link PROFILE_VAR} (so the message may name
27
+ * it) rather than from an explicit `profile` option.
28
+ */
29
+ readonly profileFromVar?: boolean;
23
30
  }
24
31
  /**
25
32
  * Thrown by {@link createSettings} when any variable is missing or invalid.
@@ -31,6 +38,63 @@ declare class SettingsError extends Error {
31
38
  constructor(issues: readonly SettingsIssue[]);
32
39
  }
33
40
 
41
+ /**
42
+ * Failing a boot on bad configuration, with an exit code that says so —
43
+ * the mirror of `ev_lib::settings::or_exit`.
44
+ */
45
+ /**
46
+ * `EX_CONFIG` from `sysexits.h`: the process died because its configuration is
47
+ * wrong, not because a dependency blipped. Restarting it unchanged cannot help
48
+ * — which is exactly what an operator (and a CrashLoopBackOff triage) needs to
49
+ * know.
50
+ */
51
+ declare const EX_CONFIG = 78;
52
+ /**
53
+ * Run a settings load; on {@link SettingsError} print every problem and exit
54
+ * {@link EX_CONFIG}. Any other error propagates untouched — a broken database
55
+ * is not a broken config.
56
+ *
57
+ * Node only, by construction: in a browser there is no exit code to set, so the
58
+ * error is rethrown and the caller decides.
59
+ *
60
+ * ```ts
61
+ * // instrumentation.ts — before the server accepts traffic
62
+ * export function register() {
63
+ * orExit(() => assertConfig());
64
+ * }
65
+ * ```
66
+ */
67
+ declare function orExit<T>(load: () => T): T;
68
+
69
+ /**
70
+ * The deployment profile — what {@link requiredIn} compares against, mirroring
71
+ * `ev_lib::settings::PROFILE_VAR` / `DEFAULT_PROFILE`.
72
+ */
73
+ /**
74
+ * The variable that names the deployment profile. Org-canonical (the same
75
+ * `APP_ENV` the Rust presets read), so "are we in production" is answered in
76
+ * one place across both stacks.
77
+ */
78
+ declare const PROFILE_VAR = "APP_ENV";
79
+ /**
80
+ * Assumed when {@link PROFILE_VAR} is unset — an unconfigured environment is a
81
+ * developer's laptop, not production.
82
+ */
83
+ declare const DEFAULT_PROFILE = "development";
84
+ /**
85
+ * Resolve the active profile from an explicit override, else the environment
86
+ * record, else {@link DEFAULT_PROFILE}. The empty string counts as unset, like
87
+ * every other variable.
88
+ *
89
+ * The override exists because Next.js deployments name their environment
90
+ * `NODE_ENV`, which the framework owns: `profile: process.env.NODE_ENV` keeps
91
+ * one source of truth instead of a second, drifting `APP_ENV`.
92
+ */
93
+ declare function activeProfile(runtimeEnv: Readonly<Record<string, string | undefined>>, override?: string): {
94
+ readonly profile: string;
95
+ readonly fromVar: boolean;
96
+ };
97
+
34
98
  /**
35
99
  * Validators — how a raw env string becomes a typed value, and the three
36
100
  * wrappers that mirror the Rust field grammar: {@link optional} ↔ `Option<T>`,
@@ -61,6 +125,11 @@ interface Validator<T> {
61
125
  readonly secret: boolean;
62
126
  /** Used when the variable is unset; parsed by the same rules as an env value. */
63
127
  readonly defaultLiteral: string | undefined;
128
+ /**
129
+ * Deployment profiles in which being unset is an error anyway — the mirror of
130
+ * `#[required_in(…)]`. Empty for every plain validator.
131
+ */
132
+ readonly requiredIn: readonly string[];
64
133
  /** Parse a raw (non-empty) env string; throws `Error(message)` when invalid. */
65
134
  readonly parse: (raw: string) => T;
66
135
  }
@@ -113,6 +182,25 @@ declare function optional<T>(validator: Validator<T>): Validator<T | undefined>;
113
182
  * `= "literal"` field default.
114
183
  */
115
184
  declare function withDefault<T>(validator: Validator<T>, literal: string): Validator<T>;
185
+ /**
186
+ * Escalate an {@link optional} or {@link withDefault} setting back to required
187
+ * in the named deployment profiles — the mirror of `#[required_in(…)]`. The
188
+ * active profile is `APP_ENV` (see `PROFILE_VAR`), unset meaning `development`.
189
+ *
190
+ * This is the fix for the settings whose absence is harmless on a laptop and
191
+ * invisible damage in production: an unset mailer host is a logged no-op, an
192
+ * unset DSN is silence where the alerts should be.
193
+ *
194
+ * ```ts
195
+ * SMTP_HOST: requiredIn(optional(str()), 'production'),
196
+ * // a dev-only default that must be stated explicitly once deployed
197
+ * PUBLIC_ORIGIN: requiredIn(withDefault(url(), 'http://localhost:3000'), 'production', 'staging'),
198
+ * ```
199
+ *
200
+ * Rejected by {@link createSettings} on a setting that is already required
201
+ * everywhere — there it would do nothing.
202
+ */
203
+ declare function requiredIn<T>(validator: Validator<T>, ...profiles: readonly string[]): Validator<T>;
116
204
  /**
117
205
  * Redact the raw value in error output — the mirror of `#[secret]`. Note that
118
206
  * unlike Rust (whose generated `Debug` prints `***`), JS has no debug-print
@@ -159,6 +247,15 @@ interface CreateSettingsOptions<S extends SettingsSchema, C extends SettingsSche
159
247
  readonly runtimeEnv: Readonly<Record<string, string | undefined>>;
160
248
  /** Contract: `VAR=` (empty string) behaves like unset. Default `true`. */
161
249
  readonly emptyStringAsUnset?: boolean;
250
+ /**
251
+ * The deployment profile `requiredIn(...)` settings are matched against.
252
+ * Default: `APP_ENV` from {@link runtimeEnv}, else `development`. Pass
253
+ * `process.env.NODE_ENV` in a Next.js app, where the framework already owns
254
+ * that name — and pass it explicitly whenever a `client` setting is
255
+ * `requiredIn(...)`, since the browser bundle carries no `APP_ENV` and would
256
+ * always resolve to `development`.
257
+ */
258
+ readonly profile?: string;
162
259
  /**
163
260
  * Where we are running. Default: `window` is absent from `globalThis`.
164
261
  * Override for workers or exotic runtimes.
@@ -251,4 +348,4 @@ declare const presets: {
251
348
  };
252
349
  };
253
350
 
254
- export { type CreateSettingsOptions, type InferSettings, SettingsError, type SettingsIssue, type SettingsIssueKind, type SettingsSchema, type Validator, bool, createSettings, int, list, num, oneOf, optional, port, presets, secret, str, url, withDefault };
351
+ export { type CreateSettingsOptions, DEFAULT_PROFILE, EX_CONFIG, type InferSettings, PROFILE_VAR, SettingsError, type SettingsIssue, type SettingsIssueKind, type SettingsSchema, type Validator, activeProfile, bool, createSettings, int, list, num, oneOf, optional, orExit, port, presets, requiredIn, secret, str, url, withDefault };
package/dist/index.js CHANGED
@@ -1,3 +1,17 @@
1
+ // src/profile.ts
2
+ var PROFILE_VAR = "APP_ENV";
3
+ var DEFAULT_PROFILE = "development";
4
+ function activeProfile(runtimeEnv, override) {
5
+ const overridden = override !== void 0 && override !== "";
6
+ const raw = overridden ? override : runtimeEnv[PROFILE_VAR];
7
+ return {
8
+ profile: raw === void 0 || raw === "" ? DEFAULT_PROFILE : raw,
9
+ // Whether an error may point at PROFILE_VAR: with an override in play,
10
+ // naming APP_ENV would send whoever reads it to the wrong variable.
11
+ fromVar: !overridden
12
+ };
13
+ }
14
+
1
15
  // src/error.ts
2
16
  var SettingsError = class extends Error {
3
17
  issues;
@@ -14,13 +28,31 @@ function formatIssues(issues) {
14
28
  }
15
29
  function formatIssue(issue) {
16
30
  if (issue.kind === "missing") return `${issue.key}: missing`;
31
+ if (issue.kind === "missing-in-profile") {
32
+ const because = issue.profileFromVar === false ? `the active profile is ${issue.profile}` : `${PROFILE_VAR}=${issue.profile}`;
33
+ return `${issue.key}: missing (required when ${because})`;
34
+ }
17
35
  const value = issue.value === void 0 ? "" : ` ${JSON.stringify(issue.value)}`;
18
36
  return `${issue.key}: invalid value${value}: ${issue.detail ?? "failed to parse"}`;
19
37
  }
20
38
 
39
+ // src/exit.ts
40
+ var EX_CONFIG = 78;
41
+ function orExit(load) {
42
+ try {
43
+ return load();
44
+ } catch (error) {
45
+ if (!(error instanceof SettingsError)) throw error;
46
+ const exit = globalThis.process?.exit;
47
+ if (typeof exit !== "function") throw error;
48
+ console.error(error.message);
49
+ return exit(EX_CONFIG);
50
+ }
51
+ }
52
+
21
53
  // src/validators.ts
22
54
  function base(kind, parse) {
23
- return { kind, optional: false, secret: false, defaultLiteral: void 0, parse };
55
+ return { kind, optional: false, secret: false, defaultLiteral: void 0, requiredIn: [], parse };
24
56
  }
25
57
  function messageOf(error) {
26
58
  return error instanceof Error ? error.message : String(error);
@@ -99,6 +131,9 @@ function optional(validator) {
99
131
  function withDefault(validator, literal) {
100
132
  return { ...validator, defaultLiteral: literal };
101
133
  }
134
+ function requiredIn(validator, ...profiles) {
135
+ return { ...validator, requiredIn: [...validator.requiredIn, ...profiles] };
136
+ }
102
137
  function secret(validator) {
103
138
  return { ...validator, secret: true };
104
139
  }
@@ -140,13 +175,23 @@ function createSettings(options) {
140
175
  `@evinvest/settings: setting "${key}" is both optional and defaulted \u2014 a defaulted setting is always present, drop one`
141
176
  );
142
177
  }
178
+ if (validator.requiredIn.length > 0 && !validator.optional && validator.defaultLiteral === void 0) {
179
+ throw new Error(
180
+ `@evinvest/settings: setting "${key}" is required everywhere, so requiredIn(...) does nothing \u2014 wrap it in optional(...) or withDefault(...)`
181
+ );
182
+ }
143
183
  }
144
184
  const active = isServer ? { ...server, ...client } : client;
185
+ const { profile, fromVar: profileFromVar } = activeProfile(options.runtimeEnv, options.profile);
145
186
  const issues = [];
146
187
  const values = {};
147
188
  for (const [key, validator] of Object.entries(active)) {
148
189
  let raw = options.runtimeEnv[key];
149
190
  if (emptyAsUnset && raw === "") raw = void 0;
191
+ if (raw === void 0 && validator.requiredIn.includes(profile)) {
192
+ issues.push({ key, kind: "missing-in-profile", profile, profileFromVar });
193
+ continue;
194
+ }
150
195
  const fromDefault = raw === void 0 && validator.defaultLiteral !== void 0;
151
196
  if (fromDefault) raw = validator.defaultLiteral;
152
197
  if (raw === void 0) {
@@ -223,7 +268,11 @@ var presets = {
223
268
  })
224
269
  };
225
270
  export {
271
+ DEFAULT_PROFILE,
272
+ EX_CONFIG,
273
+ PROFILE_VAR,
226
274
  SettingsError,
275
+ activeProfile,
227
276
  bool,
228
277
  createSettings,
229
278
  int,
@@ -231,8 +280,10 @@ export {
231
280
  num,
232
281
  oneOf,
233
282
  optional,
283
+ orExit,
234
284
  port,
235
285
  presets,
286
+ requiredIn,
236
287
  secret,
237
288
  str,
238
289
  url,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/error.ts","../src/validators.ts","../src/create-settings.ts","../src/presets.ts"],"sourcesContent":["/**\n * Aggregate settings failure — every missing/invalid variable found in one\n * pass, mirroring `SettingsError` of the Rust `settings` feature. The message\n * format is kept in step with the Rust `Display` impl:\n *\n * ```text\n * invalid settings (2 problems)\n * - DATABASE_URL: missing\n * - PORT: invalid value \"abc\": expected an integer\n * ```\n */\n\n/** What went wrong with one variable. */\nexport type SettingsIssueKind = 'missing' | 'invalid';\n\n/** One problem with one variable. */\nexport interface SettingsIssue {\n /** The env var name as it was looked up. */\n readonly key: string;\n readonly kind: SettingsIssueKind;\n /** For `invalid` issues: what went wrong while parsing. */\n readonly detail?: string;\n /** The offending raw value. Omitted for `secret(...)` settings. */\n readonly value?: string;\n}\n\n/**\n * Thrown by {@link createSettings} when any variable is missing or invalid.\n * Carries every problem at once — fix the whole list in one edit instead of\n * replaying the boot loop per variable.\n */\nexport class SettingsError extends Error {\n readonly issues: readonly SettingsIssue[];\n\n constructor(issues: readonly SettingsIssue[]) {\n super(formatIssues(issues));\n this.name = 'SettingsError';\n this.issues = issues;\n }\n}\n\nfunction formatIssues(issues: readonly SettingsIssue[]): string {\n const noun = issues.length === 1 ? 'problem' : 'problems';\n const lines = issues.map((issue) => ` - ${formatIssue(issue)}`);\n return [`invalid settings (${issues.length} ${noun})`, ...lines].join('\\n');\n}\n\nfunction formatIssue(issue: SettingsIssue): string {\n if (issue.kind === 'missing') return `${issue.key}: missing`;\n // JSON.stringify quotes/escapes like Rust's `{:?}` for printable values\n // (control characters escape differently — cosmetic, not part of the contract).\n const value = issue.value === undefined ? '' : ` ${JSON.stringify(issue.value)}`;\n return `${issue.key}: invalid value${value}: ${issue.detail ?? 'failed to parse'}`;\n}\n","/**\n * Validators — how a raw env string becomes a typed value, and the three\n * wrappers that mirror the Rust field grammar: {@link optional} ↔ `Option<T>`,\n * {@link withDefault} ↔ `= \"literal\"`, {@link secret} ↔ `#[secret]`.\n *\n * The shared Rust↔TS parsing contract lives here: `bool` accepts\n * `true`/`false`/`1`/`0` case-insensitively, {@link list} is comma-separated\n * with trimmed items and empty items dropped, and scalars are **not** trimmed —\n * `\" 8080\"` is invalid for a number on both sides.\n */\n\n/**\n * Parses one env value into a typed setting. Build one with {@link str},\n * {@link num}, {@link int}, {@link port}, {@link bool}, {@link url},\n * {@link list}, or {@link oneOf}, then refine it with {@link optional},\n * {@link withDefault}, and {@link secret}.\n *\n * The named validators are idiomatic TS and deliberately *not* a 1:1 copy of\n * the Rust side, where types parse themselves through the `FromEnvValue` trait\n * (`port: u16`, `bind: SocketAddr`); what both sides share is the semantics\n * listed in the module docs.\n */\nexport interface Validator<T> {\n /** Human name used in error messages (e.g. `number`). */\n readonly kind: string;\n /** Unset is fine and yields `undefined` — the mirror of `Option<T>`. */\n readonly optional: boolean;\n /** Redact the raw value in error output — the mirror of `#[secret]`. */\n readonly secret: boolean;\n /** Used when the variable is unset; parsed by the same rules as an env value. */\n readonly defaultLiteral: string | undefined;\n /** Parse a raw (non-empty) env string; throws `Error(message)` when invalid. */\n readonly parse: (raw: string) => T;\n}\n\nfunction base<T>(kind: string, parse: (raw: string) => T): Validator<T> {\n return { kind, optional: false, secret: false, defaultLiteral: undefined, parse };\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Any string, taken verbatim (no trimming). */\nexport function str(): Validator<string> {\n return base('string', (raw) => raw);\n}\n\n/** Contract: `true`/`1` and `false`/`0`, case-insensitive, no trimming. */\nexport function bool(): Validator<boolean> {\n return base('boolean', (raw) => {\n const lower = raw.toLowerCase();\n if (lower === 'true' || lower === '1') return true;\n if (lower === 'false' || lower === '0') return false;\n throw new Error('expected one of `true`, `false`, `1`, `0` (case-insensitive)');\n });\n}\n\n// Rust `f64::from_str` grammar minus `inf`/`NaN`: sign, digits with an\n// optional point (`5.`/`.5` are valid), optional exponent. Rejects the extra\n// JS `Number()` literals (`0x10`, `0b101`, `0o17`) so both sides agree.\nconst FLOAT_GRAMMAR = /^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$/;\nconst INT_GRAMMAR = /^[+-]?\\d+$/;\n\n/**\n * A finite decimal number (point/exponent forms included). Not trimmed:\n * `\" 3\"` is invalid, like Rust's `FromStr`. Divergence from the Rust mirror:\n * Rust `f64` also accepts `inf`/`NaN` — this validator requires finite.\n */\nexport function num(): Validator<number> {\n return base('number', (raw) => {\n if (!FLOAT_GRAMMAR.test(raw)) throw new Error('expected a finite number');\n const value = Number(raw);\n if (!Number.isFinite(value)) throw new Error('expected a finite number');\n return value;\n });\n}\n\n/**\n * A plain decimal integer (no exponent/point/hex forms — Rust integer\n * `FromStr` grammar), limited to the safe range `±(2^53 - 1)`: JS numbers are\n * doubles, so bigger values would silently round (Rust 64-bit integers parse\n * them exactly — use {@link str} for 64-bit ids).\n */\nexport function int(): Validator<number> {\n return base('integer', (raw) => {\n if (!INT_GRAMMAR.test(raw)) throw new Error('expected an integer');\n const value = Number(raw);\n if (!Number.isSafeInteger(value)) throw new Error('expected a safe integer (within ±(2^53 - 1))');\n return value;\n });\n}\n\n/** A TCP/UDP port: an integer in `1..=65535`. */\nexport function port(): Validator<number> {\n const inner = int();\n return base('port', (raw) => {\n const value = inner.parse(raw);\n if (value < 1 || value > 65535) throw new Error('expected a port (1-65535)');\n return value;\n });\n}\n\n/** An absolute URL (validated with `new URL`, returned as the original string). */\nexport function url(): Validator<string> {\n return base('url', (raw) => {\n // `new URL` silently strips whitespace/tabs/newlines; a padded value would\n // validate here and then break whatever consumes the raw string.\n if (raw === '' || /\\s/.test(raw)) throw new Error('expected an absolute URL');\n try {\n new URL(raw);\n } catch {\n throw new Error('expected an absolute URL');\n }\n return raw;\n });\n}\n\n/**\n * One of a fixed set of strings, narrowed to the literal union:\n *\n * ```ts\n * const env = oneOf(['development', 'production']); // Validator<'development' | 'production'>\n * ```\n */\nexport function oneOf<const V extends string>(values: readonly V[]): Validator<V> {\n const expected = `expected one of ${values.map((value) => `\\`${value}\\``).join(', ')}`;\n return base('choice', (raw) => {\n if ((values as readonly string[]).includes(raw)) return raw as V;\n throw new Error(expected);\n });\n}\n\n/**\n * Contract: split on `,`, trim each item, drop empty items, parse the rest\n * with `item` (default {@link str}). Item numbering in errors counts the kept\n * items from 1 and never leaks the item value, so secret lists stay\n * redactable. Wrap the *list* in {@link secret}/{@link optional}, not the item.\n */\nexport function list(): Validator<readonly string[]>;\nexport function list<T>(item: Validator<T>): Validator<readonly T[]>;\nexport function list<T>(item?: Validator<T>): Validator<readonly (T | string)[]> {\n const inner = item ?? str();\n return base('list', (raw) =>\n raw\n .split(',')\n .map((piece) => piece.trim())\n .filter((piece) => piece !== '')\n .map((piece, index) => {\n try {\n return inner.parse(piece);\n } catch (error) {\n throw new Error(`item ${index + 1}: ${messageOf(error)}`);\n }\n }),\n );\n}\n\n/**\n * Unset (or empty) is fine and yields `undefined` — the mirror of a Rust\n * `Option<T>` field. Cannot be combined with {@link withDefault}: a defaulted\n * setting is always present ({@link createSettings} rejects the combination).\n */\nexport function optional<T>(validator: Validator<T>): Validator<T | undefined> {\n return { ...validator, optional: true };\n}\n\n/**\n * Fall back to `literal` when the variable is unset. The literal goes through\n * the exact same parsing rules as an env value — the mirror of the Rust\n * `= \"literal\"` field default.\n */\nexport function withDefault<T>(validator: Validator<T>, literal: string): Validator<T> {\n return { ...validator, defaultLiteral: literal };\n}\n\n/**\n * Redact the raw value in error output — the mirror of `#[secret]`. Note that\n * unlike Rust (whose generated `Debug` prints `***`), JS has no debug-print\n * boundary: `console.log(settings.TOKEN)` prints the real value. The redaction\n * covers what the *library* emits: `SettingsError` messages and issues.\n */\nexport function secret<T>(validator: Validator<T>): Validator<T> {\n return { ...validator, secret: true };\n}\n","/**\n * {@link createSettings} — read a validated, typed settings object out of an\n * explicitly injected environment record, with a server/client split for\n * browser bundles.\n */\n\nimport { SettingsError, type SettingsIssue } from './error';\nimport type { Validator } from './validators';\n\n/** A settings declaration: env var name → {@link Validator}. */\nexport type SettingsSchema = Record<string, Validator<unknown>>;\n\n/** The typed settings object a schema produces. */\nexport type InferSettings<S extends SettingsSchema> = {\n readonly [K in keyof S]: S[K] extends Validator<infer T> ? T : never;\n};\n\nexport interface CreateSettingsOptions<S extends SettingsSchema, C extends SettingsSchema> {\n /** Server-only settings. Accessing one from client code throws. */\n readonly server?: S;\n /**\n * Client-exposable settings. Every key must start with {@link clientPrefix}\n * so nothing leaks into a browser bundle by accident.\n */\n readonly client?: C;\n /** Required when `client` is non-empty — e.g. `NEXT_PUBLIC_`. */\n readonly clientPrefix?: string;\n /**\n * The environment record, destructured **explicitly** — bundlers inline\n * `process.env.NEXT_PUBLIC_*` / `import.meta.env.*` at build time only when\n * each variable is a static member expression, so a bare `process.env` pass\n * works on the server but leaves client values undefined in the browser:\n *\n * ```ts\n * runtimeEnv: {\n * DATABASE_URL: process.env.DATABASE_URL,\n * NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,\n * }\n * ```\n */\n readonly runtimeEnv: Readonly<Record<string, string | undefined>>;\n /** Contract: `VAR=` (empty string) behaves like unset. Default `true`. */\n readonly emptyStringAsUnset?: boolean;\n /**\n * Where we are running. Default: `window` is absent from `globalThis`.\n * Override for workers or exotic runtimes.\n */\n readonly isServer?: boolean;\n}\n\n/**\n * Validate the injected environment against the schema and return the typed,\n * read-only settings object. Mirrors the Rust `settings!` macro's\n * `from_source`, plus the client/server split (a bundler concern with no Rust\n * equivalent).\n *\n * - Every missing/invalid variable is reported at once in one\n * {@link SettingsError} — no fix-one-reboot-fix-next loops.\n * - Validation runs eagerly, at the `createSettings` call. Call it at module\n * scope of a single `settings.ts` so a bad environment fails the boot (or,\n * in Next.js, the build — import your settings module from `next.config`).\n * - On the client only the `client` schema is validated (server values never\n * reach the bundle); accessing a server key there throws.\n *\n * @example\n * ```ts\n * export const settings = createSettings({\n * server: {\n * DATABASE_URL: url(),\n * SIGNING_KEY: secret(str()),\n * PORT: withDefault(port(), '8080'),\n * ...presets.posthog(),\n * },\n * clientPrefix: 'NEXT_PUBLIC_',\n * client: { ...presets.posthogClient() },\n * runtimeEnv: {\n * DATABASE_URL: process.env.DATABASE_URL,\n * SIGNING_KEY: process.env.SIGNING_KEY,\n * PORT: process.env.PORT,\n * POSTHOG_KEY: process.env.POSTHOG_KEY,\n * POSTHOG_HOST: process.env.POSTHOG_HOST,\n * NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,\n * NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,\n * },\n * });\n * ```\n */\nexport function createSettings<\n S extends SettingsSchema = Record<never, never>,\n C extends SettingsSchema = Record<never, never>,\n>(options: CreateSettingsOptions<S, C>): InferSettings<S> & InferSettings<C> {\n const server: SettingsSchema = options.server ?? {};\n const client: SettingsSchema = options.client ?? {};\n const { clientPrefix } = options;\n const emptyAsUnset = options.emptyStringAsUnset ?? true;\n const isServer = options.isServer ?? !('window' in globalThis);\n\n const serverKeys = Object.keys(server);\n const clientKeys = Object.keys(client);\n\n // Declaration bugs (not environment problems) fail fast as plain errors.\n if (clientKeys.length > 0 && clientPrefix === undefined) {\n throw new Error('@evinvest/settings: `clientPrefix` is required when `client` settings are declared');\n }\n for (const key of serverKeys) {\n // `in` would also match Object.prototype members (\"toString\", …).\n if (Object.hasOwn(client, key)) {\n throw new Error(`@evinvest/settings: setting \"${key}\" is declared in both server and client`);\n }\n }\n if (clientPrefix !== undefined) {\n for (const key of clientKeys) {\n if (!key.startsWith(clientPrefix)) {\n throw new Error(`@evinvest/settings: client setting \"${key}\" must start with clientPrefix \"${clientPrefix}\"`);\n }\n }\n for (const key of serverKeys) {\n if (key.startsWith(clientPrefix)) {\n throw new Error(\n `@evinvest/settings: server setting \"${key}\" starts with clientPrefix \"${clientPrefix}\" — declare it under \\`client\\``,\n );\n }\n }\n }\n for (const [key, validator] of [...Object.entries(server), ...Object.entries(client)]) {\n if (validator.optional && validator.defaultLiteral !== undefined) {\n throw new Error(\n `@evinvest/settings: setting \"${key}\" is both optional and defaulted — a defaulted setting is always present, drop one`,\n );\n }\n }\n\n // On the client, server values never reach the bundle: validate (and store)\n // the client schema only.\n const active = isServer ? { ...server, ...client } : client;\n const issues: SettingsIssue[] = [];\n const values: Record<string, unknown> = {};\n for (const [key, validator] of Object.entries(active)) {\n let raw = options.runtimeEnv[key];\n if (emptyAsUnset && raw === '') raw = undefined;\n // A default literal lives in source code, so it is never redacted.\n const fromDefault = raw === undefined && validator.defaultLiteral !== undefined;\n if (fromDefault) raw = validator.defaultLiteral;\n if (raw === undefined) {\n if (validator.optional) {\n values[key] = undefined;\n continue;\n }\n issues.push({ key, kind: 'missing' });\n continue;\n }\n try {\n values[key] = validator.parse(raw);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n issues.push({\n key,\n kind: 'invalid',\n detail: fromDefault ? `invalid default: ${message}` : message,\n ...(validator.secret && !fromDefault ? {} : { value: raw }),\n });\n }\n }\n if (issues.length > 0) throw new SettingsError(issues);\n\n Object.freeze(values); // the documented read-only contract, enforced (ESM is strict mode: writes throw)\n const serverOnly = new Set(serverKeys);\n return new Proxy(values, {\n get(target, prop, receiver) {\n if (typeof prop !== 'string') return Reflect.get(target, prop, receiver);\n if (!isServer && serverOnly.has(prop)) {\n throw new Error(`@evinvest/settings: attempted to access server-only setting \"${prop}\" on the client`);\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as InferSettings<S> & InferSettings<C>;\n}\n","/**\n * Org-canonical shared variable groups — one place that fixes the names, so\n * `POSTHOG_KEY` vs `POSTHOG_API_KEY` vs `NEXT_PUBLIC_POSTHOG_KEY` drift stops\n * at the source. Spread a preset into the `server` / `client` block of\n * {@link createSettings}; the Rust mirror ships the same server-side groups as\n * ready-made structs (`settings::presets`).\n */\n\nimport { optional, str, withDefault, type Validator } from './validators';\n\nexport const presets = {\n /**\n * PostHog capture credentials, canonical names: `POSTHOG_KEY` /\n * `POSTHOG_HOST`. Both optional — capture is simply off without them. The\n * project key (`phc_…`) is write-only and ships in frontend bundles anyway,\n * so it is not `secret(...)`.\n */\n posthog: (): { POSTHOG_KEY: Validator<string | undefined>; POSTHOG_HOST: Validator<string | undefined> } => ({\n POSTHOG_KEY: optional(str()),\n POSTHOG_HOST: optional(str()),\n }),\n\n /**\n * Sentry reporting, canonical name: `SENTRY_DSN`. Optional — monitoring is\n * off without it. A DSN authorises event *submission* only, so it is not\n * `secret(...)`.\n */\n sentry: (): { SENTRY_DSN: Validator<string | undefined> } => ({\n SENTRY_DSN: optional(str()),\n }),\n\n /**\n * The deployment environment, canonical name: `APP_ENV`. Defaults to\n * `development`; kept a free string on purpose — constraining the values\n * would break consumers that add a stage.\n */\n appEnv: (): { APP_ENV: Validator<string> } => ({\n APP_ENV: withDefault(str(), 'development'),\n }),\n\n /** The `client`-block variant of {@link presets.posthog} for Next.js bundles. */\n posthogClient: (): {\n NEXT_PUBLIC_POSTHOG_KEY: Validator<string | undefined>;\n NEXT_PUBLIC_POSTHOG_HOST: Validator<string | undefined>;\n } => ({\n NEXT_PUBLIC_POSTHOG_KEY: optional(str()),\n NEXT_PUBLIC_POSTHOG_HOST: optional(str()),\n }),\n\n /** The `client`-block variant of {@link presets.sentry} + `APP_ENV` for Next.js bundles. */\n sentryClient: (): {\n NEXT_PUBLIC_SENTRY_DSN: Validator<string | undefined>;\n NEXT_PUBLIC_APP_ENV: Validator<string>;\n } => ({\n NEXT_PUBLIC_SENTRY_DSN: optional(str()),\n NEXT_PUBLIC_APP_ENV: withDefault(str(), 'development'),\n }),\n} as const;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/profile.ts","../src/error.ts","../src/exit.ts","../src/validators.ts","../src/create-settings.ts","../src/presets.ts"],"sourcesContent":["/**\n * The deployment profile — what {@link requiredIn} compares against, mirroring\n * `ev_lib::settings::PROFILE_VAR` / `DEFAULT_PROFILE`.\n */\n\n/**\n * The variable that names the deployment profile. Org-canonical (the same\n * `APP_ENV` the Rust presets read), so \"are we in production\" is answered in\n * one place across both stacks.\n */\nexport const PROFILE_VAR = 'APP_ENV';\n\n/**\n * Assumed when {@link PROFILE_VAR} is unset — an unconfigured environment is a\n * developer's laptop, not production.\n */\nexport const DEFAULT_PROFILE = 'development';\n\n/**\n * Resolve the active profile from an explicit override, else the environment\n * record, else {@link DEFAULT_PROFILE}. The empty string counts as unset, like\n * every other variable.\n *\n * The override exists because Next.js deployments name their environment\n * `NODE_ENV`, which the framework owns: `profile: process.env.NODE_ENV` keeps\n * one source of truth instead of a second, drifting `APP_ENV`.\n */\nexport function activeProfile(\n runtimeEnv: Readonly<Record<string, string | undefined>>,\n override?: string,\n): { readonly profile: string; readonly fromVar: boolean } {\n const overridden = override !== undefined && override !== '';\n const raw = overridden ? override : runtimeEnv[PROFILE_VAR];\n return {\n profile: raw === undefined || raw === '' ? DEFAULT_PROFILE : raw,\n // Whether an error may point at PROFILE_VAR: with an override in play,\n // naming APP_ENV would send whoever reads it to the wrong variable.\n fromVar: !overridden,\n };\n}\n","/**\n * Aggregate settings failure — every missing/invalid variable found in one\n * pass, mirroring `SettingsError` of the Rust `settings` feature. The message\n * format is kept in step with the Rust `Display` impl:\n *\n * ```text\n * invalid settings (2 problems)\n * - DATABASE_URL: missing\n * - PORT: invalid value \"abc\": expected an integer\n * ```\n */\n\nimport { PROFILE_VAR } from './profile';\n\n/** What went wrong with one variable. */\nexport type SettingsIssueKind = 'missing' | 'missing-in-profile' | 'invalid';\n\n/** One problem with one variable. */\nexport interface SettingsIssue {\n /** The env var name as it was looked up. */\n readonly key: string;\n readonly kind: SettingsIssueKind;\n /** For `invalid` issues: what went wrong while parsing. */\n readonly detail?: string;\n /** The offending raw value. Omitted for `secret(...)` settings. */\n readonly value?: string;\n /** For `missing-in-profile` issues: the active profile that demanded it. */\n readonly profile?: string;\n /**\n * Whether the profile came from {@link PROFILE_VAR} (so the message may name\n * it) rather than from an explicit `profile` option.\n */\n readonly profileFromVar?: boolean;\n}\n\n/**\n * Thrown by {@link createSettings} when any variable is missing or invalid.\n * Carries every problem at once — fix the whole list in one edit instead of\n * replaying the boot loop per variable.\n */\nexport class SettingsError extends Error {\n readonly issues: readonly SettingsIssue[];\n\n constructor(issues: readonly SettingsIssue[]) {\n super(formatIssues(issues));\n this.name = 'SettingsError';\n this.issues = issues;\n }\n}\n\nfunction formatIssues(issues: readonly SettingsIssue[]): string {\n const noun = issues.length === 1 ? 'problem' : 'problems';\n const lines = issues.map((issue) => ` - ${formatIssue(issue)}`);\n return [`invalid settings (${issues.length} ${noun})`, ...lines].join('\\n');\n}\n\nfunction formatIssue(issue: SettingsIssue): string {\n if (issue.kind === 'missing') return `${issue.key}: missing`;\n if (issue.kind === 'missing-in-profile') {\n // Name the variable only when it is what decided the profile — the Rust\n // mirror always can, having no override.\n const because = issue.profileFromVar === false ? `the active profile is ${issue.profile}` : `${PROFILE_VAR}=${issue.profile}`;\n return `${issue.key}: missing (required when ${because})`;\n }\n // JSON.stringify quotes/escapes like Rust's `{:?}` for printable values\n // (control characters escape differently — cosmetic, not part of the contract).\n const value = issue.value === undefined ? '' : ` ${JSON.stringify(issue.value)}`;\n return `${issue.key}: invalid value${value}: ${issue.detail ?? 'failed to parse'}`;\n}\n","/**\n * Failing a boot on bad configuration, with an exit code that says so —\n * the mirror of `ev_lib::settings::or_exit`.\n */\n\nimport { SettingsError } from './error';\n\n/**\n * `EX_CONFIG` from `sysexits.h`: the process died because its configuration is\n * wrong, not because a dependency blipped. Restarting it unchanged cannot help\n * — which is exactly what an operator (and a CrashLoopBackOff triage) needs to\n * know.\n */\nexport const EX_CONFIG = 78;\n\n/**\n * Run a settings load; on {@link SettingsError} print every problem and exit\n * {@link EX_CONFIG}. Any other error propagates untouched — a broken database\n * is not a broken config.\n *\n * Node only, by construction: in a browser there is no exit code to set, so the\n * error is rethrown and the caller decides.\n *\n * ```ts\n * // instrumentation.ts — before the server accepts traffic\n * export function register() {\n * orExit(() => assertConfig());\n * }\n * ```\n */\nexport function orExit<T>(load: () => T): T {\n try {\n return load();\n } catch (error) {\n if (!(error instanceof SettingsError)) throw error;\n const exit = (globalThis as { process?: { exit?: (code: number) => never } }).process?.exit;\n if (typeof exit !== 'function') throw error;\n console.error(error.message);\n return exit(EX_CONFIG); // `never`, so it satisfies T without pretending to produce one\n }\n}\n","/**\n * Validators — how a raw env string becomes a typed value, and the three\n * wrappers that mirror the Rust field grammar: {@link optional} ↔ `Option<T>`,\n * {@link withDefault} ↔ `= \"literal\"`, {@link secret} ↔ `#[secret]`.\n *\n * The shared Rust↔TS parsing contract lives here: `bool` accepts\n * `true`/`false`/`1`/`0` case-insensitively, {@link list} is comma-separated\n * with trimmed items and empty items dropped, and scalars are **not** trimmed —\n * `\" 8080\"` is invalid for a number on both sides.\n */\n\n/**\n * Parses one env value into a typed setting. Build one with {@link str},\n * {@link num}, {@link int}, {@link port}, {@link bool}, {@link url},\n * {@link list}, or {@link oneOf}, then refine it with {@link optional},\n * {@link withDefault}, and {@link secret}.\n *\n * The named validators are idiomatic TS and deliberately *not* a 1:1 copy of\n * the Rust side, where types parse themselves through the `FromEnvValue` trait\n * (`port: u16`, `bind: SocketAddr`); what both sides share is the semantics\n * listed in the module docs.\n */\nexport interface Validator<T> {\n /** Human name used in error messages (e.g. `number`). */\n readonly kind: string;\n /** Unset is fine and yields `undefined` — the mirror of `Option<T>`. */\n readonly optional: boolean;\n /** Redact the raw value in error output — the mirror of `#[secret]`. */\n readonly secret: boolean;\n /** Used when the variable is unset; parsed by the same rules as an env value. */\n readonly defaultLiteral: string | undefined;\n /**\n * Deployment profiles in which being unset is an error anyway — the mirror of\n * `#[required_in(…)]`. Empty for every plain validator.\n */\n readonly requiredIn: readonly string[];\n /** Parse a raw (non-empty) env string; throws `Error(message)` when invalid. */\n readonly parse: (raw: string) => T;\n}\n\nfunction base<T>(kind: string, parse: (raw: string) => T): Validator<T> {\n return { kind, optional: false, secret: false, defaultLiteral: undefined, requiredIn: [], parse };\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Any string, taken verbatim (no trimming). */\nexport function str(): Validator<string> {\n return base('string', (raw) => raw);\n}\n\n/** Contract: `true`/`1` and `false`/`0`, case-insensitive, no trimming. */\nexport function bool(): Validator<boolean> {\n return base('boolean', (raw) => {\n const lower = raw.toLowerCase();\n if (lower === 'true' || lower === '1') return true;\n if (lower === 'false' || lower === '0') return false;\n throw new Error('expected one of `true`, `false`, `1`, `0` (case-insensitive)');\n });\n}\n\n// Rust `f64::from_str` grammar minus `inf`/`NaN`: sign, digits with an\n// optional point (`5.`/`.5` are valid), optional exponent. Rejects the extra\n// JS `Number()` literals (`0x10`, `0b101`, `0o17`) so both sides agree.\nconst FLOAT_GRAMMAR = /^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$/;\nconst INT_GRAMMAR = /^[+-]?\\d+$/;\n\n/**\n * A finite decimal number (point/exponent forms included). Not trimmed:\n * `\" 3\"` is invalid, like Rust's `FromStr`. Divergence from the Rust mirror:\n * Rust `f64` also accepts `inf`/`NaN` — this validator requires finite.\n */\nexport function num(): Validator<number> {\n return base('number', (raw) => {\n if (!FLOAT_GRAMMAR.test(raw)) throw new Error('expected a finite number');\n const value = Number(raw);\n if (!Number.isFinite(value)) throw new Error('expected a finite number');\n return value;\n });\n}\n\n/**\n * A plain decimal integer (no exponent/point/hex forms — Rust integer\n * `FromStr` grammar), limited to the safe range `±(2^53 - 1)`: JS numbers are\n * doubles, so bigger values would silently round (Rust 64-bit integers parse\n * them exactly — use {@link str} for 64-bit ids).\n */\nexport function int(): Validator<number> {\n return base('integer', (raw) => {\n if (!INT_GRAMMAR.test(raw)) throw new Error('expected an integer');\n const value = Number(raw);\n if (!Number.isSafeInteger(value)) throw new Error('expected a safe integer (within ±(2^53 - 1))');\n return value;\n });\n}\n\n/** A TCP/UDP port: an integer in `1..=65535`. */\nexport function port(): Validator<number> {\n const inner = int();\n return base('port', (raw) => {\n const value = inner.parse(raw);\n if (value < 1 || value > 65535) throw new Error('expected a port (1-65535)');\n return value;\n });\n}\n\n/** An absolute URL (validated with `new URL`, returned as the original string). */\nexport function url(): Validator<string> {\n return base('url', (raw) => {\n // `new URL` silently strips whitespace/tabs/newlines; a padded value would\n // validate here and then break whatever consumes the raw string.\n if (raw === '' || /\\s/.test(raw)) throw new Error('expected an absolute URL');\n try {\n new URL(raw);\n } catch {\n throw new Error('expected an absolute URL');\n }\n return raw;\n });\n}\n\n/**\n * One of a fixed set of strings, narrowed to the literal union:\n *\n * ```ts\n * const env = oneOf(['development', 'production']); // Validator<'development' | 'production'>\n * ```\n */\nexport function oneOf<const V extends string>(values: readonly V[]): Validator<V> {\n const expected = `expected one of ${values.map((value) => `\\`${value}\\``).join(', ')}`;\n return base('choice', (raw) => {\n if ((values as readonly string[]).includes(raw)) return raw as V;\n throw new Error(expected);\n });\n}\n\n/**\n * Contract: split on `,`, trim each item, drop empty items, parse the rest\n * with `item` (default {@link str}). Item numbering in errors counts the kept\n * items from 1 and never leaks the item value, so secret lists stay\n * redactable. Wrap the *list* in {@link secret}/{@link optional}, not the item.\n */\nexport function list(): Validator<readonly string[]>;\nexport function list<T>(item: Validator<T>): Validator<readonly T[]>;\nexport function list<T>(item?: Validator<T>): Validator<readonly (T | string)[]> {\n const inner = item ?? str();\n return base('list', (raw) =>\n raw\n .split(',')\n .map((piece) => piece.trim())\n .filter((piece) => piece !== '')\n .map((piece, index) => {\n try {\n return inner.parse(piece);\n } catch (error) {\n throw new Error(`item ${index + 1}: ${messageOf(error)}`);\n }\n }),\n );\n}\n\n/**\n * Unset (or empty) is fine and yields `undefined` — the mirror of a Rust\n * `Option<T>` field. Cannot be combined with {@link withDefault}: a defaulted\n * setting is always present ({@link createSettings} rejects the combination).\n */\nexport function optional<T>(validator: Validator<T>): Validator<T | undefined> {\n return { ...validator, optional: true };\n}\n\n/**\n * Fall back to `literal` when the variable is unset. The literal goes through\n * the exact same parsing rules as an env value — the mirror of the Rust\n * `= \"literal\"` field default.\n */\nexport function withDefault<T>(validator: Validator<T>, literal: string): Validator<T> {\n return { ...validator, defaultLiteral: literal };\n}\n\n/**\n * Escalate an {@link optional} or {@link withDefault} setting back to required\n * in the named deployment profiles — the mirror of `#[required_in(…)]`. The\n * active profile is `APP_ENV` (see `PROFILE_VAR`), unset meaning `development`.\n *\n * This is the fix for the settings whose absence is harmless on a laptop and\n * invisible damage in production: an unset mailer host is a logged no-op, an\n * unset DSN is silence where the alerts should be.\n *\n * ```ts\n * SMTP_HOST: requiredIn(optional(str()), 'production'),\n * // a dev-only default that must be stated explicitly once deployed\n * PUBLIC_ORIGIN: requiredIn(withDefault(url(), 'http://localhost:3000'), 'production', 'staging'),\n * ```\n *\n * Rejected by {@link createSettings} on a setting that is already required\n * everywhere — there it would do nothing.\n */\nexport function requiredIn<T>(validator: Validator<T>, ...profiles: readonly string[]): Validator<T> {\n return { ...validator, requiredIn: [...validator.requiredIn, ...profiles] };\n}\n\n/**\n * Redact the raw value in error output — the mirror of `#[secret]`. Note that\n * unlike Rust (whose generated `Debug` prints `***`), JS has no debug-print\n * boundary: `console.log(settings.TOKEN)` prints the real value. The redaction\n * covers what the *library* emits: `SettingsError` messages and issues.\n */\nexport function secret<T>(validator: Validator<T>): Validator<T> {\n return { ...validator, secret: true };\n}\n","/**\n * {@link createSettings} — read a validated, typed settings object out of an\n * explicitly injected environment record, with a server/client split for\n * browser bundles.\n */\n\nimport { SettingsError, type SettingsIssue } from './error';\nimport { activeProfile } from './profile';\nimport type { Validator } from './validators';\n\n/** A settings declaration: env var name → {@link Validator}. */\nexport type SettingsSchema = Record<string, Validator<unknown>>;\n\n/** The typed settings object a schema produces. */\nexport type InferSettings<S extends SettingsSchema> = {\n readonly [K in keyof S]: S[K] extends Validator<infer T> ? T : never;\n};\n\nexport interface CreateSettingsOptions<S extends SettingsSchema, C extends SettingsSchema> {\n /** Server-only settings. Accessing one from client code throws. */\n readonly server?: S;\n /**\n * Client-exposable settings. Every key must start with {@link clientPrefix}\n * so nothing leaks into a browser bundle by accident.\n */\n readonly client?: C;\n /** Required when `client` is non-empty — e.g. `NEXT_PUBLIC_`. */\n readonly clientPrefix?: string;\n /**\n * The environment record, destructured **explicitly** — bundlers inline\n * `process.env.NEXT_PUBLIC_*` / `import.meta.env.*` at build time only when\n * each variable is a static member expression, so a bare `process.env` pass\n * works on the server but leaves client values undefined in the browser:\n *\n * ```ts\n * runtimeEnv: {\n * DATABASE_URL: process.env.DATABASE_URL,\n * NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,\n * }\n * ```\n */\n readonly runtimeEnv: Readonly<Record<string, string | undefined>>;\n /** Contract: `VAR=` (empty string) behaves like unset. Default `true`. */\n readonly emptyStringAsUnset?: boolean;\n /**\n * The deployment profile `requiredIn(...)` settings are matched against.\n * Default: `APP_ENV` from {@link runtimeEnv}, else `development`. Pass\n * `process.env.NODE_ENV` in a Next.js app, where the framework already owns\n * that name — and pass it explicitly whenever a `client` setting is\n * `requiredIn(...)`, since the browser bundle carries no `APP_ENV` and would\n * always resolve to `development`.\n */\n readonly profile?: string;\n /**\n * Where we are running. Default: `window` is absent from `globalThis`.\n * Override for workers or exotic runtimes.\n */\n readonly isServer?: boolean;\n}\n\n/**\n * Validate the injected environment against the schema and return the typed,\n * read-only settings object. Mirrors the Rust `settings!` macro's\n * `from_source`, plus the client/server split (a bundler concern with no Rust\n * equivalent).\n *\n * - Every missing/invalid variable is reported at once in one\n * {@link SettingsError} — no fix-one-reboot-fix-next loops.\n * - Validation runs eagerly, at the `createSettings` call. Call it at module\n * scope of a single `settings.ts` so a bad environment fails the boot (or,\n * in Next.js, the build — import your settings module from `next.config`).\n * - On the client only the `client` schema is validated (server values never\n * reach the bundle); accessing a server key there throws.\n *\n * @example\n * ```ts\n * export const settings = createSettings({\n * server: {\n * DATABASE_URL: url(),\n * SIGNING_KEY: secret(str()),\n * PORT: withDefault(port(), '8080'),\n * ...presets.posthog(),\n * },\n * clientPrefix: 'NEXT_PUBLIC_',\n * client: { ...presets.posthogClient() },\n * runtimeEnv: {\n * DATABASE_URL: process.env.DATABASE_URL,\n * SIGNING_KEY: process.env.SIGNING_KEY,\n * PORT: process.env.PORT,\n * POSTHOG_KEY: process.env.POSTHOG_KEY,\n * POSTHOG_HOST: process.env.POSTHOG_HOST,\n * NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,\n * NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,\n * },\n * });\n * ```\n */\nexport function createSettings<\n S extends SettingsSchema = Record<never, never>,\n C extends SettingsSchema = Record<never, never>,\n>(options: CreateSettingsOptions<S, C>): InferSettings<S> & InferSettings<C> {\n const server: SettingsSchema = options.server ?? {};\n const client: SettingsSchema = options.client ?? {};\n const { clientPrefix } = options;\n const emptyAsUnset = options.emptyStringAsUnset ?? true;\n const isServer = options.isServer ?? !('window' in globalThis);\n\n const serverKeys = Object.keys(server);\n const clientKeys = Object.keys(client);\n\n // Declaration bugs (not environment problems) fail fast as plain errors.\n if (clientKeys.length > 0 && clientPrefix === undefined) {\n throw new Error('@evinvest/settings: `clientPrefix` is required when `client` settings are declared');\n }\n for (const key of serverKeys) {\n // `in` would also match Object.prototype members (\"toString\", …).\n if (Object.hasOwn(client, key)) {\n throw new Error(`@evinvest/settings: setting \"${key}\" is declared in both server and client`);\n }\n }\n if (clientPrefix !== undefined) {\n for (const key of clientKeys) {\n if (!key.startsWith(clientPrefix)) {\n throw new Error(`@evinvest/settings: client setting \"${key}\" must start with clientPrefix \"${clientPrefix}\"`);\n }\n }\n for (const key of serverKeys) {\n if (key.startsWith(clientPrefix)) {\n throw new Error(\n `@evinvest/settings: server setting \"${key}\" starts with clientPrefix \"${clientPrefix}\" — declare it under \\`client\\``,\n );\n }\n }\n }\n for (const [key, validator] of [...Object.entries(server), ...Object.entries(client)]) {\n if (validator.optional && validator.defaultLiteral !== undefined) {\n throw new Error(\n `@evinvest/settings: setting \"${key}\" is both optional and defaulted — a defaulted setting is always present, drop one`,\n );\n }\n // Mirrors the Rust `#[required_in]`-on-a-required-field compile error.\n if (validator.requiredIn.length > 0 && !validator.optional && validator.defaultLiteral === undefined) {\n throw new Error(\n `@evinvest/settings: setting \"${key}\" is required everywhere, so requiredIn(...) does nothing — wrap it in optional(...) or withDefault(...)`,\n );\n }\n }\n\n // On the client, server values never reach the bundle: validate (and store)\n // the client schema only.\n const active = isServer ? { ...server, ...client } : client;\n const { profile, fromVar: profileFromVar } = activeProfile(options.runtimeEnv, options.profile);\n const issues: SettingsIssue[] = [];\n const values: Record<string, unknown> = {};\n for (const [key, validator] of Object.entries(active)) {\n let raw = options.runtimeEnv[key];\n if (emptyAsUnset && raw === '') raw = undefined;\n // Checked before the default is applied: in a profile that demands the\n // value, a dev-shaped default is exactly what must not silently stand in.\n if (raw === undefined && validator.requiredIn.includes(profile)) {\n issues.push({ key, kind: 'missing-in-profile', profile, profileFromVar });\n continue;\n }\n // A default literal lives in source code, so it is never redacted.\n const fromDefault = raw === undefined && validator.defaultLiteral !== undefined;\n if (fromDefault) raw = validator.defaultLiteral;\n if (raw === undefined) {\n if (validator.optional) {\n values[key] = undefined;\n continue;\n }\n issues.push({ key, kind: 'missing' });\n continue;\n }\n try {\n values[key] = validator.parse(raw);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n issues.push({\n key,\n kind: 'invalid',\n detail: fromDefault ? `invalid default: ${message}` : message,\n ...(validator.secret && !fromDefault ? {} : { value: raw }),\n });\n }\n }\n if (issues.length > 0) throw new SettingsError(issues);\n\n Object.freeze(values); // the documented read-only contract, enforced (ESM is strict mode: writes throw)\n const serverOnly = new Set(serverKeys);\n return new Proxy(values, {\n get(target, prop, receiver) {\n if (typeof prop !== 'string') return Reflect.get(target, prop, receiver);\n if (!isServer && serverOnly.has(prop)) {\n throw new Error(`@evinvest/settings: attempted to access server-only setting \"${prop}\" on the client`);\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as InferSettings<S> & InferSettings<C>;\n}\n","/**\n * Org-canonical shared variable groups — one place that fixes the names, so\n * `POSTHOG_KEY` vs `POSTHOG_API_KEY` vs `NEXT_PUBLIC_POSTHOG_KEY` drift stops\n * at the source. Spread a preset into the `server` / `client` block of\n * {@link createSettings}; the Rust mirror ships the same server-side groups as\n * ready-made structs (`settings::presets`).\n */\n\nimport { optional, str, withDefault, type Validator } from './validators';\n\nexport const presets = {\n /**\n * PostHog capture credentials, canonical names: `POSTHOG_KEY` /\n * `POSTHOG_HOST`. Both optional — capture is simply off without them. The\n * project key (`phc_…`) is write-only and ships in frontend bundles anyway,\n * so it is not `secret(...)`.\n */\n posthog: (): { POSTHOG_KEY: Validator<string | undefined>; POSTHOG_HOST: Validator<string | undefined> } => ({\n POSTHOG_KEY: optional(str()),\n POSTHOG_HOST: optional(str()),\n }),\n\n /**\n * Sentry reporting, canonical name: `SENTRY_DSN`. Optional — monitoring is\n * off without it. A DSN authorises event *submission* only, so it is not\n * `secret(...)`.\n */\n sentry: (): { SENTRY_DSN: Validator<string | undefined> } => ({\n SENTRY_DSN: optional(str()),\n }),\n\n /**\n * The deployment environment, canonical name: `APP_ENV`. Defaults to\n * `development`; kept a free string on purpose — constraining the values\n * would break consumers that add a stage.\n */\n appEnv: (): { APP_ENV: Validator<string> } => ({\n APP_ENV: withDefault(str(), 'development'),\n }),\n\n /** The `client`-block variant of {@link presets.posthog} for Next.js bundles. */\n posthogClient: (): {\n NEXT_PUBLIC_POSTHOG_KEY: Validator<string | undefined>;\n NEXT_PUBLIC_POSTHOG_HOST: Validator<string | undefined>;\n } => ({\n NEXT_PUBLIC_POSTHOG_KEY: optional(str()),\n NEXT_PUBLIC_POSTHOG_HOST: optional(str()),\n }),\n\n /** The `client`-block variant of {@link presets.sentry} + `APP_ENV` for Next.js bundles. */\n sentryClient: (): {\n NEXT_PUBLIC_SENTRY_DSN: Validator<string | undefined>;\n NEXT_PUBLIC_APP_ENV: Validator<string>;\n } => ({\n NEXT_PUBLIC_SENTRY_DSN: optional(str()),\n NEXT_PUBLIC_APP_ENV: withDefault(str(), 'development'),\n }),\n} as const;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evinvest/settings",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Typed, validated env settings with aggregate error reporting and a server/client split — zero runtime deps, mirroring the settings Cargo feature. Secrets stay in sops at the shell/CI boundary; this package only reads the already-injected environment.",
5
5
  "license": "BlueOak-1.0.0",
6
6
  "publishConfig": {