@evinvest/settings 0.1.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 ADDED
@@ -0,0 +1,114 @@
1
+ # `@evinvest/settings`
2
+
3
+ Typed, validated env settings with aggregate error reporting and a
4
+ server/client split — **zero runtime dependencies**, one server-safe,
5
+ browser-safe ESM entry. The TypeScript mirror of the `settings` Cargo feature
6
+ of [`ev_lib`](../../rust/src/settings).
7
+
8
+ > This package reads **environment variables only** — no config files, no hot
9
+ > reload, and no decryption: secrets management stays at the shell/CI boundary
10
+ > (sops + age). See the [GUIDE](./GUIDE.md) and the Rust
11
+ > [GUIDE](../../rust/src/settings/GUIDE.md#secrets-the-sops-boundary) for the
12
+ > full sops workflow.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm i @evinvest/settings
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ One `settings.ts` per app, evaluated at module scope so a bad environment fails
23
+ the boot — and, imported from `next.config.*`, the build:
24
+
25
+ ```ts
26
+ import { createSettings, list, port, presets, secret, str, url, withDefault } from '@evinvest/settings';
27
+
28
+ export const settings = createSettings({
29
+ server: {
30
+ SESSION_REDIS_URL: url(),
31
+ PORT: withDefault(port(), '8080'),
32
+ BANKING_ISSUANCE_TOKEN: secret(str()),
33
+ MFE_ALLOWED_ORIGINS: withDefault(list(), ''),
34
+ ...presets.posthog(), // POSTHOG_KEY / POSTHOG_HOST, canonical names
35
+ },
36
+ clientPrefix: 'NEXT_PUBLIC_',
37
+ client: {
38
+ ...presets.posthogClient(), // NEXT_PUBLIC_POSTHOG_KEY / _HOST
39
+ },
40
+ // explicit destructure: bundlers inline NEXT_PUBLIC_* / import.meta.env.*
41
+ // only for static member expressions
42
+ runtimeEnv: {
43
+ SESSION_REDIS_URL: process.env.SESSION_REDIS_URL,
44
+ PORT: process.env.PORT,
45
+ BANKING_ISSUANCE_TOKEN: process.env.BANKING_ISSUANCE_TOKEN,
46
+ MFE_ALLOWED_ORIGINS: process.env.MFE_ALLOWED_ORIGINS,
47
+ POSTHOG_KEY: process.env.POSTHOG_KEY,
48
+ POSTHOG_HOST: process.env.POSTHOG_HOST,
49
+ NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
50
+ NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
51
+ },
52
+ });
53
+
54
+ settings.PORT; // number — typed, validated
55
+ settings.POSTHOG_KEY; // string | undefined (optional)
56
+ // on the client: settings.SESSION_REDIS_URL throws (server-only)
57
+ ```
58
+
59
+ A missing/invalid environment throws one `SettingsError` listing **every**
60
+ problem:
61
+
62
+ ```text
63
+ invalid settings (2 problems)
64
+ - SESSION_REDIS_URL: missing
65
+ - PORT: invalid value "banana": expected a finite number
66
+ ```
67
+
68
+ Validators: `str`, `num`, `int`, `port`, `bool`, `url`, `list`, `oneOf` —
69
+ refined by `optional(v)`, `withDefault(v, 'literal')`, `secret(v)`.
70
+
71
+ ## Rust ↔ TS parity
72
+
73
+ The Rust crate is the source of truth; this package preserves its *semantics*.
74
+ The full mapping table lives in the Rust
75
+ [README](../../rust/src/settings/README.md#rust--ts-parity); the load-bearing
76
+ shared rules:
77
+
78
+ - var names are written-out SCREAMING_SNAKE keys; required by default;
79
+ `optional(v)` ↔ `Option<T>`; `withDefault(v, lit)` ↔ `= "lit"` (the literal
80
+ parses by the same rules, only when unset).
81
+ - the **empty string is unset**; `bool` accepts `true`/`false`/`1`/`0`
82
+ case-insensitively; `list` splits on `,`, trims items, drops empty items;
83
+ scalars are **not** trimmed; number grammar matches Rust `FromStr` (`int`:
84
+ plain decimal; `num`: decimal/point/exponent, no hex). Documented
85
+ divergences: JS numbers are doubles, so `num()` requires finite values
86
+ (Rust `f64` also accepts `inf`/`NaN`) and `int()` stops at the safe range
87
+ `±(2^53 - 1)` — use `str()` for 64-bit ids.
88
+ - errors aggregate into one `SettingsError` (message shape shared with the
89
+ Rust `Display` impl); `secret(v)` redacts values in error output.
90
+ - the contract is pinned by mirrored vectors:
91
+ [`test/contract.node.test.ts`](./test/contract.node.test.ts) ↔
92
+ `rust/src/settings/tests.rs` (`mod contract`). Change both sides or neither.
93
+
94
+ 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.
97
+
98
+ ## Limitations
99
+
100
+ - **Env-only, flat.** No config files, no `__` nesting — by design.
101
+ - **`runtimeEnv` must destructure explicitly** for client vars: bundlers
102
+ (Next.js `NEXT_PUBLIC_*`, Vite `import.meta.env`) inline only static member
103
+ expressions at build time.
104
+ - **`secret(v)` redacts what the library emits** (errors/issues). JS has no
105
+ `Debug` boundary — `console.log(settings.TOKEN)` still prints the value
106
+ (unlike Rust, where the generated `Debug` prints `***`).
107
+ - **Worker runtimes:** the default server detection is "no `window` in
108
+ `globalThis`" — pass `isServer` explicitly in web workers.
109
+
110
+ ## Develop
111
+
112
+ ```sh
113
+ npm run typecheck && npm test && npm run build
114
+ ```
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Aggregate settings failure — every missing/invalid variable found in one
3
+ * pass, mirroring `SettingsError` of the Rust `settings` feature. The message
4
+ * format is kept in step with the Rust `Display` impl:
5
+ *
6
+ * ```text
7
+ * invalid settings (2 problems)
8
+ * - DATABASE_URL: missing
9
+ * - PORT: invalid value "abc": expected an integer
10
+ * ```
11
+ */
12
+ /** What went wrong with one variable. */
13
+ type SettingsIssueKind = 'missing' | 'invalid';
14
+ /** One problem with one variable. */
15
+ interface SettingsIssue {
16
+ /** The env var name as it was looked up. */
17
+ readonly key: string;
18
+ readonly kind: SettingsIssueKind;
19
+ /** For `invalid` issues: what went wrong while parsing. */
20
+ readonly detail?: string;
21
+ /** The offending raw value. Omitted for `secret(...)` settings. */
22
+ readonly value?: string;
23
+ }
24
+ /**
25
+ * Thrown by {@link createSettings} when any variable is missing or invalid.
26
+ * Carries every problem at once — fix the whole list in one edit instead of
27
+ * replaying the boot loop per variable.
28
+ */
29
+ declare class SettingsError extends Error {
30
+ readonly issues: readonly SettingsIssue[];
31
+ constructor(issues: readonly SettingsIssue[]);
32
+ }
33
+
34
+ /**
35
+ * Validators — how a raw env string becomes a typed value, and the three
36
+ * wrappers that mirror the Rust field grammar: {@link optional} ↔ `Option<T>`,
37
+ * {@link withDefault} ↔ `= "literal"`, {@link secret} ↔ `#[secret]`.
38
+ *
39
+ * The shared Rust↔TS parsing contract lives here: `bool` accepts
40
+ * `true`/`false`/`1`/`0` case-insensitively, {@link list} is comma-separated
41
+ * with trimmed items and empty items dropped, and scalars are **not** trimmed —
42
+ * `" 8080"` is invalid for a number on both sides.
43
+ */
44
+ /**
45
+ * Parses one env value into a typed setting. Build one with {@link str},
46
+ * {@link num}, {@link int}, {@link port}, {@link bool}, {@link url},
47
+ * {@link list}, or {@link oneOf}, then refine it with {@link optional},
48
+ * {@link withDefault}, and {@link secret}.
49
+ *
50
+ * The named validators are idiomatic TS and deliberately *not* a 1:1 copy of
51
+ * the Rust side, where types parse themselves through the `FromEnvValue` trait
52
+ * (`port: u16`, `bind: SocketAddr`); what both sides share is the semantics
53
+ * listed in the module docs.
54
+ */
55
+ interface Validator<T> {
56
+ /** Human name used in error messages (e.g. `number`). */
57
+ readonly kind: string;
58
+ /** Unset is fine and yields `undefined` — the mirror of `Option<T>`. */
59
+ readonly optional: boolean;
60
+ /** Redact the raw value in error output — the mirror of `#[secret]`. */
61
+ readonly secret: boolean;
62
+ /** Used when the variable is unset; parsed by the same rules as an env value. */
63
+ readonly defaultLiteral: string | undefined;
64
+ /** Parse a raw (non-empty) env string; throws `Error(message)` when invalid. */
65
+ readonly parse: (raw: string) => T;
66
+ }
67
+ /** Any string, taken verbatim (no trimming). */
68
+ declare function str(): Validator<string>;
69
+ /** Contract: `true`/`1` and `false`/`0`, case-insensitive, no trimming. */
70
+ declare function bool(): Validator<boolean>;
71
+ /**
72
+ * A finite decimal number (point/exponent forms included). Not trimmed:
73
+ * `" 3"` is invalid, like Rust's `FromStr`. Divergence from the Rust mirror:
74
+ * Rust `f64` also accepts `inf`/`NaN` — this validator requires finite.
75
+ */
76
+ declare function num(): Validator<number>;
77
+ /**
78
+ * A plain decimal integer (no exponent/point/hex forms — Rust integer
79
+ * `FromStr` grammar), limited to the safe range `±(2^53 - 1)`: JS numbers are
80
+ * doubles, so bigger values would silently round (Rust 64-bit integers parse
81
+ * them exactly — use {@link str} for 64-bit ids).
82
+ */
83
+ declare function int(): Validator<number>;
84
+ /** A TCP/UDP port: an integer in `1..=65535`. */
85
+ declare function port(): Validator<number>;
86
+ /** An absolute URL (validated with `new URL`, returned as the original string). */
87
+ declare function url(): Validator<string>;
88
+ /**
89
+ * One of a fixed set of strings, narrowed to the literal union:
90
+ *
91
+ * ```ts
92
+ * const env = oneOf(['development', 'production']); // Validator<'development' | 'production'>
93
+ * ```
94
+ */
95
+ declare function oneOf<const V extends string>(values: readonly V[]): Validator<V>;
96
+ /**
97
+ * Contract: split on `,`, trim each item, drop empty items, parse the rest
98
+ * with `item` (default {@link str}). Item numbering in errors counts the kept
99
+ * items from 1 and never leaks the item value, so secret lists stay
100
+ * redactable. Wrap the *list* in {@link secret}/{@link optional}, not the item.
101
+ */
102
+ declare function list(): Validator<readonly string[]>;
103
+ declare function list<T>(item: Validator<T>): Validator<readonly T[]>;
104
+ /**
105
+ * Unset (or empty) is fine and yields `undefined` — the mirror of a Rust
106
+ * `Option<T>` field. Cannot be combined with {@link withDefault}: a defaulted
107
+ * setting is always present ({@link createSettings} rejects the combination).
108
+ */
109
+ declare function optional<T>(validator: Validator<T>): Validator<T | undefined>;
110
+ /**
111
+ * Fall back to `literal` when the variable is unset. The literal goes through
112
+ * the exact same parsing rules as an env value — the mirror of the Rust
113
+ * `= "literal"` field default.
114
+ */
115
+ declare function withDefault<T>(validator: Validator<T>, literal: string): Validator<T>;
116
+ /**
117
+ * Redact the raw value in error output — the mirror of `#[secret]`. Note that
118
+ * unlike Rust (whose generated `Debug` prints `***`), JS has no debug-print
119
+ * boundary: `console.log(settings.TOKEN)` prints the real value. The redaction
120
+ * covers what the *library* emits: `SettingsError` messages and issues.
121
+ */
122
+ declare function secret<T>(validator: Validator<T>): Validator<T>;
123
+
124
+ /**
125
+ * {@link createSettings} — read a validated, typed settings object out of an
126
+ * explicitly injected environment record, with a server/client split for
127
+ * browser bundles.
128
+ */
129
+
130
+ /** A settings declaration: env var name → {@link Validator}. */
131
+ type SettingsSchema = Record<string, Validator<unknown>>;
132
+ /** The typed settings object a schema produces. */
133
+ type InferSettings<S extends SettingsSchema> = {
134
+ readonly [K in keyof S]: S[K] extends Validator<infer T> ? T : never;
135
+ };
136
+ interface CreateSettingsOptions<S extends SettingsSchema, C extends SettingsSchema> {
137
+ /** Server-only settings. Accessing one from client code throws. */
138
+ readonly server?: S;
139
+ /**
140
+ * Client-exposable settings. Every key must start with {@link clientPrefix}
141
+ * so nothing leaks into a browser bundle by accident.
142
+ */
143
+ readonly client?: C;
144
+ /** Required when `client` is non-empty — e.g. `NEXT_PUBLIC_`. */
145
+ readonly clientPrefix?: string;
146
+ /**
147
+ * The environment record, destructured **explicitly** — bundlers inline
148
+ * `process.env.NEXT_PUBLIC_*` / `import.meta.env.*` at build time only when
149
+ * each variable is a static member expression, so a bare `process.env` pass
150
+ * works on the server but leaves client values undefined in the browser:
151
+ *
152
+ * ```ts
153
+ * runtimeEnv: {
154
+ * DATABASE_URL: process.env.DATABASE_URL,
155
+ * NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
156
+ * }
157
+ * ```
158
+ */
159
+ readonly runtimeEnv: Readonly<Record<string, string | undefined>>;
160
+ /** Contract: `VAR=` (empty string) behaves like unset. Default `true`. */
161
+ readonly emptyStringAsUnset?: boolean;
162
+ /**
163
+ * Where we are running. Default: `window` is absent from `globalThis`.
164
+ * Override for workers or exotic runtimes.
165
+ */
166
+ readonly isServer?: boolean;
167
+ }
168
+ /**
169
+ * Validate the injected environment against the schema and return the typed,
170
+ * read-only settings object. Mirrors the Rust `settings!` macro's
171
+ * `from_source`, plus the client/server split (a bundler concern with no Rust
172
+ * equivalent).
173
+ *
174
+ * - Every missing/invalid variable is reported at once in one
175
+ * {@link SettingsError} — no fix-one-reboot-fix-next loops.
176
+ * - Validation runs eagerly, at the `createSettings` call. Call it at module
177
+ * scope of a single `settings.ts` so a bad environment fails the boot (or,
178
+ * in Next.js, the build — import your settings module from `next.config`).
179
+ * - On the client only the `client` schema is validated (server values never
180
+ * reach the bundle); accessing a server key there throws.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * export const settings = createSettings({
185
+ * server: {
186
+ * DATABASE_URL: url(),
187
+ * SIGNING_KEY: secret(str()),
188
+ * PORT: withDefault(port(), '8080'),
189
+ * ...presets.posthog(),
190
+ * },
191
+ * clientPrefix: 'NEXT_PUBLIC_',
192
+ * client: { ...presets.posthogClient() },
193
+ * runtimeEnv: {
194
+ * DATABASE_URL: process.env.DATABASE_URL,
195
+ * SIGNING_KEY: process.env.SIGNING_KEY,
196
+ * PORT: process.env.PORT,
197
+ * POSTHOG_KEY: process.env.POSTHOG_KEY,
198
+ * POSTHOG_HOST: process.env.POSTHOG_HOST,
199
+ * NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
200
+ * NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
201
+ * },
202
+ * });
203
+ * ```
204
+ */
205
+ declare function createSettings<S extends SettingsSchema = Record<never, never>, C extends SettingsSchema = Record<never, never>>(options: CreateSettingsOptions<S, C>): InferSettings<S> & InferSettings<C>;
206
+
207
+ /**
208
+ * Org-canonical shared variable groups — one place that fixes the names, so
209
+ * `POSTHOG_KEY` vs `POSTHOG_API_KEY` vs `NEXT_PUBLIC_POSTHOG_KEY` drift stops
210
+ * at the source. Spread a preset into the `server` / `client` block of
211
+ * {@link createSettings}; the Rust mirror ships the same server-side groups as
212
+ * ready-made structs (`settings::presets`).
213
+ */
214
+
215
+ declare const presets: {
216
+ /**
217
+ * PostHog capture credentials, canonical names: `POSTHOG_KEY` /
218
+ * `POSTHOG_HOST`. Both optional — capture is simply off without them. The
219
+ * project key (`phc_…`) is write-only and ships in frontend bundles anyway,
220
+ * so it is not `secret(...)`.
221
+ */
222
+ readonly posthog: () => {
223
+ POSTHOG_KEY: Validator<string | undefined>;
224
+ POSTHOG_HOST: Validator<string | undefined>;
225
+ };
226
+ /**
227
+ * Sentry reporting, canonical name: `SENTRY_DSN`. Optional — monitoring is
228
+ * off without it. A DSN authorises event *submission* only, so it is not
229
+ * `secret(...)`.
230
+ */
231
+ readonly sentry: () => {
232
+ SENTRY_DSN: Validator<string | undefined>;
233
+ };
234
+ /**
235
+ * The deployment environment, canonical name: `APP_ENV`. Defaults to
236
+ * `development`; kept a free string on purpose — constraining the values
237
+ * would break consumers that add a stage.
238
+ */
239
+ readonly appEnv: () => {
240
+ APP_ENV: Validator<string>;
241
+ };
242
+ /** The `client`-block variant of {@link presets.posthog} for Next.js bundles. */
243
+ readonly posthogClient: () => {
244
+ NEXT_PUBLIC_POSTHOG_KEY: Validator<string | undefined>;
245
+ NEXT_PUBLIC_POSTHOG_HOST: Validator<string | undefined>;
246
+ };
247
+ /** The `client`-block variant of {@link presets.sentry} + `APP_ENV` for Next.js bundles. */
248
+ readonly sentryClient: () => {
249
+ NEXT_PUBLIC_SENTRY_DSN: Validator<string | undefined>;
250
+ NEXT_PUBLIC_APP_ENV: Validator<string>;
251
+ };
252
+ };
253
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,241 @@
1
+ // src/error.ts
2
+ var SettingsError = class extends Error {
3
+ issues;
4
+ constructor(issues) {
5
+ super(formatIssues(issues));
6
+ this.name = "SettingsError";
7
+ this.issues = issues;
8
+ }
9
+ };
10
+ function formatIssues(issues) {
11
+ const noun = issues.length === 1 ? "problem" : "problems";
12
+ const lines = issues.map((issue) => ` - ${formatIssue(issue)}`);
13
+ return [`invalid settings (${issues.length} ${noun})`, ...lines].join("\n");
14
+ }
15
+ function formatIssue(issue) {
16
+ if (issue.kind === "missing") return `${issue.key}: missing`;
17
+ const value = issue.value === void 0 ? "" : ` ${JSON.stringify(issue.value)}`;
18
+ return `${issue.key}: invalid value${value}: ${issue.detail ?? "failed to parse"}`;
19
+ }
20
+
21
+ // src/validators.ts
22
+ function base(kind, parse) {
23
+ return { kind, optional: false, secret: false, defaultLiteral: void 0, parse };
24
+ }
25
+ function messageOf(error) {
26
+ return error instanceof Error ? error.message : String(error);
27
+ }
28
+ function str() {
29
+ return base("string", (raw) => raw);
30
+ }
31
+ function bool() {
32
+ return base("boolean", (raw) => {
33
+ const lower = raw.toLowerCase();
34
+ if (lower === "true" || lower === "1") return true;
35
+ if (lower === "false" || lower === "0") return false;
36
+ throw new Error("expected one of `true`, `false`, `1`, `0` (case-insensitive)");
37
+ });
38
+ }
39
+ var FLOAT_GRAMMAR = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
40
+ var INT_GRAMMAR = /^[+-]?\d+$/;
41
+ function num() {
42
+ return base("number", (raw) => {
43
+ if (!FLOAT_GRAMMAR.test(raw)) throw new Error("expected a finite number");
44
+ const value = Number(raw);
45
+ if (!Number.isFinite(value)) throw new Error("expected a finite number");
46
+ return value;
47
+ });
48
+ }
49
+ function int() {
50
+ return base("integer", (raw) => {
51
+ if (!INT_GRAMMAR.test(raw)) throw new Error("expected an integer");
52
+ const value = Number(raw);
53
+ if (!Number.isSafeInteger(value)) throw new Error("expected a safe integer (within \xB1(2^53 - 1))");
54
+ return value;
55
+ });
56
+ }
57
+ function port() {
58
+ const inner = int();
59
+ return base("port", (raw) => {
60
+ const value = inner.parse(raw);
61
+ if (value < 1 || value > 65535) throw new Error("expected a port (1-65535)");
62
+ return value;
63
+ });
64
+ }
65
+ function url() {
66
+ return base("url", (raw) => {
67
+ if (raw === "" || /\s/.test(raw)) throw new Error("expected an absolute URL");
68
+ try {
69
+ new URL(raw);
70
+ } catch {
71
+ throw new Error("expected an absolute URL");
72
+ }
73
+ return raw;
74
+ });
75
+ }
76
+ function oneOf(values) {
77
+ const expected = `expected one of ${values.map((value) => `\`${value}\``).join(", ")}`;
78
+ return base("choice", (raw) => {
79
+ if (values.includes(raw)) return raw;
80
+ throw new Error(expected);
81
+ });
82
+ }
83
+ function list(item) {
84
+ const inner = item ?? str();
85
+ return base(
86
+ "list",
87
+ (raw) => raw.split(",").map((piece) => piece.trim()).filter((piece) => piece !== "").map((piece, index) => {
88
+ try {
89
+ return inner.parse(piece);
90
+ } catch (error) {
91
+ throw new Error(`item ${index + 1}: ${messageOf(error)}`);
92
+ }
93
+ })
94
+ );
95
+ }
96
+ function optional(validator) {
97
+ return { ...validator, optional: true };
98
+ }
99
+ function withDefault(validator, literal) {
100
+ return { ...validator, defaultLiteral: literal };
101
+ }
102
+ function secret(validator) {
103
+ return { ...validator, secret: true };
104
+ }
105
+
106
+ // src/create-settings.ts
107
+ function createSettings(options) {
108
+ const server = options.server ?? {};
109
+ const client = options.client ?? {};
110
+ const { clientPrefix } = options;
111
+ const emptyAsUnset = options.emptyStringAsUnset ?? true;
112
+ const isServer = options.isServer ?? !("window" in globalThis);
113
+ const serverKeys = Object.keys(server);
114
+ const clientKeys = Object.keys(client);
115
+ if (clientKeys.length > 0 && clientPrefix === void 0) {
116
+ throw new Error("@evinvest/settings: `clientPrefix` is required when `client` settings are declared");
117
+ }
118
+ for (const key of serverKeys) {
119
+ if (Object.hasOwn(client, key)) {
120
+ throw new Error(`@evinvest/settings: setting "${key}" is declared in both server and client`);
121
+ }
122
+ }
123
+ if (clientPrefix !== void 0) {
124
+ for (const key of clientKeys) {
125
+ if (!key.startsWith(clientPrefix)) {
126
+ throw new Error(`@evinvest/settings: client setting "${key}" must start with clientPrefix "${clientPrefix}"`);
127
+ }
128
+ }
129
+ for (const key of serverKeys) {
130
+ if (key.startsWith(clientPrefix)) {
131
+ throw new Error(
132
+ `@evinvest/settings: server setting "${key}" starts with clientPrefix "${clientPrefix}" \u2014 declare it under \`client\``
133
+ );
134
+ }
135
+ }
136
+ }
137
+ for (const [key, validator] of [...Object.entries(server), ...Object.entries(client)]) {
138
+ if (validator.optional && validator.defaultLiteral !== void 0) {
139
+ throw new Error(
140
+ `@evinvest/settings: setting "${key}" is both optional and defaulted \u2014 a defaulted setting is always present, drop one`
141
+ );
142
+ }
143
+ }
144
+ const active = isServer ? { ...server, ...client } : client;
145
+ const issues = [];
146
+ const values = {};
147
+ for (const [key, validator] of Object.entries(active)) {
148
+ let raw = options.runtimeEnv[key];
149
+ if (emptyAsUnset && raw === "") raw = void 0;
150
+ const fromDefault = raw === void 0 && validator.defaultLiteral !== void 0;
151
+ if (fromDefault) raw = validator.defaultLiteral;
152
+ if (raw === void 0) {
153
+ if (validator.optional) {
154
+ values[key] = void 0;
155
+ continue;
156
+ }
157
+ issues.push({ key, kind: "missing" });
158
+ continue;
159
+ }
160
+ try {
161
+ values[key] = validator.parse(raw);
162
+ } catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ issues.push({
165
+ key,
166
+ kind: "invalid",
167
+ detail: fromDefault ? `invalid default: ${message}` : message,
168
+ ...validator.secret && !fromDefault ? {} : { value: raw }
169
+ });
170
+ }
171
+ }
172
+ if (issues.length > 0) throw new SettingsError(issues);
173
+ Object.freeze(values);
174
+ const serverOnly = new Set(serverKeys);
175
+ return new Proxy(values, {
176
+ get(target, prop, receiver) {
177
+ if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
178
+ if (!isServer && serverOnly.has(prop)) {
179
+ throw new Error(`@evinvest/settings: attempted to access server-only setting "${prop}" on the client`);
180
+ }
181
+ return Reflect.get(target, prop, receiver);
182
+ }
183
+ });
184
+ }
185
+
186
+ // src/presets.ts
187
+ var presets = {
188
+ /**
189
+ * PostHog capture credentials, canonical names: `POSTHOG_KEY` /
190
+ * `POSTHOG_HOST`. Both optional — capture is simply off without them. The
191
+ * project key (`phc_…`) is write-only and ships in frontend bundles anyway,
192
+ * so it is not `secret(...)`.
193
+ */
194
+ posthog: () => ({
195
+ POSTHOG_KEY: optional(str()),
196
+ POSTHOG_HOST: optional(str())
197
+ }),
198
+ /**
199
+ * Sentry reporting, canonical name: `SENTRY_DSN`. Optional — monitoring is
200
+ * off without it. A DSN authorises event *submission* only, so it is not
201
+ * `secret(...)`.
202
+ */
203
+ sentry: () => ({
204
+ SENTRY_DSN: optional(str())
205
+ }),
206
+ /**
207
+ * The deployment environment, canonical name: `APP_ENV`. Defaults to
208
+ * `development`; kept a free string on purpose — constraining the values
209
+ * would break consumers that add a stage.
210
+ */
211
+ appEnv: () => ({
212
+ APP_ENV: withDefault(str(), "development")
213
+ }),
214
+ /** The `client`-block variant of {@link presets.posthog} for Next.js bundles. */
215
+ posthogClient: () => ({
216
+ NEXT_PUBLIC_POSTHOG_KEY: optional(str()),
217
+ NEXT_PUBLIC_POSTHOG_HOST: optional(str())
218
+ }),
219
+ /** The `client`-block variant of {@link presets.sentry} + `APP_ENV` for Next.js bundles. */
220
+ sentryClient: () => ({
221
+ NEXT_PUBLIC_SENTRY_DSN: optional(str()),
222
+ NEXT_PUBLIC_APP_ENV: withDefault(str(), "development")
223
+ })
224
+ };
225
+ export {
226
+ SettingsError,
227
+ bool,
228
+ createSettings,
229
+ int,
230
+ list,
231
+ num,
232
+ oneOf,
233
+ optional,
234
+ port,
235
+ presets,
236
+ secret,
237
+ str,
238
+ url,
239
+ withDefault
240
+ };
241
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":";AA+BO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAC9B;AAAA,EAET,YAAY,QAAkC;AAC5C,UAAM,aAAa,MAAM,CAAC;AAC1B,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEA,SAAS,aAAa,QAA0C;AAC9D,QAAM,OAAO,OAAO,WAAW,IAAI,YAAY;AAC/C,QAAM,QAAQ,OAAO,IAAI,CAAC,UAAU,OAAO,YAAY,KAAK,CAAC,EAAE;AAC/D,SAAO,CAAC,qBAAqB,OAAO,MAAM,IAAI,IAAI,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI;AAC5E;AAEA,SAAS,YAAY,OAA8B;AACjD,MAAI,MAAM,SAAS,UAAW,QAAO,GAAG,MAAM,GAAG;AAGjD,QAAM,QAAQ,MAAM,UAAU,SAAY,KAAK,IAAI,KAAK,UAAU,MAAM,KAAK,CAAC;AAC9E,SAAO,GAAG,MAAM,GAAG,kBAAkB,KAAK,KAAK,MAAM,UAAU,iBAAiB;AAClF;;;AClBA,SAAS,KAAQ,MAAc,OAAyC;AACtE,SAAO,EAAE,MAAM,UAAU,OAAO,QAAQ,OAAO,gBAAgB,QAAW,MAAM;AAClF;AAEA,SAAS,UAAU,OAAwB;AACzC,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAGO,SAAS,MAAyB;AACvC,SAAO,KAAK,UAAU,CAAC,QAAQ,GAAG;AACpC;AAGO,SAAS,OAA2B;AACzC,SAAO,KAAK,WAAW,CAAC,QAAQ;AAC9B,UAAM,QAAQ,IAAI,YAAY;AAC9B,QAAI,UAAU,UAAU,UAAU,IAAK,QAAO;AAC9C,QAAI,UAAU,WAAW,UAAU,IAAK,QAAO;AAC/C,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAChF,CAAC;AACH;AAKA,IAAM,gBAAgB;AACtB,IAAM,cAAc;AAOb,SAAS,MAAyB;AACvC,SAAO,KAAK,UAAU,CAAC,QAAQ;AAC7B,QAAI,CAAC,cAAc,KAAK,GAAG,EAAG,OAAM,IAAI,MAAM,0BAA0B;AACxE,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,OAAM,IAAI,MAAM,0BAA0B;AACvE,WAAO;AAAA,EACT,CAAC;AACH;AAQO,SAAS,MAAyB;AACvC,SAAO,KAAK,WAAW,CAAC,QAAQ;AAC9B,QAAI,CAAC,YAAY,KAAK,GAAG,EAAG,OAAM,IAAI,MAAM,qBAAqB;AACjE,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,CAAC,OAAO,cAAc,KAAK,EAAG,OAAM,IAAI,MAAM,iDAA8C;AAChG,WAAO;AAAA,EACT,CAAC;AACH;AAGO,SAAS,OAA0B;AACxC,QAAM,QAAQ,IAAI;AAClB,SAAO,KAAK,QAAQ,CAAC,QAAQ;AAC3B,UAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,QAAI,QAAQ,KAAK,QAAQ,MAAO,OAAM,IAAI,MAAM,2BAA2B;AAC3E,WAAO;AAAA,EACT,CAAC;AACH;AAGO,SAAS,MAAyB;AACvC,SAAO,KAAK,OAAO,CAAC,QAAQ;AAG1B,QAAI,QAAQ,MAAM,KAAK,KAAK,GAAG,EAAG,OAAM,IAAI,MAAM,0BAA0B;AAC5E,QAAI;AACF,UAAI,IAAI,GAAG;AAAA,IACb,QAAQ;AACN,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AACA,WAAO;AAAA,EACT,CAAC;AACH;AASO,SAAS,MAA8B,QAAoC;AAChF,QAAM,WAAW,mBAAmB,OAAO,IAAI,CAAC,UAAU,KAAK,KAAK,IAAI,EAAE,KAAK,IAAI,CAAC;AACpF,SAAO,KAAK,UAAU,CAAC,QAAQ;AAC7B,QAAK,OAA6B,SAAS,GAAG,EAAG,QAAO;AACxD,UAAM,IAAI,MAAM,QAAQ;AAAA,EAC1B,CAAC;AACH;AAUO,SAAS,KAAQ,MAAyD;AAC/E,QAAM,QAAQ,QAAQ,IAAI;AAC1B,SAAO;AAAA,IAAK;AAAA,IAAQ,CAAC,QACnB,IACG,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,CAAC,UAAU,UAAU,EAAE,EAC9B,IAAI,CAAC,OAAO,UAAU;AACrB,UAAI;AACF,eAAO,MAAM,MAAM,KAAK;AAAA,MAC1B,SAAS,OAAO;AACd,cAAM,IAAI,MAAM,QAAQ,QAAQ,CAAC,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACL;AACF;AAOO,SAAS,SAAY,WAAmD;AAC7E,SAAO,EAAE,GAAG,WAAW,UAAU,KAAK;AACxC;AAOO,SAAS,YAAe,WAAyB,SAA+B;AACrF,SAAO,EAAE,GAAG,WAAW,gBAAgB,QAAQ;AACjD;AAQO,SAAS,OAAU,WAAuC;AAC/D,SAAO,EAAE,GAAG,WAAW,QAAQ,KAAK;AACtC;;;ACjGO,SAAS,eAGd,SAA2E;AAC3E,QAAM,SAAyB,QAAQ,UAAU,CAAC;AAClD,QAAM,SAAyB,QAAQ,UAAU,CAAC;AAClD,QAAM,EAAE,aAAa,IAAI;AACzB,QAAM,eAAe,QAAQ,sBAAsB;AACnD,QAAM,WAAW,QAAQ,YAAY,EAAE,YAAY;AAEnD,QAAM,aAAa,OAAO,KAAK,MAAM;AACrC,QAAM,aAAa,OAAO,KAAK,MAAM;AAGrC,MAAI,WAAW,SAAS,KAAK,iBAAiB,QAAW;AACvD,UAAM,IAAI,MAAM,oFAAoF;AAAA,EACtG;AACA,aAAW,OAAO,YAAY;AAE5B,QAAI,OAAO,OAAO,QAAQ,GAAG,GAAG;AAC9B,YAAM,IAAI,MAAM,gCAAgC,GAAG,yCAAyC;AAAA,IAC9F;AAAA,EACF;AACA,MAAI,iBAAiB,QAAW;AAC9B,eAAW,OAAO,YAAY;AAC5B,UAAI,CAAC,IAAI,WAAW,YAAY,GAAG;AACjC,cAAM,IAAI,MAAM,uCAAuC,GAAG,mCAAmC,YAAY,GAAG;AAAA,MAC9G;AAAA,IACF;AACA,eAAW,OAAO,YAAY;AAC5B,UAAI,IAAI,WAAW,YAAY,GAAG;AAChC,cAAM,IAAI;AAAA,UACR,uCAAuC,GAAG,+BAA+B,YAAY;AAAA,QACvF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,aAAW,CAAC,KAAK,SAAS,KAAK,CAAC,GAAG,OAAO,QAAQ,MAAM,GAAG,GAAG,OAAO,QAAQ,MAAM,CAAC,GAAG;AACrF,QAAI,UAAU,YAAY,UAAU,mBAAmB,QAAW;AAChE,YAAM,IAAI;AAAA,QACR,gCAAgC,GAAG;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAIA,QAAM,SAAS,WAAW,EAAE,GAAG,QAAQ,GAAG,OAAO,IAAI;AACrD,QAAM,SAA0B,CAAC;AACjC,QAAM,SAAkC,CAAC;AACzC,aAAW,CAAC,KAAK,SAAS,KAAK,OAAO,QAAQ,MAAM,GAAG;AACrD,QAAI,MAAM,QAAQ,WAAW,GAAG;AAChC,QAAI,gBAAgB,QAAQ,GAAI,OAAM;AAEtC,UAAM,cAAc,QAAQ,UAAa,UAAU,mBAAmB;AACtE,QAAI,YAAa,OAAM,UAAU;AACjC,QAAI,QAAQ,QAAW;AACrB,UAAI,UAAU,UAAU;AACtB,eAAO,GAAG,IAAI;AACd;AAAA,MACF;AACA,aAAO,KAAK,EAAE,KAAK,MAAM,UAAU,CAAC;AACpC;AAAA,IACF;AACA,QAAI;AACF,aAAO,GAAG,IAAI,UAAU,MAAM,GAAG;AAAA,IACnC,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,aAAO,KAAK;AAAA,QACV;AAAA,QACA,MAAM;AAAA,QACN,QAAQ,cAAc,oBAAoB,OAAO,KAAK;AAAA,QACtD,GAAI,UAAU,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,OAAO,IAAI;AAAA,MAC3D,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,OAAO,SAAS,EAAG,OAAM,IAAI,cAAc,MAAM;AAErD,SAAO,OAAO,MAAM;AACpB,QAAM,aAAa,IAAI,IAAI,UAAU;AACrC,SAAO,IAAI,MAAM,QAAQ;AAAA,IACvB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,OAAO,SAAS,SAAU,QAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AACvE,UAAI,CAAC,YAAY,WAAW,IAAI,IAAI,GAAG;AACrC,cAAM,IAAI,MAAM,gEAAgE,IAAI,iBAAiB;AAAA,MACvG;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;;;ACtKO,IAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrB,SAAS,OAAoG;AAAA,IAC3G,aAAa,SAAS,IAAI,CAAC;AAAA,IAC3B,cAAc,SAAS,IAAI,CAAC;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,OAAsD;AAAA,IAC5D,YAAY,SAAS,IAAI,CAAC;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,OAAuC;AAAA,IAC7C,SAAS,YAAY,IAAI,GAAG,aAAa;AAAA,EAC3C;AAAA;AAAA,EAGA,eAAe,OAGT;AAAA,IACJ,yBAAyB,SAAS,IAAI,CAAC;AAAA,IACvC,0BAA0B,SAAS,IAAI,CAAC;AAAA,EAC1C;AAAA;AAAA,EAGA,cAAc,OAGR;AAAA,IACJ,wBAAwB,SAAS,IAAI,CAAC;AAAA,IACtC,qBAAqB,YAAY,IAAI,GAAG,aAAa;AAAA,EACvD;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@evinvest/settings",
3
+ "version": "0.1.0",
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
+ "license": "BlueOak-1.0.0",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/EV-invest/lib.git",
12
+ "directory": "ts/settings"
13
+ },
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "types": "./dist/index.d.ts",
22
+ "main": "./dist/index.js",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "sideEffects": false,
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "test": "vitest run",
33
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.core.json --noEmit",
34
+ "prepare": "tsup"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.10.0",
38
+ "tsup": "^8.3.5",
39
+ "typescript": "^5.7.2",
40
+ "vitest": "^2.1.8"
41
+ }
42
+ }