@nifrajs/env 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nifra Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @nifrajs/env
2
+
3
+ Typed, validated environment variables. Define a schema once at startup; get a **frozen, typed**
4
+ object — or a **loud boot-time failure listing every problem at once**, so a misconfigured deploy
5
+ fails immediately instead of erroring on the first request that touches a bad var. The config half
6
+ of nifra's "validate at every boundary" rule. Dependency-free, edge-safe.
7
+
8
+ ```ts
9
+ // env.ts — imported once at startup
10
+ import { defineEnv, env } from "@nifrajs/env"
11
+
12
+ export const ENV = defineEnv({
13
+ DATABASE_URL: env.url(),
14
+ PORT: env.port({ default: 3000 }),
15
+ NODE_ENV: env.enum(["development", "production", "test"], { default: "development" }),
16
+ STRIPE_SECRET: env.string(),
17
+ DEBUG: env.boolean({ default: false }),
18
+ SENTRY_DSN: env.url({ optional: true }),
19
+ })
20
+
21
+ // ENV.PORT → number
22
+ // ENV.NODE_ENV → "development" | "production" | "test"
23
+ // ENV.DEBUG → boolean
24
+ // ENV.SENTRY_DSN → string | undefined
25
+ ```
26
+
27
+ A missing/invalid var fails at import:
28
+
29
+ ```
30
+ [nifra/env] invalid environment — 2 problem(s):
31
+ DATABASE_URL: must be a valid URL
32
+ STRIPE_SECRET: is required
33
+
34
+ Set the variable(s) above and restart.
35
+ ```
36
+
37
+ ## Coercing helpers
38
+
39
+ Every value in `process.env` is a `string | undefined`; these turn it into the value you want:
40
+
41
+ | helper | type | notes |
42
+ | --- | --- | --- |
43
+ | `env.string({ default?, optional? })` | `string` | empty string counts as unset |
44
+ | `env.number({ default?, optional? })` | `number` | finite, coerced |
45
+ | `env.port({ default? })` | `number` | integer 1–65535 |
46
+ | `env.boolean({ default? })` | `boolean` | `true`/`1`/`yes`/`on` vs `false`/`0`/`no`/`off`/empty |
47
+ | `env.enum([...], { default? })` | union | one of the listed values |
48
+ | `env.url({ default?, optional? })` | `string` | parses with WHATWG `URL`, returns the normalized href |
49
+
50
+ `optional: true` makes the type `T | undefined`; `default` supplies a fallback when unset.
51
+
52
+ ## Bring your own validator
53
+
54
+ `defineEnv` accepts **any Standard Schema** (`t` from `@nifrajs/schema`, zod, valibot), not just the
55
+ `env.*` helpers — handy for a shape the helpers don't cover. Only the `env.*` helpers coerce from
56
+ strings, though: a plain `t.number()` would see the raw string, so reach for `env.number()` when you
57
+ need coercion.
58
+
59
+ ## Edge runtimes
60
+
61
+ `defineEnv` reads `process.env` by default (Bun/Node). Cloudflare Workers has no `process.env` —
62
+ pass the request's bindings: `defineEnv(shape, { source: env })`.
63
+
64
+ ## Secret safety
65
+
66
+ Error messages name the offending variable and the reason — **never its value** (it may be a
67
+ secret). `defineEnv` returns a frozen object, so a validated config can't be mutated downstream.
@@ -0,0 +1,39 @@
1
+ import type { InferOutput, StandardSchemaV1 } from "./schema.js";
2
+ /** A schema per variable name. */
3
+ export type EnvShape = Record<string, StandardSchemaV1>;
4
+ /** The frozen, validated result — each key typed by its schema's output. */
5
+ export type EnvResult<S extends EnvShape> = {
6
+ readonly [K in keyof S]: InferOutput<S[K]>;
7
+ };
8
+ export interface DefineEnvOptions {
9
+ /**
10
+ * Where to read variables from. Defaults to `process.env` (Bun/Node). On Cloudflare Workers there
11
+ * is no `process.env` — pass the request's `env` bindings: `defineEnv(shape, { source: env })`.
12
+ */
13
+ readonly source?: Record<string, string | undefined>;
14
+ }
15
+ /**
16
+ * Validate the environment against a schema and return a **frozen, typed** object — or throw at
17
+ * startup listing **every** problem at once (not just the first), so a misconfigured deploy fails
18
+ * loud and immediately instead of erroring on the first request that touches a bad var. This is the
19
+ * boot-time half of nifra's "validate at every boundary" rule, for config.
20
+ *
21
+ * ```ts
22
+ * // env.ts — imported once at startup
23
+ * import { defineEnv, env } from "@nifrajs/env"
24
+ * export const ENV = defineEnv({
25
+ * DATABASE_URL: env.url(),
26
+ * PORT: env.port({ default: 3000 }),
27
+ * NODE_ENV: env.enum(["development", "production", "test"], { default: "development" }),
28
+ * STRIPE_SECRET: env.string(),
29
+ * DEBUG: env.boolean({ default: false }),
30
+ * })
31
+ * // ENV.PORT: number, ENV.NODE_ENV: "development" | "production" | "test", … all validated.
32
+ * ```
33
+ *
34
+ * Errors name the offending variable and the reason — never its value (it may be a secret). BYO
35
+ * validators (`t`, zod, valibot) work too; only the `env.*` helpers coerce from strings, so a plain
36
+ * `t.number()` would see the raw string — use `env.number()` for coercion.
37
+ */
38
+ export declare function defineEnv<S extends EnvShape>(shape: S, options?: DefineEnvOptions): EnvResult<S>;
39
+ //# sourceMappingURL=define.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEhE,kCAAkC;AAClC,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;AAEvD,4EAA4E;AAC5E,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,QAAQ,IAAI;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAA;AAE1F,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC,CAAC,CAAC,CA8BhG"}
package/dist/define.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Validate the environment against a schema and return a **frozen, typed** object — or throw at
3
+ * startup listing **every** problem at once (not just the first), so a misconfigured deploy fails
4
+ * loud and immediately instead of erroring on the first request that touches a bad var. This is the
5
+ * boot-time half of nifra's "validate at every boundary" rule, for config.
6
+ *
7
+ * ```ts
8
+ * // env.ts — imported once at startup
9
+ * import { defineEnv, env } from "@nifrajs/env"
10
+ * export const ENV = defineEnv({
11
+ * DATABASE_URL: env.url(),
12
+ * PORT: env.port({ default: 3000 }),
13
+ * NODE_ENV: env.enum(["development", "production", "test"], { default: "development" }),
14
+ * STRIPE_SECRET: env.string(),
15
+ * DEBUG: env.boolean({ default: false }),
16
+ * })
17
+ * // ENV.PORT: number, ENV.NODE_ENV: "development" | "production" | "test", … all validated.
18
+ * ```
19
+ *
20
+ * Errors name the offending variable and the reason — never its value (it may be a secret). BYO
21
+ * validators (`t`, zod, valibot) work too; only the `env.*` helpers coerce from strings, so a plain
22
+ * `t.number()` would see the raw string — use `env.number()` for coercion.
23
+ */
24
+ export function defineEnv(shape, options) {
25
+ const source = options?.source ??
26
+ globalThis.process?.env ??
27
+ {};
28
+ const out = {};
29
+ const problems = [];
30
+ for (const key of Object.keys(shape)) {
31
+ const schema = shape[key];
32
+ const result = schema["~standard"].validate(source[key]);
33
+ if (result instanceof Promise) {
34
+ // Env validation must be synchronous (it runs once, at import) — an async validator is a
35
+ // configuration mistake, surfaced as a problem rather than an unhandled promise.
36
+ problems.push(` ${key}: async validators are not supported in defineEnv`);
37
+ continue;
38
+ }
39
+ if (result.issues !== undefined) {
40
+ for (const i of result.issues)
41
+ problems.push(` ${key}: ${i.message}`);
42
+ }
43
+ else {
44
+ out[key] = result.value;
45
+ }
46
+ }
47
+ if (problems.length > 0) {
48
+ throw new Error(`[nifra/env] invalid environment — ${problems.length} problem(s):\n${problems.join("\n")}\n\nSet the variable(s) above and restart.`);
49
+ }
50
+ return Object.freeze(out);
51
+ }
52
+ //# sourceMappingURL=define.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define.js","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,SAAS,CAAqB,KAAQ,EAAE,OAA0B;IAChF,MAAM,MAAM,GACV,OAAO,EAAE,MAAM;QACd,UAAyE,CAAC,OAAO,EAAE,GAAG;QACvF,EAAE,CAAA;IACJ,MAAM,GAAG,GAA4B,EAAE,CAAA;IACvC,MAAM,QAAQ,GAAa,EAAE,CAAA;IAE7B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAqB,CAAA;QAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QACxD,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;YAC9B,yFAAyF;YACzF,iFAAiF;YACjF,QAAQ,CAAC,IAAI,CAAC,KAAK,GAAG,mDAAmD,CAAC,CAAA;YAC1E,SAAQ;QACV,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAChC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM;gBAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;QACxE,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAA;QACzB,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,qCAAqC,QAAQ,CAAC,MAAM,iBAAiB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,4CAA4C,CACrI,CAAA;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAiB,CAAA;AAC3C,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Coercing env validators. Every value in `process.env` is a `string | undefined`, so these turn
3
+ * that into the typed value you actually want — with defaults, optionality, and clear errors. Each
4
+ * is a Standard Schema, so they slot straight into {@link defineEnv} (and could be used anywhere a
5
+ * Standard Schema is accepted).
6
+ *
7
+ * Error messages are **value-free** — they say what was wrong, never echo the variable's value
8
+ * (it may be a secret). {@link defineEnv} prepends the variable name.
9
+ */
10
+ import { type StandardSchemaV1 } from "./schema.js";
11
+ interface Base<T> {
12
+ /** Used when the variable is unset/empty. Makes the variable optional-with-a-fallback. */
13
+ readonly default?: T;
14
+ }
15
+ interface Optional {
16
+ /** When true, an unset variable yields `undefined` instead of being an error. */
17
+ readonly optional?: boolean;
18
+ }
19
+ /** A required (or defaulted/optional) non-empty string. */
20
+ export declare function string(opts?: Base<string> & Optional): StandardSchemaV1<string | undefined>;
21
+ /** A finite number, coerced from its decimal string. */
22
+ export declare function number(opts?: Base<number> & Optional): StandardSchemaV1<number | undefined>;
23
+ /** A TCP port: an integer in 1–65535. */
24
+ export declare function port(opts?: Base<number>): StandardSchemaV1<number>;
25
+ /** A boolean: `true`/`1`/`yes`/`on` → true; `false`/`0`/`no`/`off`/empty → false (case-insensitive). */
26
+ export declare function boolean(opts?: Base<boolean>): StandardSchemaV1<boolean>;
27
+ /** One of a fixed set of string values. */
28
+ export declare function enumValue<const V extends readonly [string, ...string[]]>(values: V, opts?: Base<V[number]>): StandardSchemaV1<V[number]>;
29
+ /** A valid absolute URL (parses with the WHATWG `URL`). Returns the normalized href string. */
30
+ export declare function url(opts?: Base<string> & Optional): StandardSchemaV1<string | undefined>;
31
+ /** The coercing env validators, grouped — `env.string()`, `env.port()`, `env.enum([...])`, … */
32
+ export declare const env: {
33
+ string: typeof string;
34
+ number: typeof number;
35
+ port: typeof port;
36
+ boolean: typeof boolean;
37
+ enum: typeof enumValue;
38
+ url: typeof url;
39
+ };
40
+ export {};
41
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAErE,UAAU,IAAI,CAAC,CAAC;IACd,0FAA0F;IAC1F,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;CACrB;AACD,UAAU,QAAQ;IAChB,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAID,2DAA2D;AAC3D,wBAAgB,MAAM,CAAC,IAAI,GAAE,IAAI,CAAC,MAAM,CAAC,GAAG,QAAa,GAAG,gBAAgB,CAAC,MAAM,GAAG,SAAS,CAAC,CAS/F;AAED,wDAAwD;AACxD,wBAAgB,MAAM,CAAC,IAAI,GAAE,IAAI,CAAC,MAAM,CAAC,GAAG,QAAa,GAAG,gBAAgB,CAAC,MAAM,GAAG,SAAS,CAAC,CAW/F;AAED,yCAAyC;AACzC,wBAAgB,IAAI,CAAC,IAAI,GAAE,IAAI,CAAC,MAAM,CAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAYtE;AAKD,wGAAwG;AACxG,wBAAgB,OAAO,CAAC,IAAI,GAAE,IAAI,CAAC,OAAO,CAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAW3E;AAED,2CAA2C;AAC3C,wBAAgB,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,EACtE,MAAM,EAAE,CAAC,EACT,IAAI,GAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAM,GACzB,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAU7B;AAED,+FAA+F;AAC/F,wBAAgB,GAAG,CAAC,IAAI,GAAE,IAAI,CAAC,MAAM,CAAC,GAAG,QAAa,GAAG,gBAAgB,CAAC,MAAM,GAAG,SAAS,CAAC,CAa5F;AAED,gGAAgG;AAChG,eAAO,MAAM,GAAG;;;;;;;CAOf,CAAA"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Coercing env validators. Every value in `process.env` is a `string | undefined`, so these turn
3
+ * that into the typed value you actually want — with defaults, optionality, and clear errors. Each
4
+ * is a Standard Schema, so they slot straight into {@link defineEnv} (and could be used anywhere a
5
+ * Standard Schema is accepted).
6
+ *
7
+ * Error messages are **value-free** — they say what was wrong, never echo the variable's value
8
+ * (it may be a secret). {@link defineEnv} prepends the variable name.
9
+ */
10
+ import { envSchema, issue } from "./schema.js";
11
+ const isUnset = (raw) => raw === undefined || raw === "";
12
+ /** A required (or defaulted/optional) non-empty string. */
13
+ export function string(opts = {}) {
14
+ return envSchema((raw) => {
15
+ if (isUnset(raw)) {
16
+ if (opts.default !== undefined)
17
+ return { value: opts.default };
18
+ if (opts.optional)
19
+ return { value: undefined };
20
+ return issue("is required");
21
+ }
22
+ return { value: raw };
23
+ });
24
+ }
25
+ /** A finite number, coerced from its decimal string. */
26
+ export function number(opts = {}) {
27
+ return envSchema((raw) => {
28
+ if (isUnset(raw)) {
29
+ if (opts.default !== undefined)
30
+ return { value: opts.default };
31
+ if (opts.optional)
32
+ return { value: undefined };
33
+ return issue("is required");
34
+ }
35
+ const n = Number(raw);
36
+ if (!Number.isFinite(n))
37
+ return issue("must be a number");
38
+ return { value: n };
39
+ });
40
+ }
41
+ /** A TCP port: an integer in 1–65535. */
42
+ export function port(opts = {}) {
43
+ return envSchema((raw) => {
44
+ if (isUnset(raw)) {
45
+ if (opts.default !== undefined)
46
+ return { value: opts.default };
47
+ return issue("is required");
48
+ }
49
+ const n = Number(raw);
50
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
51
+ return issue("must be an integer port in 1–65535");
52
+ }
53
+ return { value: n };
54
+ });
55
+ }
56
+ const TRUE = new Set(["true", "1", "yes", "on"]);
57
+ const FALSE = new Set(["false", "0", "no", "off", ""]);
58
+ /** A boolean: `true`/`1`/`yes`/`on` → true; `false`/`0`/`no`/`off`/empty → false (case-insensitive). */
59
+ export function boolean(opts = {}) {
60
+ return envSchema((raw) => {
61
+ if (raw === undefined) {
62
+ if (opts.default !== undefined)
63
+ return { value: opts.default };
64
+ return issue("is required");
65
+ }
66
+ const v = raw.trim().toLowerCase();
67
+ if (TRUE.has(v))
68
+ return { value: true };
69
+ if (FALSE.has(v))
70
+ return { value: false };
71
+ return issue("must be a boolean (true/false/1/0/yes/no/on/off)");
72
+ });
73
+ }
74
+ /** One of a fixed set of string values. */
75
+ export function enumValue(values, opts = {}) {
76
+ const set = new Set(values);
77
+ return envSchema((raw) => {
78
+ if (raw === undefined || raw === "") {
79
+ if (opts.default !== undefined)
80
+ return { value: opts.default };
81
+ return issue(`is required (one of: ${values.join(", ")})`);
82
+ }
83
+ if (!set.has(raw))
84
+ return issue(`must be one of: ${values.join(", ")}`);
85
+ return { value: raw };
86
+ });
87
+ }
88
+ /** A valid absolute URL (parses with the WHATWG `URL`). Returns the normalized href string. */
89
+ export function url(opts = {}) {
90
+ return envSchema((raw) => {
91
+ if (raw === undefined || raw === "") {
92
+ if (opts.default !== undefined)
93
+ return { value: opts.default };
94
+ if (opts.optional)
95
+ return { value: undefined };
96
+ return issue("is required");
97
+ }
98
+ try {
99
+ return { value: new URL(raw).href };
100
+ }
101
+ catch {
102
+ return issue("must be a valid URL");
103
+ }
104
+ });
105
+ }
106
+ /** The coercing env validators, grouped — `env.string()`, `env.port()`, `env.enum([...])`, … */
107
+ export const env = {
108
+ string,
109
+ number,
110
+ port,
111
+ boolean,
112
+ enum: enumValue,
113
+ url,
114
+ };
115
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @nifrajs/env — typed, validated environment variables. Define a schema once at startup; get a
3
+ * frozen typed object, or a loud boot-time failure listing every problem. Coercing helpers
4
+ * (`env.number`/`port`/`boolean`/`url`/`enum`) turn `string | undefined` into the value you want;
5
+ * any Standard Schema (`t`, zod, valibot) is also accepted. Dependency-free, edge-safe.
6
+ */
7
+ export { type DefineEnvOptions, defineEnv, type EnvResult, type EnvShape } from "./define.js";
8
+ export { boolean, enumValue, env, number, port, string, url } from "./helpers.js";
9
+ export type { InferOutput, StandardResult, StandardSchemaV1 } from "./schema.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC7F,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AACjF,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @nifrajs/env — typed, validated environment variables. Define a schema once at startup; get a
3
+ * frozen typed object, or a loud boot-time failure listing every problem. Coercing helpers
4
+ * (`env.number`/`port`/`boolean`/`url`/`enum`) turn `string | undefined` into the value you want;
5
+ * any Standard Schema (`t`, zod, valibot) is also accepted. Dependency-free, edge-safe.
6
+ */
7
+ export { defineEnv } from "./define.js";
8
+ export { boolean, enumValue, env, number, port, string, url } from "./helpers.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAyB,SAAS,EAAiC,MAAM,aAAa,CAAA;AAC7F,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Minimal Standard Schema surface (the public https://standardschema.dev v1 spec), inlined so
3
+ * `@nifrajs/env` stays dependency-free and accepts validators from `t` (@nifrajs/schema), zod, valibot,
4
+ * or its own coercing `env.*` helpers interchangeably.
5
+ */
6
+ export interface StandardIssue {
7
+ readonly message: string;
8
+ }
9
+ export type StandardResult<Output> = {
10
+ readonly value: Output;
11
+ readonly issues?: undefined;
12
+ } | {
13
+ readonly issues: ReadonlyArray<StandardIssue>;
14
+ };
15
+ export interface StandardSchemaV1<Output = unknown> {
16
+ readonly "~standard": {
17
+ readonly version: 1;
18
+ readonly vendor: string;
19
+ readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
20
+ readonly types?: {
21
+ readonly output: Output;
22
+ };
23
+ };
24
+ }
25
+ /** Extract a Standard Schema's validated output type. */
26
+ export type InferOutput<S extends StandardSchemaV1> = NonNullable<S["~standard"]["types"]>["output"];
27
+ /** Build an env validator: a Standard Schema whose `validate` coerces from `string | undefined`. */
28
+ export declare function envSchema<Output>(validate: (raw: string | undefined) => StandardResult<Output>): StandardSchemaV1<Output>;
29
+ /** A single issue. Messages NEVER include the variable's value — it may be a secret. */
30
+ export declare const issue: (message: string) => {
31
+ readonly issues: ReadonlyArray<StandardIssue>;
32
+ };
33
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,MAAM,cAAc,CAAC,MAAM,IAC7B;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;CAAE,CAAA;AAErD,MAAM,WAAW,gBAAgB,CAAC,MAAM,GAAG,OAAO;IAChD,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;QACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAA;QAC/F,QAAQ,CAAC,KAAK,CAAC,EAAE;YAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7C,CAAA;CACF;AAED,yDAAyD;AACzD,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,gBAAgB,IAAI,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;AAEpG,oGAAoG;AACpG,wBAAgB,SAAS,CAAC,MAAM,EAC9B,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,KAAK,cAAc,CAAC,MAAM,CAAC,GAC5D,gBAAgB,CAAC,MAAM,CAAC,CAU1B;AAED,wFAAwF;AACxF,eAAO,MAAM,KAAK,GAAI,SAAS,MAAM,KAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;CAErF,CAAA"}
package/dist/schema.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Minimal Standard Schema surface (the public https://standardschema.dev v1 spec), inlined so
3
+ * `@nifrajs/env` stays dependency-free and accepts validators from `t` (@nifrajs/schema), zod, valibot,
4
+ * or its own coercing `env.*` helpers interchangeably.
5
+ */
6
+ /** Build an env validator: a Standard Schema whose `validate` coerces from `string | undefined`. */
7
+ export function envSchema(validate) {
8
+ return {
9
+ "~standard": {
10
+ version: 1,
11
+ vendor: "nifra-env",
12
+ validate: (value) => validate(value),
13
+ // Phantom: carries the output type for InferOutput; no runtime value.
14
+ types: undefined,
15
+ },
16
+ };
17
+ }
18
+ /** A single issue. Messages NEVER include the variable's value — it may be a secret. */
19
+ export const issue = (message) => ({
20
+ issues: [{ message }],
21
+ });
22
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAsBH,oGAAoG;AACpG,MAAM,UAAU,SAAS,CACvB,QAA6D;IAE7D,OAAO;QACL,WAAW,EAAE;YACX,OAAO,EAAE,CAAC;YACV,MAAM,EAAE,WAAW;YACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAA2B,CAAC;YAC1D,sEAAsE;YACtE,KAAK,EAAE,SAAmD;SAC3D;KACF,CAAA;AACH,CAAC;AAED,wFAAwF;AACxF,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,OAAe,EAAqD,EAAE,CAAC,CAAC;IAC5F,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@nifrajs/env",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Typed, validated environment variables for nifra — define a schema, get a frozen typed object, fail loud at boot listing every problem. Coercing helpers (number/port/boolean/url/enum) + any Standard Schema. Dependency-free, edge-safe.",
5
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "web-framework",
10
+ "full-stack",
11
+ "edge",
12
+ "ssr"
13
+ ],
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "bun": "./src/index.ts",
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src"
28
+ ],
29
+ "sideEffects": false,
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
32
+ },
33
+ "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/nifrajs/nifra.git",
40
+ "directory": "packages/env"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/nifrajs/nifra/issues"
44
+ },
45
+ "homepage": "https://github.com/nifrajs/nifra#readme"
46
+ }
package/src/define.ts ADDED
@@ -0,0 +1,70 @@
1
+ import type { InferOutput, StandardSchemaV1 } from "./schema.ts"
2
+
3
+ /** A schema per variable name. */
4
+ export type EnvShape = Record<string, StandardSchemaV1>
5
+
6
+ /** The frozen, validated result — each key typed by its schema's output. */
7
+ export type EnvResult<S extends EnvShape> = { readonly [K in keyof S]: InferOutput<S[K]> }
8
+
9
+ export interface DefineEnvOptions {
10
+ /**
11
+ * Where to read variables from. Defaults to `process.env` (Bun/Node). On Cloudflare Workers there
12
+ * is no `process.env` — pass the request's `env` bindings: `defineEnv(shape, { source: env })`.
13
+ */
14
+ readonly source?: Record<string, string | undefined>
15
+ }
16
+
17
+ /**
18
+ * Validate the environment against a schema and return a **frozen, typed** object — or throw at
19
+ * startup listing **every** problem at once (not just the first), so a misconfigured deploy fails
20
+ * loud and immediately instead of erroring on the first request that touches a bad var. This is the
21
+ * boot-time half of nifra's "validate at every boundary" rule, for config.
22
+ *
23
+ * ```ts
24
+ * // env.ts — imported once at startup
25
+ * import { defineEnv, env } from "@nifrajs/env"
26
+ * export const ENV = defineEnv({
27
+ * DATABASE_URL: env.url(),
28
+ * PORT: env.port({ default: 3000 }),
29
+ * NODE_ENV: env.enum(["development", "production", "test"], { default: "development" }),
30
+ * STRIPE_SECRET: env.string(),
31
+ * DEBUG: env.boolean({ default: false }),
32
+ * })
33
+ * // ENV.PORT: number, ENV.NODE_ENV: "development" | "production" | "test", … all validated.
34
+ * ```
35
+ *
36
+ * Errors name the offending variable and the reason — never its value (it may be a secret). BYO
37
+ * validators (`t`, zod, valibot) work too; only the `env.*` helpers coerce from strings, so a plain
38
+ * `t.number()` would see the raw string — use `env.number()` for coercion.
39
+ */
40
+ export function defineEnv<S extends EnvShape>(shape: S, options?: DefineEnvOptions): EnvResult<S> {
41
+ const source =
42
+ options?.source ??
43
+ (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ??
44
+ {}
45
+ const out: Record<string, unknown> = {}
46
+ const problems: string[] = []
47
+
48
+ for (const key of Object.keys(shape)) {
49
+ const schema = shape[key] as StandardSchemaV1
50
+ const result = schema["~standard"].validate(source[key])
51
+ if (result instanceof Promise) {
52
+ // Env validation must be synchronous (it runs once, at import) — an async validator is a
53
+ // configuration mistake, surfaced as a problem rather than an unhandled promise.
54
+ problems.push(` ${key}: async validators are not supported in defineEnv`)
55
+ continue
56
+ }
57
+ if (result.issues !== undefined) {
58
+ for (const i of result.issues) problems.push(` ${key}: ${i.message}`)
59
+ } else {
60
+ out[key] = result.value
61
+ }
62
+ }
63
+
64
+ if (problems.length > 0) {
65
+ throw new Error(
66
+ `[nifra/env] invalid environment — ${problems.length} problem(s):\n${problems.join("\n")}\n\nSet the variable(s) above and restart.`,
67
+ )
68
+ }
69
+ return Object.freeze(out) as EnvResult<S>
70
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Coercing env validators. Every value in `process.env` is a `string | undefined`, so these turn
3
+ * that into the typed value you actually want — with defaults, optionality, and clear errors. Each
4
+ * is a Standard Schema, so they slot straight into {@link defineEnv} (and could be used anywhere a
5
+ * Standard Schema is accepted).
6
+ *
7
+ * Error messages are **value-free** — they say what was wrong, never echo the variable's value
8
+ * (it may be a secret). {@link defineEnv} prepends the variable name.
9
+ */
10
+
11
+ import { envSchema, issue, type StandardSchemaV1 } from "./schema.ts"
12
+
13
+ interface Base<T> {
14
+ /** Used when the variable is unset/empty. Makes the variable optional-with-a-fallback. */
15
+ readonly default?: T
16
+ }
17
+ interface Optional {
18
+ /** When true, an unset variable yields `undefined` instead of being an error. */
19
+ readonly optional?: boolean
20
+ }
21
+
22
+ const isUnset = (raw: string | undefined): boolean => raw === undefined || raw === ""
23
+
24
+ /** A required (or defaulted/optional) non-empty string. */
25
+ export function string(opts: Base<string> & Optional = {}): StandardSchemaV1<string | undefined> {
26
+ return envSchema((raw) => {
27
+ if (isUnset(raw)) {
28
+ if (opts.default !== undefined) return { value: opts.default }
29
+ if (opts.optional) return { value: undefined }
30
+ return issue("is required")
31
+ }
32
+ return { value: raw }
33
+ })
34
+ }
35
+
36
+ /** A finite number, coerced from its decimal string. */
37
+ export function number(opts: Base<number> & Optional = {}): StandardSchemaV1<number | undefined> {
38
+ return envSchema((raw) => {
39
+ if (isUnset(raw)) {
40
+ if (opts.default !== undefined) return { value: opts.default }
41
+ if (opts.optional) return { value: undefined }
42
+ return issue("is required")
43
+ }
44
+ const n = Number(raw)
45
+ if (!Number.isFinite(n)) return issue("must be a number")
46
+ return { value: n }
47
+ })
48
+ }
49
+
50
+ /** A TCP port: an integer in 1–65535. */
51
+ export function port(opts: Base<number> = {}): StandardSchemaV1<number> {
52
+ return envSchema((raw) => {
53
+ if (isUnset(raw)) {
54
+ if (opts.default !== undefined) return { value: opts.default }
55
+ return issue("is required")
56
+ }
57
+ const n = Number(raw)
58
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
59
+ return issue("must be an integer port in 1–65535")
60
+ }
61
+ return { value: n }
62
+ }) as StandardSchemaV1<number>
63
+ }
64
+
65
+ const TRUE = new Set(["true", "1", "yes", "on"])
66
+ const FALSE = new Set(["false", "0", "no", "off", ""])
67
+
68
+ /** A boolean: `true`/`1`/`yes`/`on` → true; `false`/`0`/`no`/`off`/empty → false (case-insensitive). */
69
+ export function boolean(opts: Base<boolean> = {}): StandardSchemaV1<boolean> {
70
+ return envSchema((raw) => {
71
+ if (raw === undefined) {
72
+ if (opts.default !== undefined) return { value: opts.default }
73
+ return issue("is required")
74
+ }
75
+ const v = raw.trim().toLowerCase()
76
+ if (TRUE.has(v)) return { value: true }
77
+ if (FALSE.has(v)) return { value: false }
78
+ return issue("must be a boolean (true/false/1/0/yes/no/on/off)")
79
+ }) as StandardSchemaV1<boolean>
80
+ }
81
+
82
+ /** One of a fixed set of string values. */
83
+ export function enumValue<const V extends readonly [string, ...string[]]>(
84
+ values: V,
85
+ opts: Base<V[number]> = {},
86
+ ): StandardSchemaV1<V[number]> {
87
+ const set = new Set<string>(values)
88
+ return envSchema((raw) => {
89
+ if (raw === undefined || raw === "") {
90
+ if (opts.default !== undefined) return { value: opts.default }
91
+ return issue(`is required (one of: ${values.join(", ")})`)
92
+ }
93
+ if (!set.has(raw)) return issue(`must be one of: ${values.join(", ")}`)
94
+ return { value: raw as V[number] }
95
+ }) as StandardSchemaV1<V[number]>
96
+ }
97
+
98
+ /** A valid absolute URL (parses with the WHATWG `URL`). Returns the normalized href string. */
99
+ export function url(opts: Base<string> & Optional = {}): StandardSchemaV1<string | undefined> {
100
+ return envSchema((raw) => {
101
+ if (raw === undefined || raw === "") {
102
+ if (opts.default !== undefined) return { value: opts.default }
103
+ if (opts.optional) return { value: undefined }
104
+ return issue("is required")
105
+ }
106
+ try {
107
+ return { value: new URL(raw).href }
108
+ } catch {
109
+ return issue("must be a valid URL")
110
+ }
111
+ })
112
+ }
113
+
114
+ /** The coercing env validators, grouped — `env.string()`, `env.port()`, `env.enum([...])`, … */
115
+ export const env = {
116
+ string,
117
+ number,
118
+ port,
119
+ boolean,
120
+ enum: enumValue,
121
+ url,
122
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @nifrajs/env — typed, validated environment variables. Define a schema once at startup; get a
3
+ * frozen typed object, or a loud boot-time failure listing every problem. Coercing helpers
4
+ * (`env.number`/`port`/`boolean`/`url`/`enum`) turn `string | undefined` into the value you want;
5
+ * any Standard Schema (`t`, zod, valibot) is also accepted. Dependency-free, edge-safe.
6
+ */
7
+
8
+ export { type DefineEnvOptions, defineEnv, type EnvResult, type EnvShape } from "./define.ts"
9
+ export { boolean, enumValue, env, number, port, string, url } from "./helpers.ts"
10
+ export type { InferOutput, StandardResult, StandardSchemaV1 } from "./schema.ts"
package/src/schema.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Minimal Standard Schema surface (the public https://standardschema.dev v1 spec), inlined so
3
+ * `@nifrajs/env` stays dependency-free and accepts validators from `t` (@nifrajs/schema), zod, valibot,
4
+ * or its own coercing `env.*` helpers interchangeably.
5
+ */
6
+
7
+ export interface StandardIssue {
8
+ readonly message: string
9
+ }
10
+
11
+ export type StandardResult<Output> =
12
+ | { readonly value: Output; readonly issues?: undefined }
13
+ | { readonly issues: ReadonlyArray<StandardIssue> }
14
+
15
+ export interface StandardSchemaV1<Output = unknown> {
16
+ readonly "~standard": {
17
+ readonly version: 1
18
+ readonly vendor: string
19
+ readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>
20
+ readonly types?: { readonly output: Output }
21
+ }
22
+ }
23
+
24
+ /** Extract a Standard Schema's validated output type. */
25
+ export type InferOutput<S extends StandardSchemaV1> = NonNullable<S["~standard"]["types"]>["output"]
26
+
27
+ /** Build an env validator: a Standard Schema whose `validate` coerces from `string | undefined`. */
28
+ export function envSchema<Output>(
29
+ validate: (raw: string | undefined) => StandardResult<Output>,
30
+ ): StandardSchemaV1<Output> {
31
+ return {
32
+ "~standard": {
33
+ version: 1,
34
+ vendor: "nifra-env",
35
+ validate: (value) => validate(value as string | undefined),
36
+ // Phantom: carries the output type for InferOutput; no runtime value.
37
+ types: undefined as unknown as { readonly output: Output },
38
+ },
39
+ }
40
+ }
41
+
42
+ /** A single issue. Messages NEVER include the variable's value — it may be a secret. */
43
+ export const issue = (message: string): { readonly issues: ReadonlyArray<StandardIssue> } => ({
44
+ issues: [{ message }],
45
+ })