@joinremba/beacon 0.4.0 → 0.5.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/README.md CHANGED
@@ -8,13 +8,13 @@
8
8
 
9
9
  <p align="center">
10
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>
11
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/@joinremba/beacon.svg" alt="License"></a>
12
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
13
  <img src="https://img.shields.io/badge/Bun-%3E%3D1.3.1-black?logo=bun" alt="Bun">
14
14
  <img src="https://img.shields.io/badge/TypeScript-6-blue" alt="TypeScript">
15
15
  </p>
16
16
 
17
- Beacon helps TypeScript teams boot applications safely by validating environment variables, config, secrets, and runtime feature gates before production breaks.
17
+ Beacon validates environment variables, config, secrets, and runtime feature gates before production breaks. Define your schema once — get type coercion, secret redaction, profile merging, per-command CLI tooling, and AES-256-GCM encryption for your `.env` files.
18
18
 
19
19
  ```sh
20
20
  bun add @joinremba/beacon
@@ -24,79 +24,207 @@ bunx beacon check
24
24
 
25
25
  ---
26
26
 
27
+ ## Features
28
+
29
+ - **Schema-based validation** with type coercion — `string`, `url`, `number`, `integer`, `boolean`, `enum`, `port`, `host`, `email`
30
+ - **Custom Zod schemas** for advanced validation rules
31
+ - **Aggregated error reporting** — all errors collected at once, not fail-fast
32
+ - **Secret redaction** — `[REDACTED]` in error messages and CLI output
33
+ - **Default values** for optional variables
34
+ - **Profile merging** — define different schemas per environment (dev, staging, production)
35
+ - **Feature gates** with percentage-based rollout (deterministic djb2 hash)
36
+ - **Kill switches** — `KILL_<NAME>` env var takes priority over feature gates
37
+ - **Environment overrides** — `FEATURE_<NAME>` env var toggles features at runtime
38
+ - **Remote config** via `client` option — connect to Nexus backend
39
+ - **`strict: false` mode** for tests and bootstrap
40
+ - **CLI commands** — `init`, `check`, `encrypt`, `decrypt`, `rotate`, `drift`, `docker`
41
+ - **AES-256-GCM encryption** for `.env` files
42
+ - **Zero runtime dependencies** beyond `zod` and `@joinremba/core`
43
+
44
+ ---
45
+
46
+ ## Installation
47
+
48
+ ```sh
49
+ bun add @joinremba/beacon
50
+ ```
51
+
52
+ ---
53
+
27
54
  ## Quick Start
28
55
 
29
56
  ```ts
30
57
  import { createBeacon } from "@joinremba/beacon";
31
58
 
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 },
59
+ const beacon = createBeacon({
60
+ PORT: { type: "port", default: 3000 },
61
+ DATABASE_URL: { type: "url", secret: true },
62
+ NODE_ENV: { type: "enum", values: ["dev", "prod"], default: "dev" },
63
+ FEATURE_NEW_CHECKOUT: { type: "boolean", default: "false" },
42
64
  });
43
65
 
