@joinremba/beacon 0.4.0 → 0.5.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/README.md CHANGED
@@ -6,20 +6,38 @@
6
6
  <strong>@joinremba/beacon</strong>
7
7
  </p>
8
8
 
9
+ <p align="center">
10
+ Validate environment variables, config, secrets, and runtime feature gates before production breaks.
11
+ </p>
12
+
9
13
  <p align="center">
10
14
  <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>
15
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/@joinremba/beacon.svg" alt="License"></a>
12
16
  <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
17
  <img src="https://img.shields.io/badge/Bun-%3E%3D1.3.1-black?logo=bun" alt="Bun">
14
18
  <img src="https://img.shields.io/badge/TypeScript-6-blue" alt="TypeScript">
15
19
  </p>
16
20
 
17
- Beacon helps TypeScript teams boot applications safely by validating environment variables, config, secrets, and runtime feature gates before production breaks.
21
+ ---
22
+
23
+ ## Features
24
+
25
+ - **Schema-based validation** — Define env vars with simple string types (`"url"`, `"port"`, `"enum"`, etc.) or raw Zod schemas.
26
+ - **Aggregated errors** — All missing/invalid variables are reported at once, not one at a time.
27
+ - **Secrets redaction** — Values marked `secret: true` are automatically replaced with `[REDACTED]` in errors and CLI output.
28
+ - **Profile overrides** — Define per-environment schemas (development, staging, production) with inheritance.
29
+ - **Feature gates & kill switches** — Runtime toggles with env-var overrides and deterministic rollout hashing.
30
+ - **CLI tool** — `beacon init` generates `.env.example`, `beacon check` validates the runtime environment, `beacon drift` detects config drift.
31
+ - **Encrypted .env** — AES-256-GCM encrypt/decrypt `.env` files for safe committing.
32
+ - **Remote config** — Fetches config from the Remba cloud via `@joinremba/core` client with local fallback.
33
+ - **TypeScript-first** — Strict types, generic getter, full type exports.
34
+
35
+ ---
36
+
37
+ ## Installation
18
38
 
19
39
  ```sh
20
40
  bun add @joinremba/beacon
21
- bunx beacon init
22
- bunx beacon check
23
41
  ```
24
42
 
25
43
  ---
@@ -32,201 +50,185 @@ import { createBeacon } from "@joinremba/beacon";
32
50
  const config = createBeacon({
33
51
  DATABASE_URL: { type: "url", required: true },
34
52
  REDIS_URL: { type: "url", required: true },
35
- NODE_ENV: {
36
- type: "enum",
37
- values: ["development", "test", "staging", "production"],
38
- default: "development",
39
- },
40
53
  PORT: { type: "port", default: 3000 },
54
+ NODE_ENV: { type: "enum", values: ["development", "production"], default: "development" },
41
55
  API_KEY: { type: "string", required: true, secret: true },
42
56
  });
43
57
 
44
- config.ensure();
58
+ await config.ensure();
45
59
 
46
60
  const dbUrl = config.get<string>("DATABASE_URL");
