@iskra-bun/config-kit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @iskra-bun/config-kit
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 5c70c5b: Validation errors no longer quote the value received, which could be a secret put in the wrong variable: neither the `env*` coercers nor Zod's enum and literal messages include it.
8
+ - 840439a: Packages declare the runtime they are tested on: `engines.bun` `>=1.3.0` (the monorepo now builds and tests on Bun 1.3). `create-iskra`, a CLI that also runs under `npm create iskra`, declares `engines.node` `>=18`.
9
+
10
+ Every package is published with an npm provenance attestation (`publishConfig.provenance`), linking each version to the commit and CI run that built it.
11
+
12
+ - Updated dependencies [620da18]
13
+ - Updated dependencies [b635a2c]
14
+ - Updated dependencies [5b2b0fd]
15
+ - Updated dependencies [58d4a8f]
16
+ - Updated dependencies [5c70c5b]
17
+ - Updated dependencies [ec198d4]
18
+ - Updated dependencies [cb3ec43]
19
+ - Updated dependencies [ef2009b]
20
+ - Updated dependencies [840439a]
21
+ - Updated dependencies [dbf8817]
22
+ - Updated dependencies [3dc5581]
23
+ - Updated dependencies [9872d30]
24
+ - Updated dependencies [f2346f5]
25
+ - Updated dependencies [3579944]
26
+ - @iskra-bun/core@0.2.0
27
+
3
28
  ## 0.1.0
4
29
 
5
30
  ### Minor Changes
package/README.md CHANGED
@@ -31,7 +31,7 @@ Bun carga `.env` automaticamente en `process.env`, por lo que `loadConfig` lo re
31
31
 
32
32
  ## Documentacion
33
33
 
