@g14o/env-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gideon Ofori Addo
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,121 @@
1
+ # @g14o/env-core
2
+
3
+ Framework-agnostic, typesafe environment variables validated with any [Standard Schema](https://standardschema.dev) library (Zod, Valibot, ArkType, and others).
4
+
5
+ **Zero runtime dependencies** — install only the validator you use.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @g14o/env-core zod
11
+ ```
12
+
13
+ ### Peer dependencies
14
+
15
+ `zod`, `valibot`, and `arktype` are **optional** peers — install the validator you use. Ranges follow [Standard Schema v1](https://standardschema.dev) support on each major line (`arktype` ^2.0.0, `valibot` ^1.0.0, `zod` ^4.0.0). `typescript` (>=5) is optional but recommended for typed env definitions.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { createEnv } from "@g14o/env-core";
21
+ import * as z from "zod";
22
+
23
+ export const env = createEnv({
24
+ clientPrefix: "NEXT_PUBLIC_",
25
+ server: {
26
+ DATABASE_URL: z.url(),
27
+ OPEN_AI_API_KEY: z.string().min(1),
28
+ },
29
+ client: {
30
+ NEXT_PUBLIC_API_URL: z.url(),
31
+ },
32
+ runtimeEnv: process.env,
33
+ emptyStringAsUndefined: true,
34
+ });
35
+ ```
36
+
37
+ On the **server**, all variables are validated and readable. On the **client**, only `client` keys are validated; accessing a `server` key throws with the exact variable name.
38
+
39
+ Import the same `env` object in server and client code — no separate client export is required.
40
+
41
+ ### How client protection works
42
+
43
+ `createEnv` returns a [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) around the validated environment object (on both server and client):
44
+
45
+ - **Server**: the proxy is transparent — all validated server and client keys are readable.
46
+ - **Client**: only `client` schemas are validated at import time (missing server vars in `process.env` do not throw).
47
+ - **Client access**: any property that is not a declared `client` key throws with the exact name, e.g. `DATABASE_URL` or typos like `NOT_DECLARED`.
48
+ - **Interop**: `__esModule` and `$$typeof` are ignored so bundlers and React do not trigger false positives.
49
+
50
+ ### Valibot
51
+
52
+ ```ts
53
+ import { createEnv } from "@g14o/env-core";
54
+ import * as v from "valibot";
55
+
56
+ export const env = createEnv({
57
+ clientPrefix: "PUBLIC_",
58
+ server: {
59
+ DATABASE_URL: v.pipe(v.string(), v.url()),
60
+ },
61
+ client: {
62
+ PUBLIC_API_URL: v.pipe(v.string(), v.url()),
63
+ },
64
+ runtimeEnv: process.env,
65
+ });
66
+ ```
67
+
68
+ ### ArkType
69
+
70
+ ```ts
71
+ import { createEnv } from "@g14o/env-core";
72
+ import { type } from "arktype";
73
+
74
+ export const env = createEnv({
75
+ server: {
76
+ DATABASE_URL: type("string.url"),
77
+ },
78
+ clientPrefix: "PUBLIC_",
79
+ client: {
80
+ PUBLIC_API_URL: type("string.url"),
81
+ },
82
+ runtimeEnv: process.env,
83
+ });
84
+ ```
85
+
86
+ ### Strict runtime mapping (bundler-friendly)
87
+
88
+ When your framework only inlines env vars you reference explicitly, use `runtimeEnvStrict`:
89
+
90
+ ```ts
91
+ export const env = createEnv({
92
+ server: { DATABASE_URL: z.url() },
93
+ clientPrefix: "NEXT_PUBLIC_",
94
+ client: { NEXT_PUBLIC_API_URL: z.url() },
95
+ runtimeEnvStrict: {
96
+ DATABASE_URL: process.env.DATABASE_URL,
97
+ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
98
+ },
99
+ });
100
+ ```
101
+
102
+ ## Options
103
+
104
+ | Option | Description |
105
+ |--------|-------------|
106
+ | `server` | Server-only variables (validated on server; throw on client access) |
107
+ | `client` | Client-safe variables (validated on server and client) |
108
+ | `clientPrefix` | Optional prefix required on all `client` keys (type + runtime) |
109
+ | `runtimeEnv` | Record to read values from (e.g. `process.env`) |
110
+ | `runtimeEnvStrict` | Explicit per-key mapping; mutually exclusive with `runtimeEnv` |
111
+ | `emptyStringAsUndefined` | Treat `""` as `undefined` before validation |
112
+ | `isServer` | Override server detection (default: `typeof window === "undefined"`) |
113
+ | `onInvalidAccess` | Hook before throwing when a server key is read on the client |
114
+
115
+ ## Security
116
+
117
+ Server **values** are never validated or exposed on the client. Importing a single `env.ts` that defines `server` schemas still ships those **names** to the client bundle. For sensitive names, split into `env/server.ts` and `env/client.ts`.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,122 @@
1
+ //#region src/client-guard.d.ts
2
+ type OnInvalidAccessHandler = (variable: string) => void;
3
+ //#endregion
4
+ //#region src/standard-schema.d.ts
5
+ /** @see https://standardschema.dev */
6
+ type StandardSchemaV1<Input = unknown, Output = Input> = {
7
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
8
+ };
9
+ declare namespace StandardSchemaV1 {
10
+ interface Props<Input = unknown, Output = Input> {
11
+ readonly types?: {
12
+ readonly input: Input;
13
+ readonly output: Output;
14
+ };
15
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
16
+ readonly vendor: string;
17
+ readonly version: 1;
18
+ }
19
+ type Result<Output> = SuccessResult<Output> | FailureResult;
20
+ interface SuccessResult<Output> {
21
+ readonly issues?: undefined;
22
+ readonly value: Output;
23
+ }
24
+ interface FailureResult {
25
+ readonly issues: readonly Issue[];
26
+ }
27
+ interface Issue {
28
+ readonly message: string;
29
+ readonly path?: readonly (PropertyKey | PathSegment)[] | undefined;
30
+ }
31
+ interface PathSegment {
32
+ readonly key: PropertyKey;
33
+ }
34
+ }
35
+ //#endregion
36
+ //#region src/types.d.ts
37
+ type SchemaShape = Record<string, StandardSchemaV1>;
38
+ type RuntimeEnvValue = string | number | boolean | undefined | null;
39
+ type RuntimeEnvInput = Record<string, RuntimeEnvValue>;
40
+ type Simplify<T> = { [K in keyof T]: T[K] } & {};
41
+ type InferSchemaOutput<TSchema extends StandardSchemaV1> = TSchema extends StandardSchemaV1<unknown, infer TOutput> ? TOutput : never;
42
+ type InferShapeOutput<TShape extends SchemaShape> = Simplify<{ [K in keyof TShape]: InferSchemaOutput<TShape[K]> }>;
43
+ type ClientKeyWithPrefix<TPrefix extends string, TKey extends PropertyKey> = TKey extends `${TPrefix}${string}` ? TKey : never;
44
+ type PrefixedClientShape<TPrefix extends string, TClient extends SchemaShape> = Simplify<{ [K in keyof TClient as ClientKeyWithPrefix<TPrefix, K>]: TClient[K] }>;
45
+ type InvalidClientKeys<TPrefix extends string, TClient extends SchemaShape> = Exclude<keyof TClient, `${TPrefix}${string}`>;
46
+ type AssertValidClientPrefix<TPrefix extends string | undefined, TClient extends SchemaShape | undefined> = TPrefix extends string ? TClient extends SchemaShape ? InvalidClientKeys<TPrefix, TClient> extends never ? unknown : `Client environment variable(s) must start with "${TPrefix}": ${InvalidClientKeys<TPrefix, TClient> & string}` : unknown : unknown;
47
+ type ResolveClientShape<TPrefix extends string | undefined, TClient extends SchemaShape | undefined> = TClient extends SchemaShape ? TPrefix extends string ? PrefixedClientShape<TPrefix, TClient> : TClient : Record<string, never>;
48
+ type ShapeKeys<TShape extends SchemaShape | undefined> = TShape extends SchemaShape ? keyof TShape : never;
49
+ type StrictRuntimeEnv<TServer extends SchemaShape | undefined, TClient extends SchemaShape | undefined> = Simplify<{ [K in ShapeKeys<TServer> | ShapeKeys<TClient>]: RuntimeEnvValue }>;
50
+ type MutuallyExclusive<A extends Record<string, unknown>, B extends Record<string, unknown>> = (A & { [K in keyof B]?: never }) | (B & { [K in keyof A]?: never });
51
+ type RuntimeEnvSource<TServer extends SchemaShape | undefined, TClient extends SchemaShape | undefined> = MutuallyExclusive<{
52
+ runtimeEnv: RuntimeEnvInput;
53
+ }, {
54
+ runtimeEnvStrict: StrictRuntimeEnv<TServer, TClient>;
55
+ }>;
56
+ type MergeEnvShapes<TServer extends SchemaShape | undefined, TClient extends SchemaShape | undefined> = (TServer extends SchemaShape ? TServer : Record<string, never>) & (TClient extends SchemaShape ? TClient : Record<string, never>);
57
+ type CreateEnvOptions<TServer extends SchemaShape | undefined = undefined, TClient extends SchemaShape | undefined = undefined, TPrefix extends string | undefined = undefined> = {
58
+ server?: TServer;
59
+ client?: TClient;
60
+ clientPrefix?: TPrefix;
61
+ emptyStringAsUndefined?: boolean;
62
+ isServer?: boolean;
63
+ onInvalidAccess?: OnInvalidAccessHandler; /** @internal */
64
+ skipValidation?: boolean;
65
+ } & RuntimeEnvSource<TServer, TClient>;
66
+ type CreateEnvOutput<TServer extends SchemaShape | undefined, TClient extends SchemaShape | undefined> = Readonly<InferShapeOutput<MergeEnvShapes<TServer, TClient>>>;
67
+ //#endregion
68
+ //#region src/create-env.d.ts
69
+ /**
70
+ * Validates environment variables with Standard Schema shapes and returns a typed, frozen env object.
71
+ *
72
+ * On the server (`isServer` true by default when `globalThis.window` is undefined), all declared
73
+ * `server` and `client` keys are picked from `runtimeEnv` or `runtimeEnvStrict`, validated, and
74
+ * exposed. On the client, only `client` keys are validated; `server` keys are omitted from the
75
+ * validated object and throw if accessed (via a `Proxy` guard unless `onInvalidAccess` handles it).
76
+ *
77
+ * When `skipValidation` is true, values are picked without schema validation (internal/testing use).
78
+ *
79
+ * @template TServer - Server-only schema shape.
80
+ * @template TClient - Client-safe schema shape.
81
+ * @template TPrefix - Required `client` key prefix when `clientPrefix` is set.
82
+ * @param options - Server/client schemas, runtime source, and validation behavior.
83
+ * @param options.server - Server-only variables; validated only when `isServer` is true.
84
+ * @param options.client - Variables safe on the client; always validated unless `skipValidation`.
85
+ * @param options.clientPrefix - Prefix every `client` key must use (enforced at compile time and runtime).
86
+ * @param options.runtimeEnv - Loose record to read values from (e.g. `process.env`).
87
+ * @param options.runtimeEnvStrict - Explicit per-key mapping; mutually exclusive with `runtimeEnv`.
88
+ * @param options.emptyStringAsUndefined - Treat `""` as `undefined` before validation. Default `false`.
89
+ * @param options.isServer - Override server detection. Default: no `window` on `globalThis`.
90
+ * @param options.onInvalidAccess - Called before throwing when a non-client key is read on the client.
91
+ * @param options.skipValidation - Skip schema validation and return picked runtime values only. Default `false`.
92
+ * @returns Readonly, frozen env object typed from inferred schema outputs; client reads are guarded by `Proxy`.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { createEnv } from "@g14o/env-core";
97
+ * import * as z from "zod";
98
+ *
99
+ * export const env = createEnv({
100
+ * clientPrefix: "PUBLIC_",
101
+ * server: {
102
+ * DATABASE_URL: z.url(),
103
+ * OPEN_AI_API_KEY: z.string().min(8),
104
+ * },
105
+ * client: {
106
+ * PUBLIC_API_URL: z.url(),
107
+ * PUBLIC_APP_NAME: z.string().min(1),
108
+ * },
109
+ * runtimeEnvStrict: runtimeEnvStrict(),
110
+ * emptyStringAsUndefined: true,
111
+ * });
112
+ * ```
113
+ */
114
+ declare function createEnv<TServer extends SchemaShape | undefined = undefined, const TClient extends SchemaShape | undefined = undefined, const TPrefix extends string | undefined = undefined>(options: CreateEnvOptions<TServer, TClient, TPrefix> & AssertValidClientPrefix<TPrefix, TClient>): CreateEnvOutput<TServer, ResolveClientShape<TPrefix, TClient>>;
115
+ //#endregion
116
+ //#region src/errors.d.ts
117
+ declare class InvalidEnvironmentVariablesError extends Error {
118
+ readonly issues: readonly string[];
119
+ constructor(issues: readonly string[], scope: string);
120
+ }
121
+ //#endregion
122
+ export { type AssertValidClientPrefix, type CreateEnvOptions, type CreateEnvOutput, type InferSchemaOutput, type InferShapeOutput, type InvalidClientKeys, InvalidEnvironmentVariablesError, type OnInvalidAccessHandler, type PrefixedClientShape, type RuntimeEnvInput, type RuntimeEnvValue, type SchemaShape, type Simplify, type StandardSchemaV1, type StrictRuntimeEnv, createEnv };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ var e=class extends Error{issues;constructor(e,t){super(`Invalid environment variables (${t}):\n${e.map(e=>` - ${e}`).join(`
2
+ `)}`),this.name=`InvalidEnvironmentVariablesError`,this.issues=e}};function t(e){return`${e.path&&e.path.length>0?`${e.path.map(e=>n(e)).join(`.`)}: `:``}${e.message}`}function n(e){return typeof e==`object`&&e&&`key`in e?String(e.key):String(e)}function r(e){return Error(`Attempted to access server environment variable(s) on the client: ${e}`)}const i=new Set([`__esModule`,`$$typeof`]);function a(e,t){let{isServer:n,clientKeys:a,onInvalidAccess:o}=t;return new Proxy(e,{get(e,t,s){if(typeof t!=`string`)return Reflect.get(e,t,s);if(!(n||i.has(t)||a.has(t)))throw o?.(t),r(t);return Reflect.get(e,t,s)}})}function o(e,t,n){let r={};for(let i of e){let e=Object.hasOwn(t,i)?t[i]:void 0;n&&e===``&&(e=void 0),r[i]=e}return r}function s(e,t){let n=[];for(let r of e)Object.hasOwn(t,r)||n.push(r);if(n.length>0)throw Error(`runtimeEnvStrict is missing required keys: ${n.join(`, `)}`)}function c(e,t){if(e!==void 0){for(let n of t)if(!n.startsWith(e))throw Error(`Environment variable "${n}" must start with "${e}" to be exposed on the client`)}}function l(e,t){for(let n of e)if(t.includes(n))throw Error(`Environment variable "${n}" cannot be defined in both server and client`)}function u(t,n,r){let i=Object.keys(t);if(i.length===0)return{};let a={},o=[],s=[];for(let e of i){let r=t[e];if(!r)continue;let i=r[`~standard`].validate(n[e]);if(i instanceof Promise){s.push({key:e,promise:i});continue}d(e,i,a,o)}if(s.length>0)throw Error(`Async Standard Schema validation is not supported in createEnv (${r}). Use synchronous validators.`);if(o.length>0)throw new e(o,r);return a}function d(e,n,r,i){if(n.issues){for(let r of n.issues)i.push(`${e}: ${t(r)}`);return}r[e]=n.value}function f(e){return e===void 0?globalThis.window===void 0:e}function p(e){if(`runtimeEnvStrict`in e&&e.runtimeEnvStrict!==void 0)return e.runtimeEnvStrict;if(`runtimeEnv`in e&&e.runtimeEnv!==void 0)return e.runtimeEnv;throw Error(`createEnv requires either runtimeEnv or runtimeEnvStrict`)}function m(e,t){return{server:e??{},client:t??{}}}function h(e){let{server:t,client:n}=m(e.server,e.client),r=Object.keys(t),i=Object.keys(n),a=[...r,...i];l(r,i),c(e.clientPrefix,i);let o=p(e);return`runtimeEnvStrict`in e&&e.runtimeEnvStrict!==void 0&&s(a,o),{server:t,client:n,clientPrefix:e.clientPrefix,emptyStringAsUndefined:e.emptyStringAsUndefined??!1,isServer:f(e.isServer),onInvalidAccess:e.onInvalidAccess,skipValidation:e.skipValidation??!1,runtime:o}}function g(e){let t=h(e),n=Object.keys(t.server),r=Object.keys(t.client),i=[...n,...r],s=new Set(r),c=o(i,t.runtime,t.emptyStringAsUndefined);if(t.skipValidation)return Object.freeze({...c});let l=o(r,t.runtime,t.emptyStringAsUndefined),d=u(t.client,l,`client`),f={};if(t.isServer&&n.length>0){let e=o(n,t.runtime,t.emptyStringAsUndefined);f=u(t.server,e,`server`)}return a(Object.freeze(t.isServer?{...f,...d}:{...d}),{isServer:t.isServer,clientKeys:s,onInvalidAccess:t.onInvalidAccess})}export{e as InvalidEnvironmentVariablesError,g as createEnv};
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@g14o/env-core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic, typesafe environment variables via Standard Schema (Zod, Valibot, ArkType).",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/wuzgood98/g14o.git",
11
+ "directory": "packages/env-core"
12
+ },
13
+ "bugs": "https://github.com/wuzgood98/g14o/issues",
14
+ "homepage": "https://github.com/wuzgood98/g14o/tree/main/packages/env-core#readme",
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "peerDependencies": {
20
+ "arktype": "^2.0.0",
21
+ "typescript": ">=5.0.0",
22
+ "valibot": "^1.0.0",
23
+ "zod": "^4.0.0"
24
+ },
25
+ "peerDependenciesMeta": {
26
+ "typescript": {
27
+ "optional": true
28
+ },
29
+ "arktype": {
30
+ "optional": true
31
+ },
32
+ "valibot": {
33
+ "optional": true
34
+ },
35
+ "zod": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22",
41
+ "arktype": "^2.0.0",
42
+ "rimraf": "^6.0.1",
43
+ "tsdown": "^0.22.1",
44
+ "typescript": "^5",
45
+ "valibot": "^1.0.0",
46
+ "vitest": "^3.2.4",
47
+ "zod": "^4.0.0",
48
+ "@workspace/typescript-config": "0.0.0"
49
+ },
50
+ "exports": {
51
+ ".": {
52
+ "types": "./dist/index.d.ts",
53
+ "import": "./dist/index.js"
54
+ }
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "keywords": [
60
+ "environment",
61
+ "env",
62
+ "validation",
63
+ "zod",
64
+ "valibot",
65
+ "arktype",
66
+ "standard-schema",
67
+ "typescript"
68
+ ],
69
+ "engines": {
70
+ "node": ">=22.18"
71
+ },
72
+ "scripts": {
73
+ "build": "tsdown",
74
+ "clean": "rimraf dist",
75
+ "typecheck": "tsc --noEmit && tsc --noEmit -p test-types/tsconfig.json",
76
+ "test": "vitest run"
77
+ }
78
+ }