61
+ const port = config.get<number>("PORT");
47
62
  ```
48
63
 
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.
64
+ If any required variable is missing or invalid, `ensure()` throws a `ConfigValidationError` with **all** issues collected at once — fix everything in a single pass.
50
65
 
51
66
  ---
52
67
 
53
- ## Why Beacon?
68
+ ## Schema Types
54
69
 
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:
70
+ Each field in the schema can be defined with a `type` string. Beacon coerces and validates the raw `process.env` value accordingly.
56
71
 
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
72
+ | Type | Validation | Coercion | Example |
73
+ | --------- | ------------------------------------------------------- | ------------------------------------------- | -------------------------------------- |
74
+ | `string` | Any string | — | `{ type: "string" }` |
75
+ | `host` | Any string (alias for `string`) | — | `{ type: "host" }` |
76
+ | `url` | Valid URL | — | `{ type: "url" }` |
77
+ | `number` | Finite number | `z.coerce.number()` | `{ type: "number" }` |
78
+ | `integer` | Integer | `z.coerce.number().int()` | `{ type: "integer" }` |
79
+ | `port` | Integer in range 1–65535 | `z.coerce.number().int().min(1).max(65535)` | `{ type: "port", default: 3000 }` |
80
+ | `boolean` | `"true"` / `"false"` / `"1"` / `"0"` / `"yes"` / `"no"` | String transform | `{ type: "boolean" }` |
81
+ | `enum` | Must be one of `values[]` | `z.enum(values)` | `{ type: "enum", values: ["a", "b"] }` |
82
+ | `email` | Valid email | `z.string().email()` | `{ type: "email" }` |
61
83
 
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.
84
+ ```ts
85
+ const config = createBeacon({
86
+ APP_NAME: { type: "string", default: "my-app" },
87
+ DATABASE_URL: { type: "url", required: true },
88
+ WORKERS: { type: "integer", default: 4 },
89
+ PORT: { type: "port", default: 3000 },
90
+ SUPPORT_EMAIL: { type: "email", required: true },
91
+ LOG_LEVEL: { type: "enum", values: ["debug", "info", "warn", "error"], default: "info" },
92
+ ENABLE_METRICS: { type: "boolean", default: false },
93
+ });
94
+ ```
63
95
 
64
- ---
96
+ You can also pass raw **Zod schemas** directly for custom validation:
65
97
 
66
- ## Features
98
+ ```ts
99
+ import { z } from "zod";
67
100
 
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.
101
+ const config = createBeacon({
102
+ WHITELIST: { schema: z.string().regex(/^[\d,]+$/) },
103
+ TIMEOUT: { schema: z.coerce.number().positive().max(30000) },
104
+ });
105
+ ```
76
106
 
77
107
  ---
78
108
 
79
- ## CLI
109
+ ## `.ensure()`
80
110
 
81
- Beacon ships with a CLI for development and CI workflows.
111
+ Validates every variable in the schema against the current `process.env`. Must be called before any `get()` call.
82
112
 
83
- ### `beacon init`
113
+ ```ts
114
+ await config.ensure();
115
+ ```
84
116
 
85
- Generate a documented `.env.example` from your beacon config:
117
+ **Behavior:**
86
118
 
87
- ```sh
88
- bunx beacon init
119
+ | Condition | strict: true (default) | strict: false |
120
+ | --------------------------------- | ------------------------------ | ---------------------------- |
121
+ | Required var present, valid | Passes | Passes |
122
+ | Required var missing | `ConfigValidationError` thrown | Silently skipped |
123
+ | Required var present, invalid | `ConfigValidationError` thrown | Silently skipped |
124
+ | Optional var not set, has default | Default applied | Default applied |
125
+ | Optional var not set, no default | Skipped | Skipped |
126
+ | Remote config available | Merged into validated values | Merged into validated values |
89
127
 
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
- ```
128
+ Errors are aggregated into a single `ConfigValidationError` (extends `AggregateError`). Access individual issues via `err.errors`:
96
129
 
97
- Output includes types, defaults, descriptions, and secret markers for every variable:
130
+ ```ts
131
+ import { ConfigValidationError } from "@joinremba/beacon";
98
132
 
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
133
+ try {
134
+ await config.ensure();
135
+ } catch (err) {
136
+ if (err instanceof ConfigValidationError) {
137
+ for (const issue of err.errors) {
138
+ console.error(`[${issue.key}] ${issue.message}`);
139
+ }
140
+ }
141
+ process.exit(1);
142
+ }
109
143
  ```
110
144
 
111
- ### `beacon check`
145
+ The `ensure({ strict: false })` mode is useful during testing or bootstrap when some env vars may not yet be configured.
112
146
 
113
- Validate your current environment against your schema:
147
+ ---
114
148
 
115
- ```sh
116
- bunx beacon check
149
+ ## `.getAll()`
117
150
 
118
- # With a specific profile:
119
- bunx beacon check --profile staging
151
+ Returns a `Record<string, unknown>` of **all** resolved values after `ensure()` completes.
120
152
 
121
- # Custom config:
122
- bunx beacon check -c ./config/production.json
153
+ ```ts
154
+ await config.ensure();
155
+ const all = config.getAll();
156
+ // { DATABASE_URL: "postgres://...", PORT: 3000, NODE_ENV: "development", ... }
123
157
  ```
124
158
 
