@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 +21 -0
- package/README.md +67 -0
- package/dist/define.d.ts +39 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +52 -0
- package/dist/define.js.map +1 -0
- package/dist/helpers.d.ts +41 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +115 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +33 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +22 -0
- package/dist/schema.js.map +1 -0
- package/package.json +46 -0
- package/src/define.ts +70 -0
- package/src/helpers.ts +122 -0
- package/src/index.ts +10 -0
- package/src/schema.ts +45 -0
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.
|
package/dist/define.d.ts
ADDED
|
@@ -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"}
|
package/dist/helpers.js
ADDED
|
@@ -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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"}
|
package/dist/index.d.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
|
+
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"}
|
package/dist/schema.d.ts
ADDED
|
@@ -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
|
+
})
|