34
- Guia completa: [docs/config-kit.md](../../docs/config-kit.md)
34
+ Guia completa: [@iskra-bun/config-kit](https://iskra-docs.fly.dev/es/packages/config-kit/)
35
35
 
36
36
  ## Licencia
37
37
 
package/dist/index.js CHANGED
@@ -1,29 +1,36 @@
1
1
  // src/loader.ts
2
+ import { z } from "zod";
2
3
  import { ConfigError } from "@iskra-bun/core";
3
4
  function loadConfig(options) {
4
5
  const { schema, source = process.env } = options;
5
- const result = schema.safeParse(source);
6
+ const result = schema.safeParse(source, { errorMap: valueFreeErrors });
6
7
  if (!result.success) {
7
8
  const issues = result.error.issues.map((issue) => {
8
9
  const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
9
10
  return ` \u2022 ${path}: ${issue.message}`;
10
11
  });
11
- throw new ConfigError(
12
- `Config validation failed:
13
- ${issues.join("\n")}`,
14
- {
15
- context: {
16
- // Only report field names and messages — never values.
17
- fields: result.error.issues.map((i) => ({
18
- path: i.path.join(".") || "<root>",
19
- message: i.message
20
- }))
21
- }
12
+ throw new ConfigError(`Config validation failed:
13
+ ${issues.join("\n")}`, {
14
+ context: {
15
+ // Only report field names and messages — never values.
16
+ fields: result.error.issues.map((i) => ({
17
+ path: i.path.join(".") || "<root>",
18
+ message: i.message
19
+ }))
22
20
  }
23
- );
21
+ });
24
22
  }
25
23
  return deepFreeze(result.data);
26
24
  }
25
+ var valueFreeErrors = (issue, ctx) => {
26
+ if (issue.code === z.ZodIssueCode.invalid_enum_value) {
27
+ return { message: `Invalid enum value. Expected ${issue.options.map((o) => `'${String(o)}'`).join(" | ")}` };
28
+ }
29
+ if (issue.code === z.ZodIssueCode.invalid_literal) {
30
+ return { message: `Invalid literal value, expected ${JSON.stringify(issue.expected)}` };
31
+ }
32
+ return { message: ctx.defaultError };
33
+ };
27
34
  function deepFreeze(obj) {
28
35
  if (obj === null || typeof obj !== "object") return obj;
29
36
  for (const key of Object.keys(obj)) {
@@ -36,61 +43,61 @@ function deepFreeze(obj) {
36
43
  }
37
44
 
38
45
  // src/coercers.ts
39
- import { z } from "zod";
40
- var envBool = z.string().transform((val, ctx) => {
46
+ import { z as z2 } from "zod";
47
+ var envBool = z2.string().transform((val, ctx) => {
41
48
  const lower = val.toLowerCase().trim();
42
49
  if (lower === "true" || lower === "1" || lower === "yes") return true;
43
50
  if (lower === "false" || lower === "0" || lower === "no") return false;
44
51
  ctx.addIssue({
45
- code: z.ZodIssueCode.custom,
46
- message: `Expected boolean-like string ("true"/"1"/"yes"/"false"/"0"/"no"), received "${val}"`
52
+ code: z2.ZodIssueCode.custom,
53
+ message: 'Expected boolean-like string ("true"/"1"/"yes"/"false"/"0"/"no")'
47
54
  });
48
- return z.NEVER;
49
- }).pipe(z.boolean());
50
- var envNumber = z.string().transform((val, ctx) => {
55
+ return z2.NEVER;
56
+ }).pipe(z2.boolean());
57
+ var envNumber = z2.string().transform((val, ctx) => {
51
58
  const trimmed = val.trim();
52
59
  const n = Number(trimmed);
53
60
  if (trimmed === "" || !Number.isFinite(n)) {
54
61
  ctx.addIssue({
55
- code: z.ZodIssueCode.custom,
56
- message: `Expected a finite number, received "${val}"`
62
+ code: z2.ZodIssueCode.custom,
63
+ message: "Expected a finite number"
57
64
  });
58
- return z.NEVER;
65
+ return z2.NEVER;
59
66
  }
60
67
  return n;
61
- }).pipe(z.number());
62
- var envPort = z.string().transform((val, ctx) => {
68
+ }).pipe(z2.number());
69
+ var envPort = z2.string().transform((val, ctx) => {
63
70
  const n = Number(val);
64
71
  if (!Number.isInteger(n) || n < 1 || n > 65535) {
65
72
  ctx.addIssue({
66
- code: z.ZodIssueCode.custom,
67
- message: `Expected a port number (1\u201365535), received "${val}"`
73
+ code: z2.ZodIssueCode.custom,
74
+ message: "Expected a port number (1\u201365535)"
68
75
  });
69
- return z.NEVER;
76
+ return z2.NEVER;
70
77
  }
71
78
  return n;
72
- }).pipe(z.number().int().min(1).max(65535));
79
+ }).pipe(z2.number().int().min(1).max(65535));
73
80
  function envEnum(values) {
74
- return z.string().transform((val, ctx) => {
81
+ return z2.string().transform((val, ctx) => {
75
82
  if (!values.includes(val)) {
76
83
  ctx.addIssue({
77
- code: z.ZodIssueCode.custom,
78
- message: `Expected one of [${values.map((v) => `"${v}"`).join(", ")}], received "${val}"`
84
+ code: z2.ZodIssueCode.custom,
85
+ message: `Expected one of [${values.map((v) => `"${v}"`).join(", ")}]`
79
86
  });
80
- return z.NEVER;
87
+ return z2.NEVER;
81
88
  }
82
89
  return val;
83
- }).pipe(z.enum(values));
90
+ }).pipe(z2.enum(values));
84
91
  }
85
92
 
86
93
  // src/index.ts
87
- import { z as z2 } from "zod";
94
+ import { z as z3 } from "zod";
88
95
  export {
89
96
  envBool,
90
97
  envEnum,
91
98
  envNumber,
92
99
  envPort,
93
100
  loadConfig,
94
- z2 as z
101
+ z3 as z
95
102
  };
96
103
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/loader.ts","../src/coercers.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport { ConfigError } from '@iskra-bun/core';\n\nexport interface LoadConfigOptions<TSchema extends z.ZodTypeAny> {\n /** The Zod schema to validate the source against. */\n schema: TSchema;\n /**\n * The environment source to validate.\n * Defaults to `process.env`.\n */\n source?: Record<string, string | undefined>;\n}\n\n/**\n * Validates `source` (defaults to `process.env`) against the provided Zod schema\n * and returns a deep-frozen, typed config object.\n *\n * On failure, throws a `ConfigError` listing each invalid or missing field by name\n * and reason — never echoing secret values.\n */\nexport function loadConfig<TSchema extends z.ZodTypeAny>(\n options: LoadConfigOptions<TSchema>,\n): z.output<TSchema> {\n const { schema, source = process.env } = options;\n\n const result = schema.safeParse(source);\n\n if (!result.success) {\n const issues = result.error.issues.map(issue => {\n const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';\n return ` • ${path}: ${issue.message}`;\n });\n\n throw new ConfigError(\n `Config validation failed:\\n${issues.join('\\n')}`,\n {\n context: {\n // Only report field names and messages — never values.\n fields: result.error.issues.map(i => ({\n path: i.path.join('.') || '<root>',\n message: i.message,\n })),\n },\n },\n );\n }\n\n return deepFreeze(result.data) as z.output<TSchema>;\n}\n\n/**\n * Recursively freezes an object so the returned config is truly immutable.\n */\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== 'object') return obj;\n\n for (const key of Object.keys(obj as object)) {\n const value = (obj as Record<string, unknown>)[key];\n if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {\n deepFreeze(value);\n }\n }\n\n return Object.freeze(obj);\n}\n","import { z } from 'zod';\n\n/**\n * Coerce a string env var to boolean.\n * Accepts \"true\"/\"1\"/\"yes\" as true, \"false\"/\"0\"/\"no\" as false.\n * Rejects any other value.\n */\nexport const envBool = z\n .string()\n .transform((val, ctx) => {\n const lower = val.toLowerCase().trim();\n if (lower === 'true' || lower === '1' || lower === 'yes') return true;\n if (lower === 'false' || lower === '0' || lower === 'no') return false;\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Expected boolean-like string (\"true\"/\"1\"/\"yes\"/\"false\"/\"0\"/\"no\"), received \"${val}\"`,\n });\n return z.NEVER;\n })\n .pipe(z.boolean());\n\n/**\n * Coerce a string env var to a finite number.\n */\nexport const envNumber = z\n .string()\n .transform((val, ctx) => {\n const trimmed = val.trim();\n const n = Number(trimmed);\n if (trimmed === '' || !Number.isFinite(n)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Expected a finite number, received \"${val}\"`,\n });\n return z.NEVER;\n }\n return n;\n })\n .pipe(z.number());\n\n/**\n * Coerce a string env var to a valid TCP port (1–65535).\n */\nexport const envPort = z\n .string()\n .transform((val, ctx) => {\n const n = Number(val);\n if (!Number.isInteger(n) || n < 1 || n > 65535) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Expected a port number (1–65535), received \"${val}\"`,\n });\n return z.NEVER;\n }\n return n;\n })\n .pipe(z.number().int().min(1).max(65535));\n\n/**\n * Coerce a string env var to one of the provided literal values.\n * Usage: envEnum(['development', 'production', 'test'])\n */\nexport function envEnum<T extends string>(values: readonly [T, ...T[]]) {\n return z\n .string()\n .transform((val, ctx) => {\n if (!(values as readonly string[]).includes(val)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Expected one of [${values.map(v => `\"${v}\"`).join(', ')}], received \"${val}\"`,\n });\n return z.NEVER;\n }\n return val as T;\n })\n .pipe(z.enum(values));\n}\n","export { loadConfig } from './loader';\nexport type { LoadConfigOptions } from './loader';\nexport { envBool, envNumber, envPort, envEnum } from './coercers';\nexport { z } from 'zod';\n"],"mappings":"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","names":["z"]}
1
+ {"version":3,"sources":["../src/loader.ts","../src/coercers.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport { ConfigError } from '@iskra-bun/core';\n\nexport interface LoadConfigOptions<TSchema extends z.ZodTypeAny> {\n /** The Zod schema to validate the source against. */\n schema: TSchema;\n /**\n * The environment source to validate.\n * Defaults to `process.env`.\n */\n source?: Record<string, string | undefined>;\n}\n\n/**\n * Validates `source` (defaults to `process.env`) against the provided Zod schema\n * and returns a deep-frozen, typed config object.\n *\n * On failure, throws a `ConfigError` listing each invalid or missing field by name\n * and reason — never echoing secret values.\n */\nexport function loadConfig<TSchema extends z.ZodTypeAny>(options: LoadConfigOptions<TSchema>): z.output<TSchema> {\n const { schema, source = process.env } = options;\n\n const result = schema.safeParse(source, { errorMap: valueFreeErrors });\n\n if (!result.success) {\n const issues = result.error.issues.map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';\n return ` • ${path}: ${issue.message}`;\n });\n\n throw new ConfigError(`Config validation failed:\\n${issues.join('\\n')}`, {\n context: {\n // Only report field names and messages — never values.\n fields: result.error.issues.map((i) => ({\n path: i.path.join('.') || '<root>',\n message: i.message,\n })),\n },\n });\n }\n\n return deepFreeze(result.data) as z.output<TSchema>;\n}\n\n/**\n * Zod's own messages for enums and literals quote the value received\n * (\"…, received 'hunter2'\"); these say what was expected instead.\n */\nconst valueFreeErrors: z.ZodErrorMap = (issue, ctx) => {\n if (issue.code === z.ZodIssueCode.invalid_enum_value) {\n return { message: `Invalid enum value. Expected ${issue.options.map((o) => `'${String(o)}'`).join(' | ')}` };\n }\n if (issue.code === z.ZodIssueCode.invalid_literal) {\n return { message: `Invalid literal value, expected ${JSON.stringify(issue.expected)}` };\n }\n return { message: ctx.defaultError };\n};\n\n/**\n * Recursively freezes an object so the returned config is truly immutable.\n */\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== 'object') return obj;\n\n for (const key of Object.keys(obj as object)) {\n const value = (obj as Record<string, unknown>)[key];\n if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {\n deepFreeze(value);\n }\n }\n\n return Object.freeze(obj);\n}\n","import { z } from 'zod';\n\n// The messages never include the value: it may be a secret (a password put in\n// the wrong variable), and loadConfig() reports them in errors and logs.\n\n/**\n * Coerce a string env var to boolean.\n * Accepts \"true\"/\"1\"/\"yes\" as true, \"false\"/\"0\"/\"no\" as false.\n * Rejects any other value.\n */\nexport const envBool = z\n .string()\n .transform((val, ctx) => {\n const lower = val.toLowerCase().trim();\n if (lower === 'true' || lower === '1' || lower === 'yes') return true;\n if (lower === 'false' || lower === '0' || lower === 'no') return false;\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected boolean-like string (\"true\"/\"1\"/\"yes\"/\"false\"/\"0\"/\"no\")',\n });\n return z.NEVER;\n })\n .pipe(z.boolean());\n\n/**\n * Coerce a string env var to a finite number.\n */\nexport const envNumber = z\n .string()\n .transform((val, ctx) => {\n const trimmed = val.trim();\n const n = Number(trimmed);\n if (trimmed === '' || !Number.isFinite(n)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected a finite number',\n });\n return z.NEVER;\n }\n return n;\n })\n .pipe(z.number());\n\n/**\n * Coerce a string env var to a valid TCP port (1–65535).\n */\nexport const envPort = z\n .string()\n .transform((val, ctx) => {\n const n = Number(val);\n if (!Number.isInteger(n) || n < 1 || n > 65535) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected a port number (1–65535)',\n });\n return z.NEVER;\n }\n return n;\n })\n .pipe(z.number().int().min(1).max(65535));\n\n/**\n * Coerce a string env var to one of the provided literal values.\n * Usage: envEnum(['development', 'production', 'test'])\n */\nexport function envEnum<T extends string>(values: readonly [T, ...T[]]) {\n return z\n .string()\n .transform((val, ctx) => {\n if (!(values as readonly string[]).includes(val)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Expected one of [${values.map((v) => `\"${v}\"`).join(', ')}]`,\n });\n return z.NEVER;\n }\n return val as T;\n })\n .pipe(z.enum(values));\n}\n","export { loadConfig } from './loader';\nexport type { LoadConfigOptions } from './loader';\nexport { envBool, envNumber, envPort, envEnum } from './coercers';\nexport { z } from 'zod';\n"],"mappings":"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","names":["z","z"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iskra-bun/config-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Carga y validacion de configuracion de entorno con Zod para Iskra.",
5
5
  "keywords": [
6
6
  "iskra",
@@ -20,6 +20,9 @@
20
20
  },
21
21
  "homepage": "https://github.com/fearful/iskra/tree/main/packages/config-kit#readme",
22
22
  "bugs": "https://github.com/fearful/iskra/issues",
23
+ "engines": {
24
+ "bun": ">=1.3.0"
25
+ },
23
26
  "type": "module",
24
27
  "main": "./dist/index.js",
25
28
  "module": "./dist/index.js",
@@ -47,7 +50,7 @@
47
50
  "build": "tsup --config ../../tsup.config.ts"
48
51
  },
49
52
  "dependencies": {
50
- "@iskra-bun/core": "0.1.1",
53
+ "@iskra-bun/core": "^0.2.0",
51
54
  "zod": "^3.24.1"
52
55
  },
53
56
  "devDependencies": {
package/src/coercers.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ // The messages never include the value: it may be a secret (a password put in
4
+ // the wrong variable), and loadConfig() reports them in errors and logs.
5
+
3
6
  /**
4
7
  * Coerce a string env var to boolean.
5
8
  * Accepts "true"/"1"/"yes" as true, "false"/"0"/"no" as false.
@@ -13,7 +16,7 @@ export const envBool = z
13
16
  if (lower === 'false' || lower === '0' || lower === 'no') return false;
14
17
  ctx.addIssue({
15
18
  code: z.ZodIssueCode.custom,
16
- message: `Expected boolean-like string ("true"/"1"/"yes"/"false"/"0"/"no"), received "${val}"`,
19
+ message: 'Expected boolean-like string ("true"/"1"/"yes"/"false"/"0"/"no")',
17
20
  });
18
21
  return z.NEVER;
19
22
  })
@@ -30,7 +33,7 @@ export const envNumber = z
30
33
  if (trimmed === '' || !Number.isFinite(n)) {
31
34
  ctx.addIssue({
32
35
  code: z.ZodIssueCode.custom,
33
- message: `Expected a finite number, received "${val}"`,
36
+ message: 'Expected a finite number',
34
37
  });
35
38
  return z.NEVER;
36
39
  }
@@ -48,7 +51,7 @@ export const envPort = z
48
51
  if (!Number.isInteger(n) || n < 1 || n > 65535) {
49
52
  ctx.addIssue({
50
53
  code: z.ZodIssueCode.custom,
51
- message: `Expected a port number (1–65535), received "${val}"`,
54
+ message: 'Expected a port number (1–65535)',
52
55
  });
53
56
  return z.NEVER;
54
57
  }
@@ -67,7 +70,7 @@ export function envEnum<T extends string>(values: readonly [T, ...T[]]) {
67
70
  if (!(values as readonly string[]).includes(val)) {
68
71
  ctx.addIssue({
69
72
  code: z.ZodIssueCode.custom,
70
- message: `Expected one of [${values.map(v => `"${v}"`).join(', ')}], received "${val}"`,
73
+ message: `Expected one of [${values.map((v) => `"${v}"`).join(', ')}]`,
71
74
  });
72
75
  return z.NEVER;
73
76
  }
package/src/loader.ts CHANGED
@@ -18,36 +18,45 @@ export interface LoadConfigOptions<TSchema extends z.ZodTypeAny> {
18
18
  * On failure, throws a `ConfigError` listing each invalid or missing field by name
19
19
  * and reason — never echoing secret values.
20
20
  */
21
- export function loadConfig<TSchema extends z.ZodTypeAny>(
22
- options: LoadConfigOptions<TSchema>,
23
- ): z.output<TSchema> {
21
+ export function loadConfig<TSchema extends z.ZodTypeAny>(options: LoadConfigOptions<TSchema>): z.output<TSchema> {
24
22
  const { schema, source = process.env } = options;
25
23
 
26
- const result = schema.safeParse(source);
24
+ const result = schema.safeParse(source, { errorMap: valueFreeErrors });
27
25
 
28
26
  if (!result.success) {
29
- const issues = result.error.issues.map(issue => {
27
+ const issues = result.error.issues.map((issue) => {
30
28
  const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
31
29
  return ` • ${path}: ${issue.message}`;
32
30
  });
33
31
 
34
- throw new ConfigError(
35
- `Config validation failed:\n${issues.join('\n')}`,
36
- {
37
- context: {
38
- // Only report field names and messages — never values.
39
- fields: result.error.issues.map(i => ({
40
- path: i.path.join('.') || '<root>',
41
- message: i.message,
42
- })),
43
- },
32
+ throw new ConfigError(`Config validation failed:\n${issues.join('\n')}`, {
33
+ context: {
34
+ // Only report field names and messages — never values.
35
+ fields: result.error.issues.map((i) => ({
36
+ path: i.path.join('.') || '<root>',
37
+ message: i.message,
38
+ })),
44
39
  },
45
- );
40
+ });
46
41
  }
47
42
 
48
43
  return deepFreeze(result.data) as z.output<TSchema>;
49
44
  }
50
45
 
46
+ /**
47
+ * Zod's own messages for enums and literals quote the value received
48
+ * ("…, received 'hunter2'"); these say what was expected instead.
49
+ */
50
+ const valueFreeErrors: z.ZodErrorMap = (issue, ctx) => {
51
+ if (issue.code === z.ZodIssueCode.invalid_enum_value) {
52
+ return { message: `Invalid enum value. Expected ${issue.options.map((o) => `'${String(o)}'`).join(' | ')}` };
53
+ }
54
+ if (issue.code === z.ZodIssueCode.invalid_literal) {
55
+ return { message: `Invalid literal value, expected ${JSON.stringify(issue.expected)}` };
56
+ }
57
+ return { message: ctx.defaultError };
58
+ };
59
+
51
60
  /**
52
61
  * Recursively freezes an object so the returned config is truly immutable.
53
62
  */