125
- Output is a colour-coded table:
159
+ Useful for debugging, logging startup configuration, or passing the full config object to subsystems.
126
160
 
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
- ```
161
+ ---
140
162
 
141
- Exit codes: `0` if all pass, `1` if any issues found.
163
+ ## `.get<T>(key)`
142
164
 
143
- ### Per-command help
165
+ Type-safe accessor for individual validated values. Call `.get<T>(key)` with an optional generic type parameter.
144
166
 
145
- ```sh
146
- beacon help init
147
- beacon check --help
167
+ ```ts
168
+ const port = config.get<number>("PORT"); // type: number
169
+ const dbUrl = config.get<string>("DATABASE_URL"); // type: string
170
+ const debug = config.get<boolean>("DEBUG"); // type: boolean
148
171
  ```
149
172
 
173
+ Throws `ConfigError` if:
174
+
175
+ - Called before `ensure()` — `"Call beacon.ensure() before accessing config values"`
176
+ - Key doesn't exist in the schema — `"Unknown config key: <key>"`
177
+
150
178
  ---
151
179
 
152
- ## API Reference
180
+ ## Secrets
153
181
 
154
- ### `createBeacon(schema, options?)`
182
+ Mark sensitive fields with `secret: true` to prevent their values from appearing in error messages, CLI output, or logs.
155
183
 
156
- The default export. Accepts an env schema and optional configuration.
184
+ ```ts
185
+ const config = createBeacon({
186
+ API_KEY: { type: "string", required: true, secret: true },
187
+ DATABASE_URL: { type: "url", secret: true },
188
+ });
189
+ ```
157
190
 
158
- **Parameters**
191
+ When a secret value fails validation, the error message shows `[REDACTED]` instead of the actual value:
159
192
 
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
- | `features` | `Record<string, FeatureGate>` | Feature gates for runtime toggles. |
193
+ ```
194
+ Environment variable API_KEY=[REDACTED]: Invalid value
195
+ ```
166
196
 
167
- **SchemaEntry** can be either:
197
+ The secret metadata is also accessible at runtime:
168
198
 
169
- **1. String-based** — Simple type names for everyday use:
199
+ ```ts
200
+ config.secret; // { API_KEY: true, DATABASE_URL: true }
201
+ ```
170
202
 
171
- | Field | Type | Default | Description |
172
- | ------------- | ----------- | ------- | ------------------------------------ |
173
- | `type` | `FieldType` | — | The type to validate against. |
174
- | `required` | `boolean` | `true` | Whether the variable must be set. |
175
- | `default` | `unknown` | — | Default value if not set. |
176
- | `secret` | `boolean` | `false` | Redact value from errors and logs. |
177
- | `values` | `string[]` | — | Allowed values (only for `"enum"`). |
178
- | `description` | `string` | — | Used when generating `.env.example`. |
203
+ ---
179
204
 
180
- | Type | Zod equivalent |
181
- | --------- | -------------------------------------------- |
182
- | `string` | `z.string()` |
183
- | `url` | `z.string().url()` |
184
- | `number` | `z.coerce.number()` |
185
- | `integer` | `z.coerce.number().int()` |
186
- | `boolean` | `"true"` / `"false"` / `"1"` / `"0"` coerced |
187
- | `port` | integer 1–65535 |
188
- | `enum` | requires `values[]` |
189
- | `email` | `z.string().email()` |
190
- | `host` | `z.string()` |
205
+ ## NODE_ENV Handling
191
206
 
192
- **2. Zod schema**Advanced users can pass Zod schemas directly:
207
+ Beacon handles `NODE_ENV` like any other env var define it in your schema with an `enum` type and a default:
193
208
 
194
209
  ```ts
195
- {
196
- PORT: { schema: z.coerce.number().positive().max(9999) },
197
- WHITELIST: { schema: z.string().regex(/^[\d,]+$/) },
198
- }
210
+ const config = createBeacon({
211
+ NODE_ENV: {
212
+ type: "enum",
213
+ values: ["development", "test", "staging", "production"],
214
+ default: "development",
215
+ },
216
+ });
199
217
  ```
200
218
 
201
- **Returns**
219
+ **Defaults behavior:**
202
220
 
203
- A config instance with:
221
+ - When an env var is not set and has a `default`, the default value is used silently.
222
+ - When an env var is not set, has no default, but `required: false`, it is silently skipped.
223
+ - When an env var is not set, has no default, and `required: true` (the default), it fails validation.
204
224
 
