@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 +309 -305
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/types.ts +3 -0
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="
|
|
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
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
NODE_ENV:
|
|
36
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
const dbUrl =
|
|
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
|
|
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
|
-
##
|
|
75
|
+
## Schema Field Types
|
|
54
76
|
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
### Custom Zod Schemas
|
|
105
|
+
|
|
106
|
+
For advanced validation, pass a Zod schema directly:
|
|
67
107
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
##
|
|
119
|
+
## Feature Gates
|
|
80
120
|
|
|
81
|
-
|
|
121
|
+
Toggle features on and off without redeploying. Define gates in the `features` option and check them with `isEnabled()`.
|
|
82
122
|
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
FEATURE_DARK_MODE=true bun start
|
|
159
|
+
FEATURE_NEW_DASHBOARD=false bun start
|
|
160
|
+
```
|
|
89
161
|
|
|
90
|
-
|
|
91
|
-
bunx beacon init --profile production
|
|
162
|
+
### Kill Switches
|
|
92
163
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
+
Colour-coded table output:
|
|
126
248
|
|
|
127
|
-
```
|
|
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
|
|
262
|
+
Exit codes: `0` if all pass, `1` if any issues.
|
|
142
263
|
|
|
143
|
-
###
|
|
264
|
+
### `beacon encrypt` / `beacon decrypt`
|
|
144
265
|
|
|
145
266
|
```sh
|
|
146
|
-
|
|
147
|
-
beacon
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
+
### `beacon rotate`
|
|
157
281
|
|
|
158
|
-
|
|
282
|
+
Prints a step-by-step checklist for safely rotating secrets (generate → deploy alongside → update consumers → verify → revoke → audit).
|
|
159
283
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
288
|
+
### `beacon drift`
|
|
168
289
|
|
|
169
|
-
|
|
290
|
+
Detects config drift — missing required variables, type mismatches, and unexpected enum values.
|
|
170
291
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
+
Detects Docker and Kubernetes runtimes, checks common container env vars, and runs a full schema validation.
|
|
193
300
|
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
A config instance with:
|
|
306
|
+
---
|
|
204
307
|
|
|
205
|
-
|
|
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
|
-
|
|
310
|
+
Define different schemas per environment using the `profiles` option. The active profile is selected via `profile`.
|
|
213
311
|
|
|
214
312
|
```ts
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
const config = createBeacon({
|
|
285
|
-
API_KEY: { type: "string", secret: true },
|
|
286
|
-
DATABASE_URL: { type: "url", secret: true },
|
|
287
|
-
});
|
|
366
|
+
---
|
|
288
367
|
|
|
289
|
-
|
|
290
|
-
// Error messages never show API_KEY or DATABASE_URL values
|
|
291
|
-
```
|
|
368
|
+
## Error Handling
|
|
292
369
|
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
387
|
+
### `strict: false` mode
|
|
311
388
|
|
|
312
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
396
|
+
### Secret Redaction
|
|
352
397
|
|
|
353
|
-
|
|
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
|
-
|
|
400
|
+
---
|
|
358
401
|
|
|
359
|
-
|
|
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
|
-
|
|
404
|
+
Beacon provides AES-256-GCM encryption for `.env` files via the CLI and programmatic API.
|
|
366
405
|
|
|
367
|
-
|
|
406
|
+
### Programmatic
|
|
368
407
|
|
|
369
408
|
```ts
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
418
|
-
beacon
|
|
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
|
-
|
|
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
|
-
##
|
|
435
|
-
|
|
436
|
-
**MVP** (current)
|
|
426
|
+
## API Reference
|
|
437
427
|
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
455
|
+
#### `EnsureOptions`
|
|
467
456
|
|
|
468
|
-
|
|
469
|
-
|
|
457
|
+
| Option | Type | Default | Description |
|
|
458
|
+
|----------|-----------|---------|----------------------------------------------------------|
|
|
459
|
+
| `strict` | `boolean` | `true` | When `false`, missing required vars are skipped silently. |
|
|
470
460
|
|
|
471
|
-
|
|
461
|
+
#### Types
|
|
472
462
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
478
|
+
#### Utilities
|
|
480
479
|
|
|
481
|
-
|
|
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
|
-
|
|
487
|
+
---
|
|
484
488
|
|
|
485
489
|
## License
|
|
486
490
|
|
|
487
|
-
MIT
|
|
491
|
+
MIT — see [LICENSE](LICENSE).
|
package/package.json
CHANGED
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;
|