typed-settings 0.1.0 → 0.1.4

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.
Files changed (50) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters/joi.d.cts +13 -0
  3. package/dist/adapters/joi.d.ts +13 -0
  4. package/dist/adapters/zod.d.cts +16 -0
  5. package/dist/adapters/zod.d.ts +16 -0
  6. package/dist/{chunk-VTIYOSG2.js → chunk-DEP5SD4J.js} +1 -1
  7. package/dist/{chunk-RCO6K3WQ.js → chunk-YVJEZPPZ.js} +11 -6
  8. package/dist/cli/main.cjs +11 -6
  9. package/dist/cli/main.d.cts +4 -0
  10. package/dist/cli/main.d.ts +4 -0
  11. package/dist/cli/main.js +2 -2
  12. package/dist/{errors-IU9n4f3t.d.cts → errors-itGaU4Q8.d.cts} +5 -0
  13. package/dist/{errors-IU9n4f3t.d.ts → errors-itGaU4Q8.d.ts} +5 -0
  14. package/dist/formats/env.d.cts +5 -0
  15. package/dist/formats/env.d.ts +5 -0
  16. package/dist/formats/json.d.cts +4 -0
  17. package/dist/formats/json.d.ts +4 -0
  18. package/dist/formats/toml.d.cts +4 -0
  19. package/dist/formats/toml.d.ts +4 -0
  20. package/dist/formats/yaml.d.cts +5 -0
  21. package/dist/formats/yaml.d.ts +5 -0
  22. package/dist/framework/next.cjs +11 -6
  23. package/dist/framework/next.d.cts +16 -3
  24. package/dist/framework/next.d.ts +16 -3
  25. package/dist/framework/next.js +1 -1
  26. package/dist/framework/vite.cjs +11 -6
  27. package/dist/framework/vite.d.cts +16 -3
  28. package/dist/framework/vite.d.ts +16 -3
  29. package/dist/framework/vite.js +2 -2
  30. package/dist/index.cjs +11 -6
  31. package/dist/index.d.cts +42 -3
  32. package/dist/index.d.ts +42 -3
  33. package/dist/index.js +1 -1
  34. package/dist/node.cjs +11 -6
  35. package/dist/node.d.cts +29 -2
  36. package/dist/node.d.ts +29 -2
  37. package/dist/node.js +2 -2
  38. package/dist/{shared-DBcV6jIy.d.cts → shared-CucO_XsG.d.cts} +1 -0
  39. package/dist/{shared-DBcV6jIy.d.ts → shared-CucO_XsG.d.ts} +1 -0
  40. package/dist/types-B_Dc2L4T.d.cts +69 -0
  41. package/dist/types-B_Dc2L4T.d.ts +69 -0
  42. package/dist/vault/aws.d.cts +26 -1
  43. package/dist/vault/aws.d.ts +26 -1
  44. package/dist/vault/hashicorp.d.cts +23 -2
  45. package/dist/vault/hashicorp.d.ts +23 -2
  46. package/dist/vault/http.d.cts +20 -2
  47. package/dist/vault/http.d.ts +20 -2
  48. package/package.json +9 -1
  49. package/dist/types-BswIPp7U.d.cts +0 -41
  50. package/dist/types-BswIPp7U.d.ts +0 -41
package/README.md CHANGED
@@ -185,7 +185,7 @@ Provider details: [docs/vault.md](docs/vault.md).
185
185
  | `schema` | Standard Schema | required | zod / valibot / arktype object, or a legacy bridge. An async `validate()` forces `settingsAsync()` |
186
186
  | `sources` | array | `['.env', 'env']` | layers in priority order, later wins |
187
187
  | `prefix` | `string` | - | global prefix strip, case-insensitive, once |
188
- | `envMap` | object | - | keys used literally: no prefix strip, no `__` splitting |
188
+ | `envMap` | object | `{}` | maps an env key to the literal key it should occupy (used as-is: no strip/split) |
189
189
  | `env` | object | `process.env` | the env snapshot. Pass `{}` to isolate, or a map in tests/on Edge |
190
190
  | `expand` | `boolean` | `true` | `$VAR` expansion after merging. `false` leaves literals alone |
191
191
  | `allowUnresolved` | `boolean` | `false` | `true` keeps `${MISSING}` as-is instead of throwing |