205
- | Method / Property | Description |
206
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
207
- | `ensure()` | Validates all env vars. Throws `ConfigValidationError` on failure. Returns the config instance for chaining. |
208
- | `get<T>(key): T` | Returns the validated value for the given key. Throws if called before `ensure()`. |
209
- | `secret` | Returns a `Record<string, boolean>` of which keys are marked as secrets. |
210
- | `isEnabled(feature): boolean` | Checks if a feature gate is enabled. Respects env overrides (`FEATURE_<NAME>`). |
225
+ This means you can safely deploy with minimal env vars during development as long as sensible defaults are provided.
211
226
 
212
- ### TypeScript Types
213
-
214
- ```ts
215
- import type {
216
- BeaconOptions,
217
- Beacon,
218
- SchemaEntry,
219
- FieldDefinition,
220
- FieldType,
221
- FeatureGate,
222
- ConfigError,
223
- ConfigValidationError,
224
- } from "@joinremba/beacon";
225
- ```
227
+ ---
226
228
 
227
- ### Config file (`.beaconrc.json`)
229
+ ## `.beaconrc.json` (CLI Config File)
228
230
 
229
- Used by the CLI for `init` and `check` commands:
231
+ The CLI reads schema, profiles, and features from a JSON config file. It looks for `.beaconrc.json` or `beacon.config.json` in the project root.
230
232
 
231
233
  ```json
232
234
  {
@@ -236,17 +238,29 @@ Used by the CLI for `init` and `check` commands:
236
238
  "required": true,
237
239
  "description": "PostgreSQL connection string"
238
240
  },
239
- "PORT": { "type": "port", "default": 3000, "description": "HTTP server port" },
241
+ "PORT": {
242
+ "type": "port",
243
+ "default": 3000,
244
+ "description": "HTTP server port"
245
+ },
240
246
  "NODE_ENV": {
241
247
  "type": "enum",
242
- "values": ["development", "production"],
248
+ "values": ["development", "staging", "production"],
243
249
  "default": "development"
244
250
  },
245
- "API_KEY": { "type": "string", "required": true, "secret": true }
251
+ "API_KEY": {
252
+ "type": "string",
253
+ "required": true,
254
+ "secret": true,
255
+ "description": "API signing key"
256
+ }
246
257
  },
247
258
  "profiles": {
248
259
  "production": {
249
260
  "DB_HOST": { "type": "host", "required": true, "description": "Production DB hostname" }
261
+ },
262
+ "staging": {
263
+ "DB_HOST": { "type": "host", "required": true, "description": "Staging DB hostname" }
250
264
  }
251
265
  },
252
266
  "features": {
@@ -256,232 +270,179 @@ Used by the CLI for `init` and `check` commands:
256
270
  }
257
271
  ```
258
272
 
259
- ---
273
+ **CLI commands:**
260
274
 
261
- ## Examples
275
+ ```sh
276
+ # Generate .env.example from the config
277
+ bunx beacon init
278
+ bunx beacon init --profile production
279
+ bunx beacon init --all-profiles
262
280
 
263
- ### Basic env validation
281
+ # Validate current environment against schema
282
+ bunx beacon check
283
+ bunx beacon check --profile staging
264
284
 
265
- ```ts
266
- import { createBeacon } from "@joinremba/beacon";
285
+ # Detect config drift (missing vars, type mismatches)
286
+ bunx beacon drift
287
+ bunx beacon drift --profile production
267
288
 
268
- const config = createBeacon({
269
- NODE_ENV: {
270
- type: "enum",
271
- values: ["development", "production", "test"],
272
- default: "development",
273
- },
274
- PORT: { type: "port", default: 3000 },
275
- });
289
+ # Encrypt/decrypt .env files for safe committing
290
+ BEACON_ENCRYPTION_KEY=... beacon encrypt
291
+ BEACON_ENCRYPTION_KEY=... beacon decrypt
292
+
293
+ # Validate env in Docker/Kubernetes
294
+ bunx beacon docker
276
295
 
277
- config.ensure();
278
- console.log(config.get("PORT"));
296
+ # Print secret rotation checklist
297
+ bunx beacon rotate
279
298
  ```
280
299
 
281
- ### With secrets redaction
300
+ ---
282
301
 
283
- ```ts
284
- const config = createBeacon({
285
- API_KEY: { type: "string", secret: true },
286
- DATABASE_URL: { type: "url", secret: true },
287
- });
302
+ ## Configuration Reference
288
303
 