44
- config.ensure();
45
-
46
- const dbUrl = config.get<string>("DATABASE_URL");
66
+ await beacon.ensure();
67
+ const port = beacon.get<number>("PORT"); // 3000
68
+ const dbUrl = beacon.get<string>("DATABASE_URL"); // process.env value
47
69
  ```
48
70
 
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.
71
+ If any variable is missing or invalid, `ensure()` throws a `ConfigValidationError` with **all issues reported at once** — so you fix everything in a single pass.
50
72
 
51
73
  ---
52
74
 
53
- ## Why Beacon?
75
+ ## Schema Field Types
54
76
 
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:
77
+ | Type | Description | Coercion / Validation |
78
+ |-----------|--------------------------------------|----------------------------------------|
79
+ | `string` | Any string value | `z.string()` |
80
+ | `url` | Valid URL | `z.string().url()` |
81
+ | `number` | Coerced to number | `z.coerce.number()` |
82
+ | `integer` | Coerced to integer | `z.coerce.number().int()` |
83
+ | `boolean` | `true` / `false` / `1` / `0` / `yes` / `no` | String transform + `z.boolean()` |
84
+ | `enum` | Must match one of `values[]` | `z.enum(values)` |
85
+ | `port` | Integer 1–65535 | `z.coerce.number().int().min(1).max(65535)` |
86
+ | `host` | Hostname string | `z.string()` |
87
+ | `email` | Valid email format | `z.string().email()` |
56
88
 
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
89
+ ### Schema Entry Options
61
90
 
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.
91
+ ```ts
92
+ { type: "url", required: true, default: "https://localhost", secret: true, description: "DB connection string" }
93
+ ```
63
94
 
64
- ---
95
+ | Field | Type | Default | Description |
96
+ |---------------|-------------|----------|------------------------------------------|
97
+ | `type` | `FieldType` | — | The field type to validate against. |
98
+ | `required` | `boolean` | `true` | Whether the variable must be set. |
99
+ | `default` | `unknown` | — | Default value when not set in env. |
100
+ | `secret` | `boolean` | `false` | Redact value from errors and CLI output. |
101
+ | `values` | `string[]` | — | Allowed values (for `"enum"` type only). |
102
+ | `description` | `string` | — | Used when generating `.env.example`. |
65
103
 
66
- ## Features
104
+ ### Custom Zod Schemas
105
+
106
+ For advanced validation, pass a Zod schema directly:
67
107
 
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.
108
+ ```ts
109
+ import { z } from "zod";
110
+
111
+ const beacon = createBeacon({
112
+ PORT: { schema: z.coerce.number().positive().max(9999) },
113
+ WHITELIST: { schema: z.string().regex(/^[\d,]+$/) },
114
+ });
115
+ ```
76
116
 
77
117
  ---
78
118
 
79
- ## CLI
119
+ ## Feature Gates
80
120
 
81
- Beacon ships with a CLI for development and CI workflows.
121
+ Toggle features on and off without redeploying. Define gates in the `features` option and check them with `isEnabled()`.
82
122
 
83
- ### `beacon init`
123
+ ```ts
124
+ const beacon = createBeacon(
125
+ { DATABASE_URL: { type: "url" } },
126
+ {
127
+ features: {
128
+ newDashboard: { enabled: true },
129
+ darkMode: { enabled: false },
130
+ gradualRollout: { enabled: true, rollout: 0.5 },
131
+ },
132
+ killSwitches: {
133
+ brokenFeature: true, // force-disabled
134
+ },
135
+ }
136
+ );
137
+
138
+ beacon.isEnabled("newDashboard"); // true
139
+ beacon.isEnabled("darkMode"); // false
140
+ beacon.isEnabled("gradualRollout"); // true for ~50% of instances (deterministic hash)
141
+ beacon.isEnabled("brokenFeature"); // false — killed
142
+ beacon.isKilled("brokenFeature"); // true
143
+ ```
144
+
145
+ ### `FeatureGate`
146
+
147
+ | Option | Type | Default | Description |
148
+ |---------------|-----------|----------|-----------------------------------------------------------|
149
+ | `enabled` | `boolean` | `false` | Whether the gate is on by default. |
150
+ | `rollout` | `number` | — | Fraction of instances (0–1) that see the feature. Uses deterministic djb2 hash. |
151
+ | `description` | `string` | — | Human-readable description. |
84
152
 
85
- Generate a documented `.env.example` from your beacon config:
153
+ ### Environment Overrides
154
+
155
+ Set `FEATURE_<NAME>` in the environment to override any feature gate at runtime. CamelCase names are converted to `SCREAMING_SNAKE_CASE` (`newDashboard` → `FEATURE_NEW_DASHBOARD`). Accepted truthy values: `true`, `1`, `yes`.
86
156
 
87
157
  ```sh
88
- bunx beacon init
158
+ FEATURE_DARK_MODE=true bun start
159
+ FEATURE_NEW_DASHBOARD=false bun start
160
+ ```
89
161
 
90
- # With a production profile:
91
- bunx beacon init --profile production
162
+ ### Kill Switches
92
163
 
93
- # Custom config path:
94
- bunx beacon init -c ./config/beacon.json -o .env.example.prod
164
+ Kill switches **always take priority** over feature gates. Set them via the `killSwitches` option or the `KILL_<NAME>` env var.
165
+
166
+ ```sh
167
+ KILL_BROKEN_FEATURE=true bun start
168
+ ```
169
+
170
+ ```ts
171
+ beacon.isKilled("brokenFeature"); // true
172
+ beacon.isEnabled("brokenFeature"); // false — killed overrides enabled
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Remote Config
178
+
179
+ Beacon can merge in remote configuration entries from a Nexus backend before local validation. Pass a `Client` from `@joinremba/core` via the `client` option.
180
+
181
+ ```ts
182
+ import { createBeacon } from "@joinremba/beacon";
183
+ import { createClient } from "@joinremba/core";
184
+
185
+ const beacon = createBeacon(
186
+ {
187
+ PORT: { type: "port", default: 3000 },
188
+ },
189
+ {
190
+ client: createClient({ apiKey: "api_core_live_..." }),
191
+ }
192
+ );
193
+
194
+ await beacon.ensure();
195
+ // Remote entries fill gaps not defined in the schema.
196
+ // Schema entries always take priority over remote values.
197
+ // Network failures fall back silently to local-only validation.
95
198
  ```
