@joinremba/beacon 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 Remba
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,378 @@
1
+ <p align="center">
2
+ <picture>
3
+ <img alt="@joinremba/beacon" src="./assets/logo.svg" width="80">
4
+ </picture>
5
+ <br>
6
+ <strong>@joinremba/beacon</strong>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@joinremba/beacon"><img src="https://img.shields.io/npm/v/@joinremba/beacon.svg" alt="npm version"></a>
11
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/@joinremba/beacon.svg" alt="Licence"></a>
12
+ <a href="https://github.com/joinremba/beacon/actions/workflows/ci.yml"><img src="https://github.com/joinremba/beacon/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
13
+ <img src="https://img.shields.io/badge/Bun-%3E%3D1.3.1-black?logo=bun" alt="Bun">
14
+ <img src="https://img.shields.io/badge/TypeScript-6-blue" alt="TypeScript">
15
+ </p>
16
+
17
+ Beacon helps TypeScript teams boot applications safely by validating environment variables, config, secrets, and runtime feature gates before production breaks.
18
+
19
+ ```sh
20
+ bun add @joinremba/beacon
21
+ bunx beacon init
22
+ bunx beacon check
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Quick Start
28
+
29
+ ```ts
30
+ import { createBeacon } from "@joinremba/beacon";
31
+
32
+ const config = createBeacon({
33
+ DATABASE_URL: { type: "url", required: true },
34
+ REDIS_URL: { type: "url", required: true },
35
+ NODE_ENV: {
36
+ type: "enum",
37
+ values: ["development", "test", "staging", "production"],
38
+ default: "development",
39
+ },
40
+ PORT: { type: "port", default: 3000 },
41
+ API_KEY: { type: "string", required: true, secret: true },
42
+ });
43
+
44
+ config.ensure();
45
+
46
+ const dbUrl = config.get<string>("DATABASE_URL");
47
+ ```
48
+
49
+ If any variable is missing or invalid, `ensure()` throws a `ConfigValidationError` with **all issues collected at once** — so you fix everything in a single pass, not iteratively.
50
+
51
+ ---
52
+
53
+ ## Why Beacon?
54
+
55
+ Most backend projects start with a scattered collection of helper functions for reading env vars, checking types, and remembering which vars are required. This works until:
56
+
57
+ - A new developer joins and doesn't know which env vars exist
58
+ - A staging environment crashes because a required var was renamed but not documented
59
+ - A secret leaks into an error log because nobody added redaction
60
+ - Your CI pipeline passes locally but fails in production due to config drift
61
+
62
+ Beacon solves these by giving you a **single source of truth** for your env schema, with built-in validation, secrets redaction, profile support, and CLI tools for generating `.env.example` and checking environments.
63
+
64
+ ---
65
+
66
+ ## Features
67
+
68
+ - **Schema-based validation** — Define your env schema with simple string types (`"url"`, `"port"`, `"enum"`, etc.) or raw Zod schemas.
69
+ - **Missing variable detection** — All errors are collected and reported together, not one at a time.
70
+ - **Secrets redaction** — Keep secrets out of logs and error messages automatically. Values are replaced with `[REDACTED]`.
71
+ - **Local/staging/production profiles** — Define different schemas per environment.
72
+ - **`.env.example` generation** — Generate a documented `.env.example` from your schema via the CLI.
73
+ - **CLI** — `beacon init` scaffolds config, `beacon check` validates before deploying.
74
+ - **Zero runtime overhead** — Validations run once at boot. After that, access is plain property reads.
75
+ - **Framework-agnostic** — Works with Bun, Node.js, Express, Hono, Fastify, Next.js, Elysia.
76
+
77
+ ---
78
+
79
+ ## CLI
80
+
81
+ Beacon ships with a CLI for development and CI workflows.
82
+
83
+ ### `beacon init`
84
+
85
+ Generate a documented `.env.example` from your beacon config:
86
+
87
+ ```sh
88
+ bunx beacon init
89
+
90
+ # With a production profile:
91
+ bunx beacon init --profile production
92
+
93
+ # Custom config path:
94
+ bunx beacon init -c ./config/beacon.json -o .env.example.prod
95
+ ```
96
+
97
+ Output includes types, defaults, descriptions, and secret markers for every variable:
98
+
99
+ ```sh
100
+ # PostgreSQL connection string
101
+ # Type: url
102
+ # Required: yes
103
+ # DATABASE_URL=
104
+
105
+ # HTTP server port
106
+ # Type: port
107
+ # Default: 3000
108
+ PORT=3000
109
+ ```
110
+
111
+ ### `beacon check`
112
+
113
+ Validate your current environment against your schema:
114
+
115
+ ```sh
116
+ bunx beacon check
117
+
118
+ # With a specific profile:
119
+ bunx beacon check --profile staging
120
+
121
+ # Custom config:
122
+ bunx beacon check -c ./config/production.json
123
+ ```
124
+
125
+ Output is a colour-coded table:
126
+
127
+ ```sh
128
+ KEY STATUS VALUE
129
+ ──────────── ──────── ────────────────────
130
+ DATABASE_URL pass postgres://localhost...
131
+ PORT pass Using default value
132
+ NODE_ENV pass Using default value
133
+ API_KEY MISSING Not set
134
+ LOG_LEVEL pass Optional, not set
135
+ DB_HOST MISSING Not set
136
+ Did you mean DB_HOSTNAME?
137
+
138
+ 2 issue(s), 3 pass
139
+ ```
140
+
141
+ Exit codes: `0` if all pass, `1` if any issues found.
142
+
143
+ ### Per-command help
144
+
145
+ ```sh
146
+ beacon help init
147
+ beacon check --help
148
+ ```
149
+
150
+ ---
151
+
152
+ ## API Reference
153
+
154
+ ### `createBeacon(schema, options?)`
155
+
156
+ The default export. Accepts an env schema and optional configuration.
157
+
158
+ **Parameters**
159
+
160
+ | Option | Type | Description |
161
+ | ---------- | --------------------------------------------- | ----------------------------------------------------------- |
162
+ | `schema` | `Record<string, SchemaEntry>` | Map of environment variable names to field definitions. |
163
+ | `profile` | `string` | Active profile name. Merges matching entry from `profiles`. |
164
+ | `profiles` | `Record<string, Record<string, SchemaEntry>>` | Named profile overrides. |
165
+
166
+ **SchemaEntry** can be either:
167
+
168
+ **1. String-based** — Simple type names for everyday use:
169
+
170
+ | Field | Type | Default | Description |
171
+ | ------------- | ----------- | ------- | ------------------------------------ |
172
+ | `type` | `FieldType` | — | The type to validate against. |
173
+ | `required` | `boolean` | `true` | Whether the variable must be set. |
174
+ | `default` | `unknown` | — | Default value if not set. |
175
+ | `secret` | `boolean` | `false` | Redact value from errors and logs. |
176
+ | `values` | `string[]` | — | Allowed values (only for `"enum"`). |
177
+ | `description` | `string` | — | Used when generating `.env.example`. |
178
+
179
+ | Type | Zod equivalent |
180
+ | --------- | -------------------------------------------- |
181
+ | `string` | `z.string()` |
182
+ | `url` | `z.string().url()` |
183
+ | `number` | `z.coerce.number()` |
184
+ | `integer` | `z.coerce.number().int()` |
185
+ | `boolean` | `"true"` / `"false"` / `"1"` / `"0"` coerced |
186
+ | `port` | integer 1–65535 |
187
+ | `enum` | requires `values[]` |
188
+ | `email` | `z.string().email()` |
189
+ | `host` | `z.string()` |
190
+
191
+ **2. Zod schema** — Advanced users can pass Zod schemas directly:
192
+
193
+ ```ts
194
+ {
195
+ PORT: { schema: z.coerce.number().positive().max(9999) },
196
+ WHITELIST: { schema: z.string().regex(/^[\d,]+$/) },
197
+ }
198
+ ```
199
+
200
+ **Returns**
201
+
202
+ A config instance with:
203
+
204
+ | Method / Property | Description |
205
+ | ----------------- | ------------------------------------------------------------------------------------------------------------ |
206
+ | `ensure()` | Validates all env vars. Throws `ConfigValidationError` on failure. Returns the config instance for chaining. |
207
+ | `get<T>(key): T` | Returns the validated value for the given key. Throws if called before `ensure()`. |
208
+ | `secret` | Returns a `Record<string, boolean>` of which keys are marked as secrets. |
209
+
210
+ ### TypeScript Types
211
+
212
+ ```ts
213
+ import type {
214
+ BeaconOptions,
215
+ Beacon,
216
+ SchemaEntry,
217
+ FieldDefinition,
218
+ FieldType,
219
+ ConfigError,
220
+ ConfigValidationError,
221
+ } from "@joinremba/beacon";
222
+ ```
223
+
224
+ ### Config file (`.beaconrc.json`)
225
+
226
+ Used by the CLI for `init` and `check` commands:
227
+
228
+ ```json
229
+ {
230
+ "schema": {
231
+ "DATABASE_URL": {
232
+ "type": "url",
233
+ "required": true,
234
+ "description": "PostgreSQL connection string"
235
+ },
236
+ "PORT": { "type": "port", "default": 3000, "description": "HTTP server port" },
237
+ "NODE_ENV": {
238
+ "type": "enum",
239
+ "values": ["development", "production"],
240
+ "default": "development"
241
+ },
242
+ "API_KEY": { "type": "string", "required": true, "secret": true }
243
+ },
244
+ "profiles": {
245
+ "production": {
246
+ "DB_HOST": { "type": "host", "required": true, "description": "Production DB hostname" }
247
+ }
248
+ }
249
+ }
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Examples
255
+
256
+ ### Basic env validation
257
+
258
+ ```ts
259
+ import { createBeacon } from "@joinremba/beacon";
260
+
261
+ const config = createBeacon({
262
+ NODE_ENV: {
263
+ type: "enum",
264
+ values: ["development", "production", "test"],
265
+ default: "development",
266
+ },
267
+ PORT: { type: "port", default: 3000 },
268
+ });
269
+
270
+ config.ensure();
271
+ console.log(config.get("PORT"));
272
+ ```
273
+
274
+ ### With secrets redaction
275
+
276
+ ```ts
277
+ const config = createBeacon({
278
+ API_KEY: { type: "string", secret: true },
279
+ DATABASE_URL: { type: "url", secret: true },
280
+ });
281
+
282
+ config.ensure();
283
+ // Error messages never show API_KEY or DATABASE_URL values
284
+ ```
285
+
286
+ ### Custom error handling
287
+
288
+ ```ts
289
+ import { ConfigValidationError } from "@joinremba/beacon";
290
+
291
+ try {
292
+ config.ensure();
293
+ } catch (err) {
294
+ if (err instanceof ConfigValidationError) {
295
+ for (const issue of err.errors) {
296
+ console.error(`[${issue.key}] ${issue.message}`);
297
+ }
298
+ }
299
+ process.exit(1);
300
+ }
301
+ ```
302
+
303
+ ### Production profile
304
+
305
+ ```ts
306
+ const config = createBeacon(
307
+ {
308
+ DB_HOST: { type: "string", default: "localhost" },
309
+ DB_PORT: { type: "port", default: 5432 },
310
+ },
311
+ {
312
+ profile: "production",
313
+ profiles: {
314
+ production: {
315
+ DB_HOST: { type: "host", required: true },
316
+ DB_PORT: { type: "port", required: true },
317
+ },
318
+ },
319
+ }
320
+ );
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Roadmap
326
+
327
+ **MVP** (current)
328
+
329
+ - Typed env validation with string-based types and Zod
330
+ - Missing variable detection (aggregated errors)
331
+ - Secrets redaction in errors and logs
332
+ - Local/staging/production profiles
333
+ - `.env.example` generation via CLI
334
+ - `beacon check` CLI command
335
+ - Coloured CLI output with suggestions
336
+
337
+ **V1**
338
+
339
+ - Feature gates from local config
340
+ - Kill-switch flags
341
+ - Encrypted `.env` support
342
+ - Secret rotation checklist
343
+ - CI validation action
344
+ - Docker/Kubernetes env checks
345
+ - Config drift detection
346
+
347
+ **V2**
348
+
349
+ - Hosted team secret sync
350
+ - Audit trail for config changes
351
+ - Deployment provider integrations
352
+ - GitHub Actions integration
353
+ - Remba Cloud dashboard
354
+
355
+ ---
356
+
357
+ ## Related Packages
358
+
359
+ - [@joinremba/catalog](https://github.com/joinremba/catalog) — Production-ready logging and error event layer built on Pino.
360
+ - [@joinremba/gate](https://github.com/joinremba/gate) — API safety layer: validation, responses, idempotency, rate limiting, and API keys.
361
+
362
+ ## Social Preview
363
+
364
+ For sharing on X (Twitter) and other social platforms, use `assets/og-image.svg` as the repository's social preview image in GitHub repo settings:
365
+
366
+ 1. Go to your repo **Settings** → **Social preview** → **Upload image**
367
+ 2. Select `assets/og-image.svg`
368
+ 3. Save
369
+
370
+ This will be used whenever your repo link is shared on social media, Slack, or Discord.
371
+
372
+ ## Contributing
373
+
374
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, development process, and how to submit pull requests.
375
+
376
+ ## License
377
+
378
+ MIT &mdash; see [LICENSE](LICENSE).
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@joinremba/beacon",
3
+ "version": "0.1.0",
4
+ "description": "Validate environment variables, config, secrets, and runtime feature gates before production breaks.",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "bin": {
9
+ "beacon": "src/cli.ts"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "import": "./src/index.ts",
15
+ "default": "./src/index.ts"
16
+ },
17
+ "./cli": {
18
+ "types": "./src/cli.ts",
19
+ "import": "./src/cli.ts",
20
+ "default": "./src/cli.ts"
21
+ }
22
+ },
23
+ "files": [
24
+ "src",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "scripts": {
29
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm",
30
+ "dev": "bun --watch ./src/index.ts",
31
+ "format": "prettier --write .",
32
+ "format:check": "prettier --check .",
33
+ "lint": "eslint .",
34
+ "lint:fix": "eslint . --fix",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "bun test",
37
+ "test:watch": "bun test --watch",
38
+ "prepublishOnly": "bun run build",
39
+ "check": "bun lint && bun format:check && bun typecheck && bun test"
40
+ },
41
+ "author": {
42
+ "name": "Benson Isaac",
43
+ "email": "bensxnisaac@gmail.com"
44
+ },
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/joinremba/beacon.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/joinremba/beacon/issues"
52
+ },
53
+ "homepage": "https://github.com/joinremba/beacon#readme",
54
+ "keywords": [
55
+ "env",
56
+ "environment",
57
+ "config",
58
+ "validation",
59
+ "feature-gates",
60
+ "secrets",
61
+ "type-safe",
62
+ "cli"
63
+ ],
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "engines": {
68
+ "bun": ">=1.3.1"
69
+ },
70
+ "dependencies": {
71
+ "zod": "^4.4.2"
72
+ },
73
+ "devDependencies": {
74
+ "@types/bun": "latest",
75
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
76
+ "@typescript-eslint/parser": "^7.18.0",
77
+ "eslint": "^8.57.1",
78
+ "eslint-config-prettier": "^10.1.8",
79
+ "prettier": "^3.8.3",
80
+ "typescript": "^6.0.3"
81
+ }
82
+ }
@@ -0,0 +1,221 @@
1
+ import { z } from "zod";
2
+ import type { SchemaEntry } from "./types";
3
+
4
+ export interface BeaconConfigFile {
5
+ schema: Record<string, SchemaEntry>;
6
+ profile?: string;
7
+ profiles?: Record<string, Record<string, SchemaEntry>>;
8
+ }
9
+
10
+ export async function loadConfig(path?: string): Promise<BeaconConfigFile> {
11
+ const searchPaths = path ? [path] : [".beaconrc.json", "beacon.config.json"];
12
+
13
+ for (const searchPath of searchPaths) {
14
+ const file = Bun.file(searchPath);
15
+ const exists = await file.exists();
16
+ if (exists) {
17
+ const text = await file.text();
18
+ try {
19
+ const parsed = JSON.parse(text);
20
+
21
+ if (!parsed.schema || typeof parsed.schema !== "object") {
22
+ throw new Error(`Config file "${searchPath}" must have a "schema" field`);
23
+ }
24
+
25
+ const config: BeaconConfigFile = { schema: parsed.schema };
26
+
27
+ if (parsed.profiles) {
28
+ config.profiles = parsed.profiles;
29
+ }
30
+
31
+ return config;
32
+ } catch (err) {
33
+ if (err instanceof SyntaxError) {
34
+ throw new Error(`Invalid JSON in config file "${searchPath}": ${err.message}`);
35
+ }
36
+ throw err;
37
+ }
38
+ }
39
+ }
40
+
41
+ throw new Error(
42
+ path
43
+ ? `Config file not found: ${path}`
44
+ : "No config file found. Create a .beaconrc.json or pass --config <path>"
45
+ );
46
+ }
47
+
48
+ export function generateEnvExample(config: BeaconConfigFile, activeProfile?: string): string {
49
+ const lines: string[] = [];
50
+ lines.push("# Environment Variables");
51
+ lines.push(`# Generated by @joinremba/beacon`);
52
+ lines.push(`# Profile: ${activeProfile ?? "default"}`);
53
+ lines.push("");
54
+
55
+ const mergedSchema = { ...config.schema };
56
+
57
+ if (activeProfile && config.profiles?.[activeProfile]) {
58
+ Object.assign(mergedSchema, config.profiles[activeProfile]);
59
+ }
60
+
61
+ for (const [key, entry] of Object.entries(mergedSchema)) {
62
+ if (entry.description) {
63
+ lines.push(`# ${entry.description}`);
64
+ }
65
+
66
+ const isField = "type" in entry;
67
+ const isSchema = "schema" in entry;
68
+
69
+ if (isField) {
70
+ const field = entry as {
71
+ type: string;
72
+ required?: boolean;
73
+ default?: unknown;
74
+ values?: readonly string[];
75
+ secret?: boolean;
76
+ };
77
+ const typeInfo = field.values ? `${field.type} (${field.values.join(" | ")})` : field.type;
78
+ lines.push(`# Type: ${typeInfo}`);
79
+ if (field.required !== false) {
80
+ lines.push(`# Required: yes`);
81
+ } else {
82
+ lines.push(`# Required: no`);
83
+ }
84
+ if (field.default !== undefined) {
85
+ lines.push(`# Default: ${field.default}`);
86
+ }
87
+ if (field.secret) {
88
+ lines.push(`# Secret: yes`);
89
+ }
90
+
91
+ if (field.default !== undefined) {
92
+ lines.push(`${key}=${field.default}`);
93
+ } else {
94
+ lines.push(`# ${key}=`);
95
+ }
96
+ } else if (isSchema) {
97
+ const sEntry = entry as { required?: boolean; secret?: boolean; description?: string };
98
+ if (sEntry.required !== false) {
99
+ lines.push(`# Required: yes`);
100
+ } else {
101
+ lines.push(`# Required: no`);
102
+ }
103
+ if (sEntry.secret) {
104
+ lines.push(`# Secret: yes`);
105
+ }
106
+ lines.push(`# ${key}=`);
107
+ } else {
108
+ lines.push(`# ${key}=`);
109
+ }
110
+
111
+ lines.push("");
112
+ }
113
+
114
+ return lines.join("\n");
115
+ }
116
+
117
+ const typeToSchema = (entry: Record<string, unknown>): z.ZodType<unknown> => {
118
+ const type = entry.type as string;
119
+ let base: z.ZodType<unknown>;
120
+
121
+ switch (type) {
122
+ case "string":
123
+ case "host":
124
+ base = z.string();
125
+ break;
126
+ case "url":
127
+ base = z.string().url();
128
+ break;
129
+ case "number":
130
+ base = z.coerce.number();
131
+ break;
132
+ case "integer":
133
+ base = z.coerce.number().int();
134
+ break;
135
+ case "boolean":
136
+ base = z
137
+ .string()
138
+ .transform((v) => v === "true" || v === "1")
139
+ .pipe(z.boolean());
140
+ break;
141
+ case "enum":
142
+ base = z.enum((entry.values ?? []) as unknown as [string, ...string[]]);
143
+ break;
144
+ case "port":
145
+ base = z.coerce.number().int().min(1).max(65535);
146
+ break;
147
+ case "email":
148
+ base = z.string().email();
149
+ break;
150
+ default:
151
+ base = z.string();
152
+ }
153
+
154
+ if (entry.default !== undefined) {
155
+ base = base.default(entry.default as string | number | boolean);
156
+ }
157
+
158
+ return base;
159
+ };
160
+
161
+ export async function runCheck(
162
+ config: BeaconConfigFile,
163
+ activeProfile?: string
164
+ ): Promise<{
165
+ results: Array<{ key: string; status: "ok" | "missing" | "invalid"; message: string }>;
166
+ errors: Array<{ key: string; message: string }>;
167
+ }> {
168
+ const results: Array<{ key: string; status: "ok" | "missing" | "invalid"; message: string }> = [];
169
+ const errors: Array<{ key: string; message: string }> = [];
170
+
171
+ const mergedSchema = { ...config.schema };
172
+ if (activeProfile && config.profiles?.[activeProfile]) {
173
+ Object.assign(mergedSchema, config.profiles[activeProfile]);
174
+ }
175
+
176
+ for (const [key, entry] of Object.entries(mergedSchema)) {
177
+ const raw = process.env[key];
178
+ const isField = "type" in entry;
179
+
180
+ let required = true;
181
+ let hasDefault = false;
182
+
183
+ if (isField) {
184
+ const field = entry as { required?: boolean; default?: unknown };
185
+ required = field.required !== false;
186
+ hasDefault = field.default !== undefined;
187
+ } else {
188
+ const f = entry as { required?: boolean };
189
+ required = f.required !== false;
190
+ }
191
+
192
+ if (raw === undefined || raw === "") {
193
+ if (hasDefault) {
194
+ results.push({ key, status: "ok", message: "Using default value" });
195
+ } else if (!required) {
196
+ results.push({ key, status: "ok", message: "Optional, not set" });
197
+ } else {
198
+ results.push({ key, status: "missing", message: "Not set" });
199
+ errors.push({ key, message: "Missing required environment variable" });
200
+ }
201
+ continue;
202
+ }
203
+
204
+ try {
205
+ if (isField) {
206
+ const schema = typeToSchema(entry as unknown as Record<string, unknown>);
207
+ schema.parse(raw);
208
+ }
209
+ results.push({
210
+ key,
211
+ status: "ok",
212
+ message: `Set (${raw.length > 25 ? raw.substring(0, 25) + "..." : raw})`,
213
+ });
214
+ } catch {
215
+ results.push({ key, status: "invalid", message: `Invalid: "${raw}"` });
216
+ errors.push({ key, message: `Invalid value: ${raw}` });
217
+ }
218
+ }
219
+
220
+ return { results, errors };
221
+ }