289
- config.ensure();
290
- // Error messages never show API_KEY or DATABASE_URL values
291
- ```
304
+ ### `createBeacon(schema, options?)`
292
305
 
293
- ### Custom error handling
306
+ | Option | Type | Default | Description |
307
+ | -------------- | --------------------------------------------- | ------- | --------------------------------------------------------- |
308
+ | `schema` | `Record<string, SchemaEntry>` | — | Map of env var names to field definitions (required). |
309
+ | `profile` | `string` | — | Active profile name. Merges matching entry from profiles. |
310
+ | `profiles` | `Record<string, Record<string, SchemaEntry>>` | — | Named profile overrides. Keyed by profile name. |
311
+ | `features` | `Record<string, FeatureGate>` | — | Feature gate definitions for `isEnabled()`. |
312
+ | `killSwitches` | `Record<string, boolean>` | — | Kill-switch flags. Overrides feature gates. |
313
+ | `client` | `Client` (from `@joinremba/core`) | — | Remote config client. Fetches config from cloud. |
314
+
315
+ ### `SchemaEntry`
316
+
317
+ **String-based field definition:**
318
+
319
+ | Field | Type | Default | Description |
320
+ | ------------- | ----------- | ------- | ----------------------------------------------------------------------------------------- |
321
+ | `type` | `FieldType` | — | One of: `string`, `url`, `number`, `integer`, `boolean`, `port`, `enum`, `host`, `email`. |
322
+ | `required` | `boolean` | `true` | Whether the variable must be present in env. |
323
+ | `default` | `unknown` | — | Default value when not set. |
324
+ | `secret` | `boolean` | `false` | Redact value from errors, logs, and CLI output. |
325
+ | `values` | `string[]` | — | Allowed values (required for `"enum"` type). |
326
+ | `description` | `string` | — | Human-readable description. Used in `.env.example`. |
327
+
328
+ **Zod-based field definition:**
329
+
330
+ | Field | Type | Default | Description |
331
+ | ------------- | ----------- | ------- | ----------------------------------------------- |
332
+ | `schema` | `z.ZodType` | — | A Zod schema for custom validation logic. |
333
+ | `required` | `boolean` | `true` | Whether the variable must be present in env. |
334
+ | `secret` | `boolean` | `false` | Redact value from errors, logs, and CLI output. |
335
+ | `description` | `string` | — | Human-readable description. |
336
+
337
+ ### Returned Beacon instance
338
+
339
+ | Method / Property | Returns | Description |
340
+ | -------------------- | ------------------------- | ------------------------------------------ |
341
+ | `ensure(options?)` | `Promise<Beacon>` | Validates all env vars. Throws on failure. |
342
+ | `getAll()` | `Record<string, unknown>` | Returns all resolved values. |
343
+ | `get<T>(key)` | `T` | Returns a single typed value. |
344
+ | `secret` | `Record<string, boolean>` | Which keys are marked as secrets. |
345
+ | `isEnabled(feature)` | `boolean` | Checks if a feature gate is enabled. |
346
+ | `isKilled(feature)` | `boolean` | Checks if a kill switch is active. |
347
+
348
+ ### `EnsureOptions`
349
+
350
+ | Option | Type | Default | Description |
351
+ | -------- | --------- | ------- | ------------------------------------------------ |
352
+ | `strict` | `boolean` | `true` | When `false`, missing required vars are skipped. |
353
+
354
+ ### `FeatureGate`
355
+
356
+ | Option | Type | Default | Description |
357
+ | ------------- | --------- | ------- | -------------------------------------------------- |
358
+ | `enabled` | `boolean` | `false` | Whether the feature is on by default. |
359
+ | `rollout` | `number` | — | Rollout percentage (0–1). Uses deterministic hash. |
360
+ | `description` | `string` | — | Human-readable description. |
294
361
 
295
- ```ts
296
- import { ConfigValidationError } from "@joinremba/beacon";
362
+ ---
297
363
 
298
- try {
299
- config.ensure();
300
- } catch (err) {
301
- if (err instanceof ConfigValidationError) {
302
- for (const issue of err.errors) {
303
- console.error(`[${issue.key}] ${issue.message}`);
304
- }
305
- }
306
- process.exit(1);
307
- }
308
- ```
364
+ ## TypeScript
309
365
 
310
- ### Production profile
366
+ Beacon ships with strict TypeScript types. All public APIs are fully typed.
311
367
 
312
368
  ```ts