96
199
 
97
- Output includes types, defaults, descriptions, and secret markers for every variable:
200
+ ---
201
+
202
+ ## CLI Commands
203
+
204
+ Beacon ships with a CLI for development, CI, and ops workflows.
205
+
206
+ | Command | Description |
207
+ |--------------|--------------------------------------------------|
208
+ | `init` | Generate `.env.example` from your config file. |
209
+ | `check` | Validate current environment against schema. |
210
+ | `encrypt` | Encrypt `.env` (AES-256-GCM) for safe committing.|
211
+ | `decrypt` | Decrypt `.env.encrypted` back to plaintext. |
212
+ | `rotate` | Print step-by-step secret rotation checklist. |
213
+ | `drift` | Detect config drift — schema vs. actual env. |
214
+ | `docker` | Validate env in Docker / Kubernetes contexts. |
215
+
216
+ ### `beacon init`
98
217
 
99
218
  ```sh
219
+ bunx beacon init # generate .env.example
220
+ bunx beacon init --profile production # with profile merge
221
+ bunx beacon init --all-profiles # generate for every profile
222
+ bunx beacon init -c ./config/beacon.json -o .env.example.prod
223
+ ```
224
+
225
+ Output includes types, defaults, descriptions, and secret markers:
226
+
227
+ ```
100
228
  # PostgreSQL connection string
101
229
  # Type: url
102
230
  # Required: yes
@@ -110,21 +238,15 @@ PORT=3000
110
238
 
111
239
  ### `beacon check`
112
240
 
113
- Validate your current environment against your schema:
114
-
115
241
  ```sh
116
- bunx beacon check
117
-
118
- # With a specific profile:
119
- bunx beacon check --profile staging
120
-
121
- # Custom config:
242
+ bunx beacon check # validate env
243
+ bunx beacon check --profile staging # with profile
122
244
  bunx beacon check -c ./config/production.json
123
245
  ```
124
246
 
125
- Output is a colour-coded table:
247
+ Colour-coded table output:
126
248
 
127
- ```sh
249
+ ```
128
250
  KEY STATUS VALUE
129
251
  ──────────── ──────── ────────────────────
130
252
  DATABASE_URL pass postgres://localhost...
@@ -134,114 +256,97 @@ Output is a colour-coded table:
134
256
  LOG_LEVEL pass Optional, not set
135
257
  DB_HOST MISSING Not set
136
258
  Did you mean DB_HOSTNAME?
137
-
138
- 2 issue(s), 3 pass
259
+ ✓ 3 pass, 2 issue(s)
139
260
  ```
140
261
 
141
- Exit codes: `0` if all pass, `1` if any issues found.
262
+ Exit codes: `0` if all pass, `1` if any issues.
142
263
 
143
- ### Per-command help
264
+ ### `beacon encrypt` / `beacon decrypt`
144
265
 
145
266
  ```sh
146
- beacon help init
147
- beacon check --help
148
- ```
267
+ # Encrypt .env → .env.encrypted
268
+ BEACON_ENCRYPTION_KEY=your-key beacon encrypt
149
269
 
150
- ---
270
+ # Decrypt back to plaintext
271
+ BEACON_ENCRYPTION_KEY=your-key beacon decrypt
151
272
 