@@ -279,7 +279,9 @@ Things people actually hit, in rough order of frequency:
279
279
 
280
280
  - **`USE_ASYNC: provider is async`.** One of your providers (or the schema itself) is async. Switch to `settingsAsync()`.
281
281
  - **`E_UNRESOLVED: FOO missing`.** A `$FOO` reference points at nothing. Set the variable, give it a default (`${FOO:-fallback}`), or pass `allowUnresolved: true` to leave it literal.
282
- - **Schema rejects everything.** Keys are lowercased during load, so schemas need lowercase keys. `port`, not `PORT`.
282
+ - **Schema rejects everything.** Keys are lowercased during load, so schemas need lowercase keys (`port`, not `PORT`). The failure mode is sneaky: `z.object({ PORT: z.coerce.number() })` never matches the lowercased input, so it validates `undefined`, and `z.coerce.number()` turns that into `NaN` — you get the confusing "expected number, received NaN".
283
+ - **Everything is silently empty or wrong.** Most often a global `prefix` ate your keys: matching applies to every source, so `prefix: 'APP_'` drops a YAML key like `port:` and a secrets-dir entry like `API_TOKEN` without a word. Scope it per source, or skip it and bridge env names via `envMap`.
284
+ - **Deno (JSR) refuses a fresh release.** Deno's minimum-dependency-age policy blocks versions published within ~24h. For local dev, run with `--minimum-dependency-age=0`.
283
285
  - **`E_NO_FS` under ESM.** Core can't reach `node:fs` on its own. Import `typed-settings/node` once, or stick to `{ text }` / `{ map }` sources.
284
286
  - **`E_UNKNOWN_KEY`.** Extra keys with `unknownKeys: 'reject'` (the CLI calls this `--strict`). Either drop the keys or switch to `strip` / `preserve`.
285
287
  - **`SCHEMA_LOAD_ERROR`.** The CLI can't find your schema. Export it under the name `schema`, or point at it with `--schema-export NAME`.
@@ -4,7 +4,16 @@ type AnySchema = {
4
4
  validateSync?: (v: unknown, o?: unknown) => unknown;
5
5
  validate?: (...a: never[]) => unknown;
6
6
  };
7
+ /**
8
+ * Adapts a Yup schema via `validateSync` (`stripUnknown`, all errors).
9
+ * `inner[]` failures map to issues; anything else becomes a root issue.
10
+ */
7
11
  declare function yupAdapter(schema: any): StandardSchemaV1;