313
- const config = createBeacon(
314
- {
315
- DB_HOST: { type: "string", default: "localhost" },
316
- DB_PORT: { type: "port", default: 5432 },
317
- },
318
- {
319
- profile: "production",
320
- profiles: {
321
- production: {
322
- DB_HOST: { type: "host", required: true },
323
- DB_PORT: { type: "port", required: true },
324
- },
325
- },
326
- }
327
- );
369
+ import { createBeacon } from "@joinremba/beacon";
370
+ import type {
371
+ Beacon,
372
+ BeaconOptions,
373
+ SchemaEntry,
374
+ FieldDefinition,
375
+ FieldType,
376
+ FeatureGate,
377
+ EnsureOptions,
378
+ ConfigError,
379
+ ConfigValidationError,
380
+ } from "@joinremba/beacon";
328
381
  ```
329
382
 
330
- ### Feature Gates
331
-
332
- Toggle features on/off without redeploying. Define gates in the `features` option and check them with `isEnabled()`:
383
+ **Generic inference with `.get<T>()`:**
333
384
 
334
385
  ```ts
335
- const config = createBeacon(
336
- { DATABASE_URL: { type: "url" } },
337
- {
338
- features: {
339
- newDashboard: { enabled: true },
340
- darkMode: { enabled: false },
341
- gradualRollout: { enabled: true, rollout: 0.5 },
342
- },
343
- }
344
- );
345
-
346
- config.isEnabled("newDashboard"); // true
347
- config.isEnabled("darkMode"); // false
348
- config.isEnabled("gradualRollout"); // true for ~50% of deployments (deterministic hash)
349
- ```
386
+ const config = createBeacon({
387
+ PORT: { type: "port", default: 3000 },
388
+ DEBUG: { type: "boolean", default: false },
389
+ });
350
390
 
351
- **Env overrides** — Any feature can be toggled at runtime via `FEATURE_<NAME>`:
391
+ await config.ensure();
352
392
 
353
- ```sh
354
- FEATURE_DARK_MODE=true bun start
393
+ const port = config.get<number>("PORT"); // inferred as number
394
+ const debug = config.get<boolean>("DEBUG"); // inferred as boolean
355
395
  ```
356
396
 
357
- CamelCase feature names map to `FEATURE_` prefixed uppercase with underscores (`newDashboard` → `FEATURE_NEW_DASHBOARD`). Accepted truthy values: `true`, `1`, `yes`. Everything else is `false`.
358
-
359
- | Option | Type | Default | Description |
360
- | ------------- | --------- | ------- | --------------------------------------------------------- |
361
- | `enabled` | `boolean` | `false` | Whether the gate is on by default. |
362
- | `rollout` | `number` | — | Percentage of deployments (0–1). Uses deterministic hash. |
363
- | `description` | `string` | — | Human-readable description. |
397
+ ---
364
398
 
365
- ### Kill-Switch Flags
399
+ ## Integration with `@joinremba/core`
366
400
 
367
- Force-disable a feature at runtime overrides any feature gate. Define kill switches in the `killSwitches` option or set `KILL_<NAME>` env vars:
401
+ Beacon can fetch remote configuration from the Remba cloud by passing a `Client` instance from `@joinremba/core`.
368
402
 
369
403
  ```ts
404
+ import { createBeacon } from "@joinremba/beacon";
405
+ import { createClient } from "@joinremba/core";
406
+
407
+ const client = createClient({
408
+ apiKey: "api_core_live_abc123",
409
+ });
410
+
370
411
  const config = createBeacon(
371
- { DATABASE_URL: { type: "url" } },
372
412
  {
373
- features: { newDashboard: { enabled: true } },
374
- killSwitches: { newDashboard: true },
375
- }
413
+ LOCAL_VAR: { type: "string", default: "local-fallback" },
414
+ PORT: { type: "port", default: 3000 },
415
+ },
416
+ { client }
376
417
  );
377
418
 