152
- ## API Reference
273
+ # Custom paths
274
+ beacon encrypt -i .env.prod -o .env.prod.encrypted --key "your-key"
275
+ beacon decrypt -i .env.prod.encrypted -o .env.prod --key "your-key"
276
+ ```
153
277
 
154
- ### `createBeacon(schema, options?)`
278
+ Uses **AES-256-GCM** with HKDF-SHA256 key derivation. The key can be passed via `--key` flag or `BEACON_ENCRYPTION_KEY` environment variable.
155
279
 
156
- The default export. Accepts an env schema and optional configuration.
280
+ ### `beacon rotate`
157
281
 
158
- **Parameters**
282
+ Prints a step-by-step checklist for safely rotating secrets (generate → deploy alongside → update consumers → verify → revoke → audit).
159
283
 
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. |
284
+ ```sh
285
+ bunx beacon rotate
286
+ ```
166
287
 
167
- **SchemaEntry** can be either:
288
+ ### `beacon drift`
168
289
 
169
- **1. String-based**Simple type names for everyday use:
290
+ Detects config drift missing required variables, type mismatches, and unexpected enum values.
170
291
 
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`. |
292
+ ```sh
293
+ bunx beacon drift
294
+ bunx beacon drift --profile production
295
+ ```
179
296
 
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()` |
297
+ ### `beacon docker`
191
298
 
192
- **2. Zod schema** Advanced users can pass Zod schemas directly:
299
+ Detects Docker and Kubernetes runtimes, checks common container env vars, and runs a full schema validation.
193
300
 
194
- ```ts
195
- {
196
- PORT: { schema: z.coerce.number().positive().max(9999) },
197
- WHITELIST: { schema: z.string().regex(/^[\d,]+$/) },
198
- }
301
+ ```sh
302
+ bunx beacon docker
303
+ bunx beacon docker --profile staging
199
304
  ```
200
305
 
201
- **Returns**
202
-
203
- A config instance with:
306
+ ---
204
307
 
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>`). |
308
+ ## Profiles
211
309
 
212
- ### TypeScript Types
310
+ Define different schemas per environment using the `profiles` option. The active profile is selected via `profile`.
213
311
 
214
312
  ```ts
215
- import type {
216
- BeaconOptions,
217
- Beacon,
218
- SchemaEntry,
219
- FieldDefinition,
220
- FieldType,
221
- FeatureGate,
222
- ConfigError,
223
- ConfigValidationError,
224
- } from "@joinremba/beacon";
313
+ const beacon = 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
+ staging: {
326
+ DB_HOST: { type: "host", required: true },
327
+ },
328
+ },
329
+ }
330
+ );
225
331
  ```
226
332
 
227
- ### Config file (`.beaconrc.json`)
333
+ Profile entries are **merged into the base schema** — they can add new variables, override types, change defaults, or toggle required flags.
228
334
 
229
- Used by the CLI for `init` and `check` commands:
335
+ You can also use profiles with the CLI:
336
+
337
+ ```sh
338
+ bunx beacon init --profile production
339
+ bunx beacon check --profile staging
340
+ ```
341
+
342
+ ### Config file (`.beaconrc.json`)
230
343
 
231
344
  ```json
232
345
  {
233
346
  "schema": {
234
- "DATABASE_URL": {
235
- "type": "url",
236
- "required": true,
237
- "description": "PostgreSQL connection string"
238
- },
347
+ "DATABASE_URL": { "type": "url", "required": true, "description": "PostgreSQL connection string" },
239
348
  "PORT": { "type": "port", "default": 3000, "description": "HTTP server port" },
240
- "NODE_ENV": {
241
- "type": "enum",
242
- "values": ["development", "production"],
243
- "default": "development"
244
- },
349
+ "NODE_ENV": { "type": "enum", "values": ["development", "production"], "default": "development" },
245
350
  "API_KEY": { "type": "string", "required": true, "secret": true }
246
351
  },
247
352
  "profiles": {
@@ -256,232 +361,131 @@ Used by the CLI for `init` and `check` commands:
256
361
  }
257
362
  ```
258
363
 
259
- ---
260
-
261
- ## Examples
262
-
263
- ### Basic env validation
264
-
265
- ```ts
266
- import { createBeacon } from "@joinremba/beacon";
267
-
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
- });
276
-
277
- config.ensure();
278
- console.log(config.get("PORT"));
279
- ```
280
-
281
- ### With secrets redaction
364
+ The CLI auto-discovers `.beaconrc.json` or `beacon.config.json`.
282
365
 
283
- ```ts
284
- const config = createBeacon({
285
- API_KEY: { type: "string", secret: true },
286
- DATABASE_URL: { type: "url", secret: true },
287
- });
366
+ ---
288
367
 
289
- config.ensure();
290
- // Error messages never show API_KEY or DATABASE_URL values
291
- ```
368
+ ## Error Handling
292
369
 
293
- ### Custom error handling
370
+ All validation errors are **collected and thrown together** as a single `ConfigValidationError` (an `AggregateError` subclass). Each individual issue is a `ConfigError` with the offending key and a human-readable message.
294
371
 
295
372
  ```ts