12
+ /**
13
+ * Adapts a Joi schema (`validate` with convert + stripUnknown). `details[]`
14
+ * map to issues; a throwing `validate()` or empty details still yields a
15
+ * non-empty issues array, as Standard Schema requires.
16
+ */
8
17
  declare function joiAdapter(schema: AnySchema & {
9
18
  validate?: (v: unknown, o?: unknown) => {
10
19
  error?: {
@@ -17,6 +26,10 @@ declare function joiAdapter(schema: AnySchema & {
17
26
  value: unknown;
18
27
  };
19
28
  }): StandardSchemaV1;
29
+ /**
30
+ * Adapts a Superstruct struct via `create()`. `failures()` map to issues;
31
+ * anything else falls back to the thrown message.
32
+ */
20
33
  declare function superstructAdapter(schema: AnySchema & {
21
34
  create?: (v: unknown) => unknown;
22
35
  }): StandardSchemaV1;
@@ -4,7 +4,16 @@ type AnySchema = {
4
4
  validateSync?: (v: unknown, o?: unknown) => unknown;
5
5
  validate?: (...a: never[]) => unknown;
6
6
  };
7
+ /**
8
+ * Adapts a Yup schema via `validateSync` (`stripUnknown`, all errors).
9
+ * `inner[]` failures map to issues; anything else becomes a root issue.
10
+ */
7
11
  declare function yupAdapter(schema: any): StandardSchemaV1;
12
+ /**
13
+ * Adapts a Joi schema (`validate` with convert + stripUnknown). `details[]`
14
+ * map to issues; a throwing `validate()` or empty details still yields a
15
+ * non-empty issues array, as Standard Schema requires.
16
+ */
8
17
  declare function joiAdapter(schema: AnySchema & {
9
18
  validate?: (v: unknown, o?: unknown) => {
10
19
  error?: {
@@ -17,6 +26,10 @@ declare function joiAdapter(schema: AnySchema & {
17
26
  value: unknown;
18
27
  };
19
28
  }): StandardSchemaV1;
29
+ /**
30
+ * Adapts a Superstruct struct via `create()`. `failures()` map to issues;
31
+ * anything else falls back to the thrown message.
32
+ */
20
33
  declare function superstructAdapter(schema: AnySchema & {
21
34
  create?: (v: unknown) => unknown;
22
35
  }): StandardSchemaV1;
@@ -1,6 +1,22 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
2
 
3
+ /**
4
+ * Zod helpers: `withPrefix` scopes any Standard Schema under an env prefix,
5
+ * `coerceEnv` is the identity hook for composed pipelines.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ /**
11
+ * Scopes a schema under `prefix`: matching keys strip case-insensitively before
12
+ * validation, prefixed keys win collisions, non-objects pass through untouched.
13
+ * An empty prefix returns the schema unchanged.
14
+ */
3
15
  declare function withPrefix<S extends StandardSchemaV1>(schema: S, prefix: string): S;
16
+ /**
17
+ * Identity hook for composed pipelines. Documents "this value already went
18
+ * through env coercion"; the real logic lives in `coerce.ts`.
19
+ */
4
20
  declare function coerceEnv(value: unknown): unknown;
5
21
 
6
22
  export { coerceEnv, withPrefix };
@@ -1,6 +1,22 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
2
 
3
+ /**
4
+ * Zod helpers: `withPrefix` scopes any Standard Schema under an env prefix,
5
+ * `coerceEnv` is the identity hook for composed pipelines.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ /**
11
+ * Scopes a schema under `prefix`: matching keys strip case-insensitively before
12
+ * validation, prefixed keys win collisions, non-objects pass through untouched.
13
+ * An empty prefix returns the schema unchanged.
14
+ */
3
15
  declare function withPrefix<S extends StandardSchemaV1>(schema: S, prefix: string): S;
16
+ /**
17
+ * Identity hook for composed pipelines. Documents "this value already went
18
+ * through env coercion"; the real logic lives in `coerce.ts`.
19
+ */
4
20
  declare function coerceEnv(value: unknown): unknown;
5
21
 
6
22
  export { coerceEnv, withPrefix };
@@ -1,4 +1,4 @@
1
- import { settings } from './chunk-RCO6K3WQ.js';
1
+ import { settings } from './chunk-YVJEZPPZ.js';
2
2
  import { parseEnvText } from './chunk-F6DX3HXZ.js';
3
3
  import { readFileSync, readdirSync, statSync, watch } from 'fs';
4
4
  import { join, dirname, basename, resolve } from 'path';
@@ -253,12 +253,12 @@ function expandKeys(flat, opts) {
253
253
  const stripped = stripPrefix(rawKey);
254
254
  if (stripped === null) continue;
255
255
  const useKey = stripped;
256
- if (useKey.includes("__")) {
257
- if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
258
- const parts = useKey.split("__");
256
+ if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
257
+ const parts = useKey.split("__");
258
+ const cleanSplit = parts.length > 1 && parts.every((p) => p !== "");
259
+ if (cleanSplit) {
259
260
  for (const p of parts) {
260
261
  if (FORBIDDEN.has(p.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
261
- if (!p) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_EMPTY_SEGMENT" }], "E_EMPTY_SEGMENT");
262
262
  }
263
263
  const lowered = parts.map((p) => p.toLowerCase());
264
264
  const top = lowered[0];
@@ -290,7 +290,6 @@ function expandKeys(flat, opts) {
290
290
  setPath(out, lowered, value);
291
291
  } else {
292
292
  const k = useKey.toLowerCase();
293
- if (FORBIDDEN.has(k)) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO" }], "E_PROTO");
294
293
  const kind = kindOf(k);
295
294
  if (kind.indexed) {
296
295
  throw new ConfigError(
@@ -791,7 +790,13 @@ function makeLookup(merged, envSnapshot) {
791
790
  function expandDeepStrings(obj, lookup, ctx, path = []) {
792
791
  if (typeof obj === "string") {
793
792
  const top = (path[0] ?? "<root>").toLowerCase();
794
- return expandLeaf(obj, lookup, { ...ctx, from: ctx.fromMap.get(top) ?? "config" }, path.map((p) => p.toLowerCase()));
793
+ const from = ctx.fromMap.get(top) ?? "config";
794
+ try {
795
+ return expandLeaf(obj, lookup, { ...ctx, from }, path.map((p) => p.toLowerCase()));
796
+ } catch (e) {
797
+ if (from === "env") return obj;
798
+ throw e;
799
+ }
795
800
  }
796
801
  if (Array.isArray(obj)) return obj.map((x, i) => expandDeepStrings(x, lookup, ctx, [...path, String(i)]));
797
802
  if (obj && typeof obj === "object") {
package/dist/cli/main.cjs CHANGED
@@ -485,12 +485,12 @@ function expandKeys(flat, opts) {
485
485
  const stripped = stripPrefix(rawKey);
486
486
  if (stripped === null) continue;
487
487
  const useKey = stripped;
488
- if (useKey.includes("__")) {
489
- if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
490
- const parts = useKey.split("__");
488
+ if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
489
+ const parts = useKey.split("__");
490
+ const cleanSplit = parts.length > 1 && parts.every((p) => p !== "");
491
+ if (cleanSplit) {
491
492
  for (const p of parts) {
492
493
  if (FORBIDDEN.has(p.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
493
- if (!p) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_EMPTY_SEGMENT" }], "E_EMPTY_SEGMENT");
494
494
  }
495
495
  const lowered = parts.map((p) => p.toLowerCase());
496
496
  const top = lowered[0];
@@ -522,7 +522,6 @@ function expandKeys(flat, opts) {
522
522
  setPath(out, lowered, value);
523
523
  } else {
524
524
  const k = useKey.toLowerCase();
525
- if (FORBIDDEN.has(k)) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO" }], "E_PROTO");
526
525
  const kind = kindOf(k);
527
526
  if (kind.indexed) {
528
527
  throw new ConfigError(
@@ -1044,7 +1043,13 @@ function makeLookup(merged, envSnapshot) {
1044
1043
  function expandDeepStrings(obj, lookup, ctx, path = []) {
1045
1044
  if (typeof obj === "string") {
1046
1045
  const top = (path[0] ?? "<root>").toLowerCase();
1047
- return expandLeaf(obj, lookup, { ...ctx, from: ctx.fromMap.get(top) ?? "config" }, path.map((p) => p.toLowerCase()));
1046
+ const from = ctx.fromMap.get(top) ?? "config";
1047
+ try {
1048
+ return expandLeaf(obj, lookup, { ...ctx, from }, path.map((p) => p.toLowerCase()));
1049
+ } catch (e) {
1050
+ if (from === "env") return obj;
1051
+ throw e;
1052
+ }
1048
1053
  }
1049
1054
  if (Array.isArray(obj)) return obj.map((x, i) => expandDeepStrings(x, lookup, ctx, [...path, String(i)]));
1050
1055
  if (obj && typeof obj === "object") {
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * CLI dispatcher (`check|init|gen|watch`). Returns the process exit code
4
+ * instead of exiting, so it's testable: 0 ok, 1 invalid, 2 usage/load error.
5
+ */
2
6
  declare function main(argv?: string[]): Promise<number>;
3
7
 
4
8
  export { main };
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * CLI dispatcher (`check|init|gen|watch`). Returns the process exit code
4
+ * instead of exiting, so it's testable: 0 ok, 1 invalid, 2 usage/load error.
5
+ */
2
6
  declare function main(argv?: string[]): Promise<number>;
3
7
 
4
8
  export { main };
package/dist/cli/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { watchSettings } from '../chunk-VTIYOSG2.js';
3
- import { settings } from '../chunk-RCO6K3WQ.js';
2
+ import { watchSettings } from '../chunk-DEP5SD4J.js';
3
+ import { settings } from '../chunk-YVJEZPPZ.js';
4
4
  import '../chunk-33M5GIKQ.js';
5
5
  import '../chunk-OVFMKC2O.js';
6
6
  import '../chunk-UFUHHR2J.js';
@@ -1,8 +1,13 @@
1
+ /** One validation failure: the key path, the winning source, and what went wrong. */
1
2
  interface ConfigIssue {
2
3
  path: string;
3
4
  from: string;
4
5
  message: string;
5
6
  }
7
+ /**
8
+ * Every settings failure. Carries machine-readable `issues` plus a `code`
9
+ * (`E_INVALID_CONFIG` by default, e.g. `E_UNKNOWN_KEY`, `E_TIMEOUT`).
10
+ */
6
11
  declare class ConfigError extends Error {
7
12
  code: string;
8
13
  issues: ConfigIssue[];
@@ -1,8 +1,13 @@
1
+ /** One validation failure: the key path, the winning source, and what went wrong. */
1
2
  interface ConfigIssue {
2
3
  path: string;
3
4
  from: string;
4
5
  message: string;
5
6
  }
7
+ /**
8
+ * Every settings failure. Carries machine-readable `issues` plus a `code`
9
+ * (`E_INVALID_CONFIG` by default, e.g. `E_UNKNOWN_KEY`, `E_TIMEOUT`).
10
+ */
6
11
  declare class ConfigError extends Error {
7
12
  code: string;
8
13
  issues: ConfigIssue[];
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Parses dotenv text into a flat map. Strips one BOM, joins multiline
3
+ * double-quoted values, lets the last duplicate win. Throws `ParseError` on
4
+ * backticks, unterminated quotes, bare keys, and empty keys.
5
+ */
1
6
  declare function parseEnvText(text: string): Record<string, string>;
2
7
 
3
8
  export { parseEnvText };
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Parses dotenv text into a flat map. Strips one BOM, joins multiline
3
+ * double-quoted values, lets the last duplicate win. Throws `ParseError` on
4
+ * backticks, unterminated quotes, bare keys, and empty keys.
5
+ */
1
6
  declare function parseEnvText(text: string): Record<string, string>;
2
7
 
3
8
  export { parseEnvText };
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Strict JSON parse (BOM-tolerant, blank reads as `{}`). Failures throw
3
+ * `ParseError` naming the source for attribution.
4
+ */
1
5
  declare function parseJsonText(text: string, from?: string): unknown;
2
6
 
3
7
  export { parseJsonText };
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Strict JSON parse (BOM-tolerant, blank reads as `{}`). Failures throw
3
+ * `ParseError` naming the source for attribution.
4
+ */
1
5
  declare function parseJsonText(text: string, from?: string): unknown;
2
6
 
3
7
  export { parseJsonText };
@@ -1,3 +1,7 @@
1
+ /**
2
+ * TOML parse via smol-toml (duplicate keys rejected). Blank reads as `{}`;
3
+ * failures throw `ParseError` naming the source.
4
+ */
1
5
  declare function parseTomlText(text: string, from?: string): unknown;
2
6
 
3
7
  export { parseTomlText };
@@ -1,3 +1,7 @@
1
+ /**
2
+ * TOML parse via smol-toml (duplicate keys rejected). Blank reads as `{}`;
3
+ * failures throw `ParseError` naming the source.
4
+ */
1
5
  declare function parseTomlText(text: string, from?: string): unknown;
2
6
 
3
7
  export { parseTomlText };
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Single-doc YAML parse (1.2, duplicate keys rejected). A trailing lone `---`
3
+ * still parses; multi-doc payloads, bad indentation, and unresolvable aliases
4
+ * throw `ParseError` naming the source. Explicit `null` reads as `{}`.
5
+ */
1
6
  declare function parseYamlText(text: string, from?: string): unknown;
2
7
 
3
8
  export { parseYamlText };
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Single-doc YAML parse (1.2, duplicate keys rejected). A trailing lone `---`
3
+ * still parses; multi-doc payloads, bad indentation, and unresolvable aliases
4
+ * throw `ParseError` naming the source. Explicit `null` reads as `{}`.
5
+ */
1
6
  declare function parseYamlText(text: string, from?: string): unknown;
2
7
 
3
8
  export { parseYamlText };
@@ -440,12 +440,12 @@ function expandKeys(flat, opts) {
440
440
  const stripped = stripPrefix(rawKey);
441
441
  if (stripped === null) continue;
442
442
  const useKey = stripped;
443
- if (useKey.includes("__")) {
444
- if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
445
- const parts = useKey.split("__");
443
+ if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
444
+ const parts = useKey.split("__");
445
+ const cleanSplit = parts.length > 1 && parts.every((p) => p !== "");
446
+ if (cleanSplit) {
446
447
  for (const p of parts) {
447
448
  if (FORBIDDEN.has(p.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
448
- if (!p) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_EMPTY_SEGMENT" }], "E_EMPTY_SEGMENT");
449
449
  }
450
450
  const lowered = parts.map((p) => p.toLowerCase());
451
451
  const top = lowered[0];
@@ -477,7 +477,6 @@ function expandKeys(flat, opts) {
477
477
  setPath(out, lowered, value);
478
478
  } else {
479
479
  const k = useKey.toLowerCase();
480
- if (FORBIDDEN.has(k)) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO" }], "E_PROTO");
481
480
  const kind = kindOf(k);
482
481
  if (kind.indexed) {
483
482
  throw new ConfigError(
@@ -949,7 +948,13 @@ function makeLookup(merged, envSnapshot) {
949
948
  function expandDeepStrings(obj, lookup, ctx, path = []) {
950
949
  if (typeof obj === "string") {
951
950
  const top = (path[0] ?? "<root>").toLowerCase();
952
- return expandLeaf(obj, lookup, { ...ctx, from: ctx.fromMap.get(top) ?? "config" }, path.map((p) => p.toLowerCase()));
951
+ const from = ctx.fromMap.get(top) ?? "config";
952
+ try {
953
+ return expandLeaf(obj, lookup, { ...ctx, from }, path.map((p) => p.toLowerCase()));
954
+ } catch (e) {
955
+ if (from === "env") return obj;
956
+ throw e;
957
+ }
953
958
  }
954
959
  if (Array.isArray(obj)) return obj.map((x, i) => expandDeepStrings(x, lookup, ctx, [...path, String(i)]));
955
960
  if (obj && typeof obj === "object") {
@@ -1,9 +1,22 @@
1
1
  export { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  export { pickPublic, settings, settingsAsync } from '../index.cjs';
3
- export { S as SettingsOptions } from '../types-BswIPp7U.cjs';
4
- export { C as ConfigError } from '../errors-IU9n4f3t.cjs';
3
+ export { S as SettingsOptions } from '../types-B_Dc2L4T.cjs';
4
+ export { C as ConfigError } from '../errors-itGaU4Q8.cjs';
5
5
 
6
- /** Explicit public subset — no spread, keys enumerated so secrets can't leak via `...cfg`. */
6
+ /**
7
+ * Next.js App Router binding (Edge-safe: no `fs`, no static `server-only` import).
8
+ * Load the full config on the server, hand the client only an explicit subset.
9
+ *
10
+ * IMPORTANT: add `import 'server-only'` at the top of YOUR server settings file
11
+ * (not inside this library — the `server-only` package throws when evaluated
12
+ * outside a React Server Component, which would break plain Node/vitest usage).
13
+ * // settings.ts (server)
14
+ * import 'server-only';
15
+ *
16
+ * @module
17
+ */
18
+
19
+ /** Explicit public subset - no spread, keys enumerated so secrets can't leak via `...cfg`. */
7
20
  declare function publicSettings<T extends object, K extends keyof T>(cfg: T, keys: readonly K[]): Pick<T, K>;
8
21
 
9
22
  export { publicSettings };
@@ -1,9 +1,22 @@
1
1
  export { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  export { pickPublic, settings, settingsAsync } from '../index.js';
3
- export { S as SettingsOptions } from '../types-BswIPp7U.js';
4
- export { C as ConfigError } from '../errors-IU9n4f3t.js';
3
+ export { S as SettingsOptions } from '../types-B_Dc2L4T.js';
4
+ export { C as ConfigError } from '../errors-itGaU4Q8.js';
5
5
 
6
- /** Explicit public subset — no spread, keys enumerated so secrets can't leak via `...cfg`. */
6
+ /**
7
+ * Next.js App Router binding (Edge-safe: no `fs`, no static `server-only` import).
8
+ * Load the full config on the server, hand the client only an explicit subset.
9
+ *
10
+ * IMPORTANT: add `import 'server-only'` at the top of YOUR server settings file
11
+ * (not inside this library — the `server-only` package throws when evaluated
12
+ * outside a React Server Component, which would break plain Node/vitest usage).
13
+ * // settings.ts (server)
14
+ * import 'server-only';
15
+ *
16
+ * @module
17
+ */
18
+
19
+ /** Explicit public subset - no spread, keys enumerated so secrets can't leak via `...cfg`. */
7
20
  declare function publicSettings<T extends object, K extends keyof T>(cfg: T, keys: readonly K[]): Pick<T, K>;
8
21
 
9
22
  export { publicSettings };
@@ -1,4 +1,4 @@
1
- export { pickPublic, settings, settingsAsync } from '../chunk-RCO6K3WQ.js';
1
+ export { pickPublic, settings, settingsAsync } from '../chunk-YVJEZPPZ.js';
2
2
  import '../chunk-33M5GIKQ.js';
3
3
  import '../chunk-OVFMKC2O.js';
4
4
  import '../chunk-UFUHHR2J.js';
@@ -440,12 +440,12 @@ function expandKeys(flat, opts) {
440
440
  const stripped = stripPrefix(rawKey);
441
441
  if (stripped === null) continue;
442
442
  const useKey = stripped;
443
- if (useKey.includes("__")) {
444
- if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
445
- const parts = useKey.split("__");
443
+ if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
444
+ const parts = useKey.split("__");
445
+ const cleanSplit = parts.length > 1 && parts.every((p) => p !== "");
446
+ if (cleanSplit) {
446
447
  for (const p of parts) {
447
448
  if (FORBIDDEN.has(p.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
448
- if (!p) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_EMPTY_SEGMENT" }], "E_EMPTY_SEGMENT");
449
449
  }
450
450
  const lowered = parts.map((p) => p.toLowerCase());
451
451
  const top = lowered[0];
@@ -477,7 +477,6 @@ function expandKeys(flat, opts) {
477
477
  setPath(out, lowered, value);
478
478
  } else {
479
479
  const k = useKey.toLowerCase();
480
- if (FORBIDDEN.has(k)) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO" }], "E_PROTO");
481
480
  const kind = kindOf(k);
482
481
  if (kind.indexed) {
483
482
  throw new ConfigError(
@@ -949,7 +948,13 @@ function makeLookup(merged, envSnapshot) {
949
948
  function expandDeepStrings(obj, lookup, ctx, path = []) {
950
949
  if (typeof obj === "string") {
951
950
  const top = (path[0] ?? "<root>").toLowerCase();
952
- return expandLeaf(obj, lookup, { ...ctx, from: ctx.fromMap.get(top) ?? "config" }, path.map((p) => p.toLowerCase()));
951
+ const from = ctx.fromMap.get(top) ?? "config";
952
+ try {
953
+ return expandLeaf(obj, lookup, { ...ctx, from }, path.map((p) => p.toLowerCase()));
954
+ } catch (e) {
955
+ if (from === "env") return obj;
956
+ throw e;
957
+ }
953
958
  }
954
959
  if (Array.isArray(obj)) return obj.map((x, i) => expandDeepStrings(x, lookup, ctx, [...path, String(i)]));
955
960
  if (obj && typeof obj === "object") {
@@ -1,10 +1,23 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  export { StandardSchemaV1 } from '@standard-schema/spec';
3
3
  export { pickPublic, settings, settingsAsync } from '../index.cjs';
4
- import { S as SettingsOptions, F as Flat } from '../types-BswIPp7U.cjs';
5
- export { C as ConfigError } from '../errors-IU9n4f3t.cjs';
4
+ import { S as SettingsOptions, F as Flat } from '../types-B_Dc2L4T.cjs';
5
+ export { C as ConfigError } from '../errors-itGaU4Q8.cjs';
6
6
 
7
- /** Validate `import.meta.env` (passed explicitly by the caller) against a schema. */
7
+ /**
8
+ * Vite binding (Edge-safe, no `fs`). Validates `import.meta.env` against a schema.
9
+ *
10
+ * Rules: never spread `import.meta.env` (it drags server secrets into the
11
+ * client bundle). Pass it as a map; file preloading belongs in `vite.config.ts`
12
+ * via `loadEnvFiles()` passed as `{ text }`.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ /**
18
+ * Validates `import.meta.env` (passed explicitly by the caller) against a schema.
19
+ * Sources and ambient env are fixed: the map is the only input.
20
+ */
8
21
  declare function viteSettings<S extends StandardSchemaV1>(opts: Omit<SettingsOptions<S>, 'sources' | 'env'>, metaEnv: Flat): StandardSchemaV1.InferOutput<S>;
9
22
 
10
23
  export { Flat, SettingsOptions, viteSettings };
@@ -1,10 +1,23 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  export { StandardSchemaV1 } from '@standard-schema/spec';
3
3
  export { pickPublic, settings, settingsAsync } from '../index.js';
4
- import { S as SettingsOptions, F as Flat } from '../types-BswIPp7U.js';
5
- export { C as ConfigError } from '../errors-IU9n4f3t.js';
4
+ import { S as SettingsOptions, F as Flat } from '../types-B_Dc2L4T.js';
5
+ export { C as ConfigError } from '../errors-itGaU4Q8.js';
6
6
 
7
- /** Validate `import.meta.env` (passed explicitly by the caller) against a schema. */
7
+ /**
8
+ * Vite binding (Edge-safe, no `fs`). Validates `import.meta.env` against a schema.
9
+ *
10
+ * Rules: never spread `import.meta.env` (it drags server secrets into the
11
+ * client bundle). Pass it as a map; file preloading belongs in `vite.config.ts`
12
+ * via `loadEnvFiles()` passed as `{ text }`.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ /**
18
+ * Validates `import.meta.env` (passed explicitly by the caller) against a schema.
19
+ * Sources and ambient env are fixed: the map is the only input.
20
+ */
8
21
  declare function viteSettings<S extends StandardSchemaV1>(opts: Omit<SettingsOptions<S>, 'sources' | 'env'>, metaEnv: Flat): StandardSchemaV1.InferOutput<S>;
9
22
 
10
23
  export { Flat, SettingsOptions, viteSettings };
@@ -1,5 +1,5 @@
1
- import { settings } from '../chunk-RCO6K3WQ.js';
2
- export { pickPublic, settings, settingsAsync } from '../chunk-RCO6K3WQ.js';
1
+ import { settings } from '../chunk-YVJEZPPZ.js';
2
+ export { pickPublic, settings, settingsAsync } from '../chunk-YVJEZPPZ.js';
3
3
  import '../chunk-33M5GIKQ.js';
4
4
  import '../chunk-OVFMKC2O.js';
5
5
  import '../chunk-UFUHHR2J.js';
package/dist/index.cjs CHANGED
@@ -440,12 +440,12 @@ function expandKeys(flat, opts) {
440
440
  const stripped = stripPrefix(rawKey);
441
441
  if (stripped === null) continue;
442
442
  const useKey = stripped;
443
- if (useKey.includes("__")) {
444
- if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
445
- const parts = useKey.split("__");
443
+ if (FORBIDDEN.has(useKey.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
444
+ const parts = useKey.split("__");
445
+ const cleanSplit = parts.length > 1 && parts.every((p) => p !== "");
446
+ if (cleanSplit) {
446
447
  for (const p of parts) {
447
448
  if (FORBIDDEN.has(p.toLowerCase())) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO: forbidden key" }], "E_PROTO");
448
- if (!p) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_EMPTY_SEGMENT" }], "E_EMPTY_SEGMENT");
449
449
  }
450
450
  const lowered = parts.map((p) => p.toLowerCase());
451
451
  const top = lowered[0];
@@ -477,7 +477,6 @@ function expandKeys(flat, opts) {
477
477
  setPath(out, lowered, value);
478
478
  } else {
479
479
  const k = useKey.toLowerCase();
480
- if (FORBIDDEN.has(k)) throw new ConfigError([{ path: rawKey, from: "keys", message: "E_PROTO" }], "E_PROTO");
481
480
  const kind = kindOf(k);
482
481
  if (kind.indexed) {
483
482
  throw new ConfigError(
@@ -978,7 +977,13 @@ function makeLookup(merged, envSnapshot) {
978
977
  function expandDeepStrings(obj, lookup, ctx, path = []) {
979
978
  if (typeof obj === "string") {
980
979
  const top = (path[0] ?? "<root>").toLowerCase();
981
- return expandLeaf(obj, lookup, { ...ctx, from: ctx.fromMap.get(top) ?? "config" }, path.map((p) => p.toLowerCase()));
980
+ const from = ctx.fromMap.get(top) ?? "config";
981
+ try {
982
+ return expandLeaf(obj, lookup, { ...ctx, from }, path.map((p) => p.toLowerCase()));
983
+ } catch (e) {
984
+ if (from === "env") return obj;
985
+ throw e;
986
+ }
982
987
  }
983
988
  if (Array.isArray(obj)) return obj.map((x, i) => expandDeepStrings(x, lookup, ctx, [...path, String(i)]));
984
989
  if (obj && typeof obj === "object") {