378
- config.isEnabled("newDashboard"); // false — killed
379
- config.isKilled("newDashboard"); // true
380
- ```
381
-
382
- **Env override** — `KILL_NEW_DASHBOARD=true` overrides the config. Accepted truthy values: `true`, `1`, `yes`.
383
-
384
- When a feature is killed, `isEnabled()` returns `false` regardless of the feature gate configuration.
385
-
386
- ### Encrypted .env (`beacon encrypt` / `beacon decrypt`)
387
-
388
- Commit `.env` files safely using AES-256-GCM encryption. Requires an encryption key passed via `--key` or `BEACON_ENCRYPTION_KEY`.
389
-
390
- ```sh
391
- # Encrypt .env → .env.encrypted
392
- BEACON_ENCRYPTION_KEY=your-256-bit-key beacon encrypt
393
-
394
- # Decrypt back to plaintext
395
- BEACON_ENCRYPTION_KEY=your-256-bit-key beacon decrypt
396
-
397
- # Custom paths
398
- beacon encrypt -i .env.prod -o .env.prod.encrypted --key "your-key"
399
- beacon decrypt -i .env.prod.encrypted -o .env.prod --key "your-key"
419
+ await config.ensure();
400
420
  ```
401
421
 
402
- ### Secret Rotation Checklist (`beacon rotate`)
403
-
404
- Prints a step-by-step checklist for rotating secrets (DB credentials, API keys, etc.):
422
+ **How remote config merging works:**
405
423
 
406
- ```sh
407
- beacon rotate
408
- ```
424
+ 1. On `ensure()`, beacon calls `client.getConfig()`.
425
+ 2. Remote entries whose keys are **not** in the local schema and **not** already set in `process.env` are added to the validated values.
426
+ 3. Remote entries whose keys **are** in the local schema are **ignored** — the schema definition always wins.
427
+ 4. If the network request fails (timeout, DNS error, etc.), beacon silently falls back to local-only mode.
409
428
 
410
- Follows the generate deploy alongside → update consumers → verify → revoke → audit workflow.
429
+ This gives you a layered config priority:
411
430
 
412
- ### Config Drift Detection (`beacon drift`)
413
-
414
- Detects when your actual environment differs from the schema defined in your config:
415
-
416
- ```sh
417
- beacon drift
418
- beacon drift --profile production
419
431
  ```
420
-
421
- Reports missing required variables, type mismatches, and unexpected enum values.
422
-
423
- ### Docker/Kubernetes Checks (`beacon docker`)
424
-
425
- Validates your environment in container contexts — detects Docker and Kubernetes runtimes, checks common container env vars, and runs a full schema validation:
426
-
427
- ```sh
428
- beacon docker
429
- beacon docker --profile staging
432
+ process.env > schema defaults > remote config
430
433
  ```
431
434
 
432
- ---
433
-
434
- ## Roadmap
435
-
436
- **MVP** (current)
437
-
438
- - Typed env validation with string-based types and Zod
439
- - Missing variable detection (aggregated errors)
440
- - Secrets redaction in errors and logs
441
- - Local/staging/production profiles
442
- - `.env.example` generation via CLI
443
- - `beacon check` CLI command
444
- - Coloured CLI output with suggestions
445
-
446
- **V1** ✅
447
-
448
- - Feature gates from local config
449
- - Kill-switch flags (`KILL_<NAME>` env vars, `config.isKilled()`)
450
- - Encrypted `.env` support (`beacon encrypt` / `beacon decrypt`)
451
- - Secret rotation checklist (`beacon rotate`)
452
- - CI validation action (`beacon check` in CI workflows)
453
- - Docker/Kubernetes env checks (`beacon docker`)
454
- - Config drift detection (`beacon drift`)
455
-
456
- **V2**
457
-
458
- - Hosted team secret sync
459
- - Audit trail for config changes
460
- - Deployment provider integrations
461
- - GitHub Actions integration
462
- - Remba Cloud dashboard
435
+ Use this to share non-sensitive config (feature flags, service URLs) across deployments without storing everything in every CI/CD pipeline.
463
436
 
464
437
  ---
465
438
 
466
439
  ## Related Packages
467
440
 
468
- - [@joinremba/catalog](https://github.com/joinremba/catalog) — Production-ready logging and error event layer built on Pino.
441
+ - [@joinremba/catalog](https://github.com/joinremba/catalog) — Production-ready logging and error event layer on Pino.
469
442
  - [@joinremba/gate](https://github.com/joinremba/gate) — API safety layer: validation, responses, idempotency, rate limiting, and API keys.
470
443
 
471
- ## Social Preview
472
-
473
- 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:
474
-
475
- 1. Go to your repo **Settings** → **Social preview** → **Upload image**
476
- 2. Select `assets/og-image.svg`
477
- 3. Save
478
-
479
- This will be used whenever your repo link is shared on social media, Slack, or Discord.
480
-
481
- ## Contributing
482
-
483
- Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, development process, and how to submit pull requests.
444
+ ---
484
445
 
485
446
  ## License
486
447
 
487
- MIT &mdash; see [LICENSE](LICENSE).
448
+ MIT see [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joinremba/beacon",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Validate environment variables, config, secrets, and runtime feature gates before production breaks.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/cli.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { loadConfig, type BeaconConfigFile } from "./cli-config";
4
4
  import { generateEnvExample } from "./cli-config";
5
5
  import { runCheck } from "./cli-config";
6
+ import { typeToSchema, type SchemaField } from "./schema";
6
7
  import { encryptEnv, decryptEnv } from "./encryption";
7
8
  import { color, icon, formatCheckResult, formatSummary, suggestKeys } from "./cli-format";
8
9
 
@@ -206,6 +207,29 @@ async function handleCheck(args: ParsedArgs) {
206
207
 
207
208
  process.stdout.write(formatCheckResult(result.results));
208
209
 
210
+ if (config.features) {
211
+ const featureEntries = Object.entries(config.features);
212
+ if (featureEntries.length > 0) {
213
+ process.stdout.write(`\n ${color.bold("Feature Gates")}\n\n`);
214
+ for (const [name, gate] of featureEntries) {
215
+ const envName = `FEATURE_${name
216
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
217
+ .replace(/[^a-zA-Z0-9]/g, "_")
218
+ .toUpperCase()}`;
219
+ const envVal = process.env[envName];
220
+ const enabled =
221
+ envVal !== undefined && envVal !== ""
222
+ ? envVal === "true" || envVal === "1" || envVal === "yes"
223
+ : gate.enabled;
224
+ const icon_ = enabled ? icon.pass : icon.fail;
225
+ const status = enabled ? color.green("enabled") : color.red("disabled");
226
+ const desc = gate.description ? ` ${color.dim(`— ${gate.description}`)}` : "";
227
+ process.stdout.write(` ${icon_} ${color.bold(name)}: ${status}${desc}\n`);
228
+ }
229
+ process.stdout.write("\n");
230
+ }
231
+ }
232
+
209
233
  if (result.errors.length > 0) {
210
234
  for (const err of result.errors) {
211
235
  const suggestions = suggestKeys(
@@ -343,13 +367,24 @@ async function handleDrift(args: ParsedArgs) {
343
367
  }
344
368
 
345
369
  if (isField) {
346
- const field = entry as { type?: string; values?: readonly string[] };
347
- if (field.type === "enum" && field.values && !field.values.includes(raw)) {
348
- driftResults.push({
349
- key,
350
- expected: `one of: ${field.values.join(" | ")}`,
351
- actual: raw,
352
- });
370
+ const schema = typeToSchema(entry as SchemaField);
371
+ const result = schema.safeParse(raw);
372
+ if (!result.success) {
373
+ const field = entry as { type?: string; values?: readonly string[] };
374
+ if (field.type === "enum" && field.values) {
375
+ driftResults.push({
376
+ key,
377
+ expected: `one of: ${field.values.join(" | ")}`,
378
+ actual: raw,
379
+ });
380
+ } else {
381
+ const issue = result.error.issues[0];
382
+ driftResults.push({
383
+ key,
384
+ expected: issue?.message ?? "valid value",
385
+ actual: raw,
386
+ });
387
+ }
353
388
  }
354
389
  }
355
390
  }
package/src/index.ts CHANGED
@@ -178,6 +178,13 @@ export function createBeacon(
178
178
  return validated[key] as T;
179
179
  },
180
180
 
181
+ getAll(): Record<string, unknown> {
182
+ if (validated === null) {
183
+ throw new ConfigError("beacon", "Call beacon.ensure() before accessing config values");
184
+ }
185
+ return { ...validated };
186
+ },
187
+
181
188
  get secret(): Record<string, boolean> {
182
189
  const map: Record<string, boolean> = {};
183
190
  for (const key of secretKeys) {
package/src/types.ts CHANGED
@@ -53,7 +53,8 @@ export interface EnsureOptions {
53
53
  export interface Beacon {
54
54
  ensure(options?: EnsureOptions): Promise<Beacon>;
55
55
  get<T = unknown>(key: string): T;
56
+ getAll(): Record<string, unknown>;
56
57
  readonly secret: Record<string, boolean>;
57
- isEnabled(feature: string): boolean;
58
58
  isKilled(feature: string): boolean;
59
+ isEnabled(feature: string): boolean;
59
60
  }