296
- import { ConfigValidationError } from "@joinremba/beacon";
373
+ import { createBeacon, ConfigValidationError, ConfigError } from "@joinremba/beacon";
297
374
 
298
375
  try {
299
- config.ensure();
376
+ await beacon.ensure();
300
377
  } catch (err) {
301
378
  if (err instanceof ConfigValidationError) {
302
379
  for (const issue of err.errors) {
303
380
  console.error(`[${issue.key}] ${issue.message}`);
381
+ // issue.redacted — true if the value was hidden
304
382
  }
305
383
  }
306
- process.exit(1);
307
384
  }
308
385
  ```
309
386
 
310
- ### Production profile
387
+ ### `strict: false` mode
311
388
 
312
- ```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
- );
328
- ```
329
-
330
- ### Feature Gates
331
-
332
- Toggle features on/off without redeploying. Define gates in the `features` option and check them with `isEnabled()`:
389
+ Pass `{ strict: false }` to `ensure()` to silently skip missing required variables without throwing. Useful in test environments or bootstrap scripts.
333
390
 
334
391
  ```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)
392
+ await beacon.ensure({ strict: false });
393
+ // Missing required vars are skipped instead of throwing
349
394
  ```
350
395
 
351
- **Env overrides** — Any feature can be toggled at runtime via `FEATURE_<NAME>`:
396
+ ### Secret Redaction
352
397
 
353
- ```sh
354
- FEATURE_DARK_MODE=true bun start
355
- ```
398
+ When a field is marked `{ secret: true }`, its value is replaced with `[REDACTED]` in all error messages and CLI output — never leaked into logs or terminal.
356
399
 
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`.
400
+ ---
358
401
 
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. |
402
+ ## Encryption
364
403
 
365
- ### Kill-Switch Flags
404
+ Beacon provides AES-256-GCM encryption for `.env` files via the CLI and programmatic API.
366
405
 
367
- Force-disable a feature at runtime — overrides any feature gate. Define kill switches in the `killSwitches` option or set `KILL_<NAME>` env vars:
406
+ ### Programmatic
368
407
 
369
408
  ```ts
370
- const config = createBeacon(
371
- { DATABASE_URL: { type: "url" } },
372
- {
373
- features: { newDashboard: { enabled: true } },
374
- killSwitches: { newDashboard: true },
375
- }
376
- );
377
-
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"
400
- ```
401
-
402
- ### Secret Rotation Checklist (`beacon rotate`)
409
+ import { encryptEnv, decryptEnv } from "@joinremba/beacon";
403
410
 
404
- Prints a step-by-step checklist for rotating secrets (DB credentials, API keys, etc.):
405
-
406
- ```sh
407
- beacon rotate
411
+ const encrypted = await encryptEnv("DATABASE_URL=postgres://...", "your-key");
412
+ const decrypted = await decryptEnv(encrypted, "your-key");
408
413
  ```
409
414
 
410
- Follows the generate → deploy alongside → update consumers → verify → revoke → audit workflow.
411
-
412
- ### Config Drift Detection (`beacon drift`)
413
-
414
- Detects when your actual environment differs from the schema defined in your config:
415
+ ### CLI
415
416
 
416
417
  ```sh
417
- beacon drift
418
- beacon drift --profile production
418
+ beacon encrypt -i .env -o .env.encrypted --key "your-key"
419
+ beacon decrypt -i .env.encrypted -o .env --key "your-key"
419
420
  ```
420
421
 
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
430
- ```
422
+ The key can also be set via `BEACON_ENCRYPTION_KEY` env var. Key derivation uses HKDF-SHA256 with a 32-byte salt.
431
423
 
432
424
  ---
433
425
 
434
- ## Roadmap
435
-
436
- **MVP** (current)
426
+ ## API Reference
437
427
 
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
428
+ ### `createBeacon(schema, options?)`
445
429
 
446
- **V1**
430
+ | Parameter | Type | Description |
431
+ |-----------|-----------------------------------------|------------------------------------------|
432
+ | `schema` | `Record<string, SchemaEntry>` | Map of env var names to field defs. |
433
+ | `options` | `BeaconOptions` | Optional config (features, profiles, etc.) |
447
434
 
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`)
435
+ #### `BeaconOptions`
455
436
 
