@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 +282 -321
- package/package.json +1 -1
- package/src/cli.ts +42 -7
- package/src/index.ts +7 -0
- package/src/types.ts +2 -1
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="
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
68
|
+
## Schema Types
|
|
54
69
|
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
```ts
|
|
99
|
+
import { z } from "zod";
|
|
67
100
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
##
|
|
109
|
+
## `.ensure()`
|
|
80
110
|
|
|
81
|
-
|
|
111
|
+
Validates every variable in the schema against the current `process.env`. Must be called before any `get()` call.
|
|
82
112
|
|
|
83
|
-
|
|
113
|
+
```ts
|
|
114
|
+
await config.ensure();
|
|
115
|
+
```
|
|
84
116
|
|
|
85
|
-
|
|
117
|
+
**Behavior:**
|
|
86
118
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
```ts
|
|
131
|
+
import { ConfigValidationError } from "@joinremba/beacon";
|
|
98
132
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
145
|
+
The `ensure({ strict: false })` mode is useful during testing or bootstrap when some env vars may not yet be configured.
|
|
112
146
|
|
|
113
|
-
|
|
147
|
+
---
|
|
114
148
|
|
|
115
|
-
|
|
116
|
-
bunx beacon check
|
|
149
|
+
## `.getAll()`
|
|
117
150
|
|
|
118
|
-
|
|
119
|
-
bunx beacon check --profile staging
|
|
151
|
+
Returns a `Record<string, unknown>` of **all** resolved values after `ensure()` completes.
|
|
120
152
|
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
159
|
+
Useful for debugging, logging startup configuration, or passing the full config object to subsystems.
|
|
126
160
|
|
|
127
|
-
|
|
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
|
-
|
|
163
|
+
## `.get<T>(key)`
|
|
142
164
|
|
|
143
|
-
|
|
165
|
+
Type-safe accessor for individual validated values. Call `.get<T>(key)` with an optional generic type parameter.
|
|
144
166
|
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
##
|
|
180
|
+
## Secrets
|
|
153
181
|
|
|
154
|
-
|
|
182
|
+
Mark sensitive fields with `secret: true` to prevent their values from appearing in error messages, CLI output, or logs.
|
|
155
183
|
|
|
156
|
-
|
|
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
|
-
|
|
191
|
+
When a secret value fails validation, the error message shows `[REDACTED]` instead of the actual value:
|
|
159
192
|
|
|
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. |
|
|
193
|
+
```
|
|
194
|
+
Environment variable API_KEY=[REDACTED]: Invalid value
|
|
195
|
+
```
|
|
166
196
|
|
|
167
|
-
|
|
197
|
+
The secret metadata is also accessible at runtime:
|
|
168
198
|
|
|
169
|
-
|
|
199
|
+
```ts
|
|
200
|
+
config.secret; // { API_KEY: true, DATABASE_URL: true }
|
|
201
|
+
```
|
|
170
202
|
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
**
|
|
219
|
+
**Defaults behavior:**
|
|
202
220
|
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
+
## `.beaconrc.json` (CLI Config File)
|
|
228
230
|
|
|
229
|
-
|
|
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": {
|
|
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": {
|
|
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
|
-
|
|
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
|
-
|
|
281
|
+
# Validate current environment against schema
|
|
282
|
+
bunx beacon check
|
|
283
|
+
bunx beacon check --profile staging
|
|
264
284
|
|
|
265
|
-
|
|
266
|
-
|
|
285
|
+
# Detect config drift (missing vars, type mismatches)
|
|
286
|
+
bunx beacon drift
|
|
287
|
+
bunx beacon drift --profile production
|
|
267
288
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
278
|
-
|
|
296
|
+
# Print secret rotation checklist
|
|
297
|
+
bunx beacon rotate
|
|
279
298
|
```
|
|
280
299
|
|
|
281
|
-
|
|
300
|
+
---
|
|
282
301
|
|
|
283
|
-
|
|
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
|
-
|
|
290
|
-
// Error messages never show API_KEY or DATABASE_URL values
|
|
291
|
-
```
|
|
304
|
+
### `createBeacon(schema, options?)`
|
|
292
305
|
|
|
293
|
-
|
|
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
|
-
|
|
296
|
-
import { ConfigValidationError } from "@joinremba/beacon";
|
|
362
|
+
---
|
|
297
363
|
|
|
298
|
-
|
|
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
|
-
|
|
366
|
+
Beacon ships with strict TypeScript types. All public APIs are fully typed.
|
|
311
367
|
|
|
312
368
|
```ts
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
{
|
|
338
|
-
|
|
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
|
-
|
|
391
|
+
await config.ensure();
|
|
352
392
|
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
+
## Integration with `@joinremba/core`
|
|
366
400
|
|
|
367
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
413
|
+
LOCAL_VAR: { type: "string", default: "local-fallback" },
|
|
414
|
+
PORT: { type: "port", default: 3000 },
|
|
415
|
+
},
|
|
416
|
+
{ client }
|
|
376
417
|
);
|
|
377
418
|
|
|
378
|
-
config.
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
448
|
+
MIT — see [LICENSE](LICENSE).
|
package/package.json
CHANGED
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
}
|