456
- **V2**
437
+ | Option | Type | Description |
438
+ |----------------|--------------------------------------------------|--------------------------------------------------|
439
+ | `profile` | `string` | Active profile name. |
440
+ | `profiles` | `Record<string, Record<string, SchemaEntry>>` | Named profile overrides. |
441
+ | `features` | `Record<string, FeatureGate>` | Feature gate definitions. |
442
+ | `killSwitches` | `Record<string, boolean>` | Force-disabled features. |
443
+ | `client` | `Client` (from `@joinremba/core`) | Remote config client. |
457
444
 
458
- - Hosted team secret sync
459
- - Audit trail for config changes
460
- - Deployment provider integrations
461
- - GitHub Actions integration
462
- - Remba Cloud dashboard
445
+ #### `Beacon` (returned by `createBeacon`)
463
446
 
464
- ---
447
+ | Method / Property | Description |
448
+ |--------------------------------|-------------------------------------------------------------------|
449
+ | `ensure(options?)` | Validates all env vars. Throws `ConfigValidationError` on failure. Returns the beacon instance. |
450
+ | `get<T>(key)` | Returns the validated, coerced value. Throws if called before `ensure()`. |
451
+ | `secret` | `Record<string, boolean>` — which keys are marked as secrets. |
452
+ | `isEnabled(feature)` | Checks if a feature gate is enabled. Respects kill switches, env overrides, and rollout. |
453
+ | `isKilled(feature)` | Checks if a feature is force-disabled (kill switch or env). |
465
454
 
466
- ## Related Packages
455
+ #### `EnsureOptions`
467
456
 
468
- - [@joinremba/catalog](https://github.com/joinremba/catalog) Production-ready logging and error event layer built on Pino.
469
- - [@joinremba/gate](https://github.com/joinremba/gate) — API safety layer: validation, responses, idempotency, rate limiting, and API keys.
457
+ | Option | Type | Default | Description |
458
+ |----------|-----------|---------|----------------------------------------------------------|
459
+ | `strict` | `boolean` | `true` | When `false`, missing required vars are skipped silently. |
470
460
 
471
- ## Social Preview
461
+ #### Types
472
462
 
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
463
+ ```ts
464
+ import type {
465
+ Beacon,
466
+ BeaconOptions,
467
+ EnsureOptions,
468
+ SchemaEntry,
469
+ FieldDefinition,
470
+ FieldDefinitionWithSchema,
471
+ FieldType,
472
+ FeatureGate,
473
+ ConfigError,
474
+ ConfigValidationError,
475
+ } from "@joinremba/beacon";
476
+ ```
478
477
 
479
- This will be used whenever your repo link is shared on social media, Slack, or Discord.
478
+ #### Utilities
480
479
 
481
- ## Contributing
480
+ | Export | Description |
481
+ |----------------|--------------------------------------------------|
482
+ | `ConfigError` | Individual config validation error (`key`, `message`, `redacted`). |
483
+ | `ConfigValidationError` | `AggregateError` subclass containing all `ConfigError` instances. |
484
+ | `encryptEnv` | `(content: string, key: string) => Promise<string>` — AES-256-GCM encrypt. |
485
+ | `decryptEnv` | `(encrypted: string, key: string) => Promise<string>` — AES-256-GCM decrypt. |
482
486
 
483
- Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, development process, and how to submit pull requests.
487
+ ---
484
488
 
485
489
  ## License
486
490
 
487
- MIT &mdash; see [LICENSE](LICENSE).
491
+ 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.0",
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/index.ts CHANGED
@@ -186,6 +186,13 @@ export function createBeacon(
186
186
  return map;
187
187
  },
188
188
 
189
+ getAll(): Record<string, unknown> {
190
+ if (validated === null) {
191
+ throw new ConfigError("", "Call beacon.ensure() before accessing config values");
192
+ }
193
+ return { ...validated };
194
+ },
195
+
189
196
  isKilled(feature: string): boolean {
190
197
  const envName = `KILL_${feature
191
198
  .replace(/([a-z])([A-Z])/g, "$1_$2")
package/src/types.ts CHANGED
@@ -53,6 +53,9 @@ export interface EnsureOptions {
53
53
  export interface Beacon {
54
54
  ensure(options?: EnsureOptions): Promise<Beacon>;
55
55
  get<T = unknown>(key: string): T;
56
+ /** Returns all validated entries as a flat key-value record.
57
+ * Secrets are included — callers should redact as needed. */
58
+ getAll(): Record<string, unknown>;
56
59
  readonly secret: Record<string, boolean>;
57
60
  isEnabled(feature: string): boolean;
58
61
  isKilled(feature: string): boolean;