@joinremba/beacon 0.5.0 → 0.5.2
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 +280 -323
- package/package.json +2 -2
- package/src/cli-config.ts +8 -3
- package/src/cli.ts +43 -10
- package/src/index.ts +8 -7
- package/src/schema.ts +3 -0
- package/src/types.ts +1 -3
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
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
15
|
<a href="LICENSE"><img src="https://img.shields.io/npm/l/@joinremba/beacon.svg" alt="License"></a>
|
|
@@ -14,32 +18,19 @@
|
|
|
14
18
|
<img src="https://img.shields.io/badge/TypeScript-6-blue" alt="TypeScript">
|
|
15
19
|
</p>
|
|
16
20
|
|
|
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
|
-
|
|
19
|
-
```sh
|
|
20
|
-
bun add @joinremba/beacon
|
|
21
|
-
bunx beacon init
|
|
22
|
-
bunx beacon check
|
|
23
|
-
```
|
|
24
|
-
|
|
25
21
|
---
|
|
26
22
|
|
|
27
23
|
## Features
|
|
28
24
|
|
|
29
|
-
- **Schema-based validation**
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
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`
|
|
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.
|
|
43
34
|
|
|
44
35
|
---
|
|
45
36
|
|
|
@@ -56,302 +47,220 @@ bun add @joinremba/beacon
|
|
|
56
47
|
```ts
|
|
57
48
|
import { createBeacon } from "@joinremba/beacon";
|
|
58
49
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
const config = createBeacon({
|
|
51
|
+
DATABASE_URL: { type: "url", required: true },
|
|
52
|
+
REDIS_URL: { type: "url", required: true },
|
|
53
|
+
PORT: { type: "port", default: 3000 },
|
|
54
|
+
NODE_ENV: { type: "enum", values: ["development", "production"], default: "development" },
|
|
55
|
+
API_KEY: { type: "string", required: true, secret: true },
|
|
64
56
|
});
|
|
65
57
|
|
|
66
|
-
await
|
|
67
|
-
|
|
68
|
-
const dbUrl =
|
|
58
|
+
await config.ensure();
|
|
59
|
+
|
|
60
|
+
const dbUrl = config.get<string>("DATABASE_URL");
|
|
61
|
+
const port = config.get<number>("PORT");
|
|
69
62
|
```
|
|
70
63
|
|
|
71
|
-
If any variable is missing or invalid, `ensure()` throws a `ConfigValidationError` with **all issues
|
|
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.
|
|
72
65
|
|
|
73
66
|
---
|
|
74
67
|
|
|
75
|
-
## Schema
|
|
68
|
+
## Schema Types
|
|
76
69
|
|
|
77
|
-
|
|
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()` |
|
|
70
|
+
Each field in the schema can be defined with a `type` string. Beacon coerces and validates the raw `process.env` value accordingly.
|
|
88
71
|
|
|
89
|
-
|
|
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" }` |
|
|
90
83
|
|
|
91
84
|
```ts
|
|
92
|
-
|
|
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
|
+
});
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
|
|
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`. |
|
|
103
|
-
|
|
104
|
-
### Custom Zod Schemas
|
|
105
|
-
|
|
106
|
-
For advanced validation, pass a Zod schema directly:
|
|
96
|
+
You can also pass raw **Zod schemas** directly for custom validation:
|
|
107
97
|
|
|
108
98
|
```ts
|
|
109
99
|
import { z } from "zod";
|
|
110
100
|
|
|
111
|
-
const
|
|
112
|
-
PORT: { schema: z.coerce.number().positive().max(9999) },
|
|
101
|
+
const config = createBeacon({
|
|
113
102
|
WHITELIST: { schema: z.string().regex(/^[\d,]+$/) },
|
|
103
|
+
TIMEOUT: { schema: z.coerce.number().positive().max(30000) },
|
|
114
104
|
});
|
|
115
105
|
```
|
|
116
106
|
|
|
117
107
|
---
|
|
118
108
|
|
|
119
|
-
##
|
|
109
|
+
## `.ensure()`
|
|
120
110
|
|
|
121
|
-
|
|
111
|
+
Validates every variable in the schema against the current `process.env`. Must be called before any `get()` call.
|
|
122
112
|
|
|
123
113
|
```ts
|
|
124
|
-
|
|
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
|
|
114
|
+
await config.ensure();
|
|
143
115
|
```
|
|
144
116
|
|
|
145
|
-
|
|
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. |
|
|
117
|
+
**Behavior:**
|
|
152
118
|
|
|
153
|
-
|
|
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 |
|
|
154
127
|
|
|
155
|
-
|
|
128
|
+
Errors are aggregated into a single `ConfigValidationError` (extends `AggregateError`). Access individual issues via `err.errors`:
|
|
156
129
|
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
FEATURE_NEW_DASHBOARD=false bun start
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Kill Switches
|
|
163
|
-
|
|
164
|
-
Kill switches **always take priority** over feature gates. Set them via the `killSwitches` option or the `KILL_<NAME>` env var.
|
|
130
|
+
```ts
|
|
131
|
+
import { ConfigValidationError } from "@joinremba/beacon";
|
|
165
132
|
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
}
|
|
168
143
|
```
|
|
169
144
|
|
|
170
|
-
|
|
171
|
-
beacon.isKilled("brokenFeature"); // true
|
|
172
|
-
beacon.isEnabled("brokenFeature"); // false — killed overrides enabled
|
|
173
|
-
```
|
|
145
|
+
The `ensure({ strict: false })` mode is useful during testing or bootstrap when some env vars may not yet be configured.
|
|
174
146
|
|
|
175
147
|
---
|
|
176
148
|
|
|
177
|
-
##
|
|
149
|
+
## `.getAll()`
|
|
178
150
|
|
|
179
|
-
|
|
151
|
+
Returns a `Record<string, unknown>` of **all** resolved values after `ensure()` completes.
|
|
180
152
|
|
|
181
153
|
```ts
|
|
182
|
-
|
|
183
|
-
|
|
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.
|
|
154
|
+
await config.ensure();
|
|
155
|
+
const all = config.getAll();
|
|
156
|
+
// { DATABASE_URL: "postgres://...", PORT: 3000, NODE_ENV: "development", ... }
|
|
198
157
|
```
|
|
199
158
|
|
|
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`
|
|
217
|
-
|
|
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
|
-
```
|
|
159
|
+
Useful for debugging, logging startup configuration, or passing the full config object to subsystems.
|
|
224
160
|
|
|
225
|
-
|
|
161
|
+
---
|
|
226
162
|
|
|
227
|
-
|
|
228
|
-
# PostgreSQL connection string
|
|
229
|
-
# Type: url
|
|
230
|
-
# Required: yes
|
|
231
|
-
# DATABASE_URL=
|
|
232
|
-
|
|
233
|
-
# HTTP server port
|
|
234
|
-
# Type: port
|
|
235
|
-
# Default: 3000
|
|
236
|
-
PORT=3000
|
|
237
|
-
```
|
|
163
|
+
## `.get<T>(key)`
|
|
238
164
|
|
|
239
|
-
|
|
165
|
+
Type-safe accessor for individual validated values. Call `.get<T>(key)` with an optional generic type parameter.
|
|
240
166
|
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
245
171
|
```
|
|
246
172
|
|
|
247
|
-
|
|
173
|
+
Throws `ConfigError` if:
|
|
248
174
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
──────────── ──────── ────────────────────
|
|
252
|
-
DATABASE_URL pass postgres://localhost...
|
|
253
|
-
PORT pass Using default value
|
|
254
|
-
NODE_ENV pass Using default value
|
|
255
|
-
API_KEY MISSING Not set
|
|
256
|
-
LOG_LEVEL pass Optional, not set
|
|
257
|
-
DB_HOST MISSING Not set
|
|
258
|
-
Did you mean DB_HOSTNAME?
|
|
259
|
-
✓ 3 pass, 2 issue(s)
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Exit codes: `0` if all pass, `1` if any issues.
|
|
175
|
+
- Called before `ensure()` — `"Call beacon.ensure() before accessing config values"`
|
|
176
|
+
- Key doesn't exist in the schema — `"Unknown config key: <key>"`
|
|
263
177
|
|
|
264
|
-
|
|
178
|
+
---
|
|
265
179
|
|
|
266
|
-
|
|
267
|
-
# Encrypt .env → .env.encrypted
|
|
268
|
-
BEACON_ENCRYPTION_KEY=your-key beacon encrypt
|
|
180
|
+
## Secrets
|
|
269
181
|
|
|
270
|
-
|
|
271
|
-
BEACON_ENCRYPTION_KEY=your-key beacon decrypt
|
|
182
|
+
Mark sensitive fields with `secret: true` to prevent their values from appearing in error messages, CLI output, or logs.
|
|
272
183
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
184
|
+
```ts
|
|
185
|
+
const config = createBeacon({
|
|
186
|
+
API_KEY: { type: "string", required: true, secret: true },
|
|
187
|
+
DATABASE_URL: { type: "url", secret: true },
|
|
188
|
+
});
|
|
276
189
|
```
|
|
277
190
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
### `beacon rotate`
|
|
191
|
+
When a secret value fails validation, the error message shows `[REDACTED]` instead of the actual value:
|
|
281
192
|
|
|
282
|
-
Prints a step-by-step checklist for safely rotating secrets (generate → deploy alongside → update consumers → verify → revoke → audit).
|
|
283
|
-
|
|
284
|
-
```sh
|
|
285
|
-
bunx beacon rotate
|
|
286
193
|
```
|
|
287
|
-
|
|
288
|
-
### `beacon drift`
|
|
289
|
-
|
|
290
|
-
Detects config drift — missing required variables, type mismatches, and unexpected enum values.
|
|
291
|
-
|
|
292
|
-
```sh
|
|
293
|
-
bunx beacon drift
|
|
294
|
-
bunx beacon drift --profile production
|
|
194
|
+
Environment variable API_KEY=[REDACTED]: Invalid value
|
|
295
195
|
```
|
|
296
196
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
Detects Docker and Kubernetes runtimes, checks common container env vars, and runs a full schema validation.
|
|
197
|
+
The secret metadata is also accessible at runtime:
|
|
300
198
|
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
bunx beacon docker --profile staging
|
|
199
|
+
```ts
|
|
200
|
+
config.secret; // { API_KEY: true, DATABASE_URL: true }
|
|
304
201
|
```
|
|
305
202
|
|
|
306
203
|
---
|
|
307
204
|
|
|
308
|
-
##
|
|
205
|
+
## NODE_ENV Handling
|
|
309
206
|
|
|
310
|
-
|
|
207
|
+
Beacon handles `NODE_ENV` like any other env var — define it in your schema with an `enum` type and a default:
|
|
311
208
|
|
|
312
209
|
```ts
|
|
313
|
-
const
|
|
314
|
-
{
|
|
315
|
-
|
|
316
|
-
|
|
210
|
+
const config = createBeacon({
|
|
211
|
+
NODE_ENV: {
|
|
212
|
+
type: "enum",
|
|
213
|
+
values: ["development", "test", "staging", "production"],
|
|
214
|
+
default: "development",
|
|
317
215
|
},
|
|
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
|
-
);
|
|
216
|
+
});
|
|
331
217
|
```
|
|
332
218
|
|
|
333
|
-
|
|
219
|
+
**Defaults behavior:**
|
|
334
220
|
|
|
335
|
-
|
|
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.
|
|
336
224
|
|
|
337
|
-
|
|
338
|
-
bunx beacon init --profile production
|
|
339
|
-
bunx beacon check --profile staging
|
|
340
|
-
```
|
|
225
|
+
This means you can safely deploy with minimal env vars during development as long as sensible defaults are provided.
|
|
341
226
|
|
|
342
|
-
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## `.beaconrc.json` (CLI Config File)
|
|
230
|
+
|
|
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.
|
|
343
232
|
|
|
344
233
|
```json
|
|
345
234
|
{
|
|
346
235
|
"schema": {
|
|
347
|
-
"DATABASE_URL": {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
236
|
+
"DATABASE_URL": {
|
|
237
|
+
"type": "url",
|
|
238
|
+
"required": true,
|
|
239
|
+
"description": "PostgreSQL connection string"
|
|
240
|
+
},
|
|
241
|
+
"PORT": {
|
|
242
|
+
"type": "port",
|
|
243
|
+
"default": 3000,
|
|
244
|
+
"description": "HTTP server port"
|
|
245
|
+
},
|
|
246
|
+
"NODE_ENV": {
|
|
247
|
+
"type": "enum",
|
|
248
|
+
"values": ["development", "staging", "production"],
|
|
249
|
+
"default": "development"
|
|
250
|
+
},
|
|
251
|
+
"API_KEY": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"required": true,
|
|
254
|
+
"secret": true,
|
|
255
|
+
"description": "API signing key"
|
|
256
|
+
}
|
|
351
257
|
},
|
|
352
258
|
"profiles": {
|
|
353
259
|
"production": {
|
|
354
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" }
|
|
355
264
|
}
|
|
356
265
|
},
|
|
357
266
|
"features": {
|
|
@@ -361,128 +270,176 @@ bunx beacon check --profile staging
|
|
|
361
270
|
}
|
|
362
271
|
```
|
|
363
272
|
|
|
364
|
-
|
|
273
|
+
**CLI commands:**
|
|
365
274
|
|
|
366
|
-
|
|
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
|
|
367
280
|
|
|
368
|
-
|
|
281
|
+
# Validate current environment against schema
|
|
282
|
+
bunx beacon check
|
|
283
|
+
bunx beacon check --profile staging
|
|
369
284
|
|
|
370
|
-
|
|
285
|
+
# Detect config drift (missing vars, type mismatches)
|
|
286
|
+
bunx beacon drift
|
|
287
|
+
bunx beacon drift --profile production
|
|
371
288
|
|
|
372
|
-
|
|
373
|
-
|
|
289
|
+
# Encrypt/decrypt .env files for safe committing
|
|
290
|
+
BEACON_ENCRYPTION_KEY=... beacon encrypt
|
|
291
|
+
BEACON_ENCRYPTION_KEY=... beacon decrypt
|
|
374
292
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
console.error(`[${issue.key}] ${issue.message}`);
|
|
381
|
-
// issue.redacted — true if the value was hidden
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
293
|
+
# Validate env in Docker/Kubernetes
|
|
294
|
+
bunx beacon docker
|
|
295
|
+
|
|
296
|
+
# Print secret rotation checklist
|
|
297
|
+
bunx beacon rotate
|
|
385
298
|
```
|
|
386
299
|
|
|
387
|
-
|
|
300
|
+
---
|
|
388
301
|
|
|
389
|
-
|
|
302
|
+
## Configuration Reference
|
|
390
303
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
304
|
+
### `createBeacon(schema, options?)`
|
|
305
|
+
|
|
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. |
|
|
395
353
|
|
|
396
|
-
###
|
|
354
|
+
### `FeatureGate`
|
|
397
355
|
|
|
398
|
-
|
|
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. |
|
|
399
361
|
|
|
400
362
|
---
|
|
401
363
|
|
|
402
|
-
##
|
|
403
|
-
|
|
404
|
-
Beacon provides AES-256-GCM encryption for `.env` files via the CLI and programmatic API.
|
|
364
|
+
## TypeScript
|
|
405
365
|
|
|
406
|
-
|
|
366
|
+
Beacon ships with strict TypeScript types. All public APIs are fully typed.
|
|
407
367
|
|
|
408
368
|
```ts
|
|
409
|
-
import {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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";
|
|
413
381
|
```
|
|
414
382
|
|
|
415
|
-
|
|
383
|
+
**Generic inference with `.get<T>()`:**
|
|
416
384
|
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
385
|
+
```ts
|
|
386
|
+
const config = createBeacon({
|
|
387
|
+
PORT: { type: "port", default: 3000 },
|
|
388
|
+
DEBUG: { type: "boolean", default: false },
|
|
389
|
+
});
|
|
421
390
|
|
|
422
|
-
|
|
391
|
+
await config.ensure();
|
|
392
|
+
|
|
393
|
+
const port = config.get<number>("PORT"); // inferred as number
|
|
394
|
+
const debug = config.get<boolean>("DEBUG"); // inferred as boolean
|
|
395
|
+
```
|
|
423
396
|
|
|
424
397
|
---
|
|
425
398
|
|
|
426
|
-
##
|
|
399
|
+
## Integration with `@joinremba/core`
|
|
427
400
|
|
|
428
|
-
|
|
401
|
+
Beacon can fetch remote configuration from the Remba cloud by passing a `Client` instance from `@joinremba/core`.
|
|
429
402
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
| `options` | `BeaconOptions` | Optional config (features, profiles, etc.) |
|
|
434
|
-
|
|
435
|
-
#### `BeaconOptions`
|
|
403
|
+
```ts
|
|
404
|
+
import { createBeacon } from "@joinremba/beacon";
|
|
405
|
+
import { createClient } from "@joinremba/core";
|
|
436
406
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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. |
|
|
407
|
+
const client = createClient({
|
|
408
|
+
apiKey: "api_core_live_abc123",
|
|
409
|
+
});
|
|
444
410
|
|
|
445
|
-
|
|
411
|
+
const config = createBeacon(
|
|
412
|
+
{
|
|
413
|
+
LOCAL_VAR: { type: "string", default: "local-fallback" },
|
|
414
|
+
PORT: { type: "port", default: 3000 },
|
|
415
|
+
},
|
|
416
|
+
{ client }
|
|
417
|
+
);
|
|
446
418
|
|
|
447
|
-
|
|
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). |
|
|
419
|
+
await config.ensure();
|
|
420
|
+
```
|
|
454
421
|
|
|
455
|
-
|
|
422
|
+
**How remote config merging works:**
|
|
456
423
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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.
|
|
460
428
|
|
|
461
|
-
|
|
429
|
+
This gives you a layered config priority:
|
|
462
430
|
|
|
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
431
|
```
|
|
432
|
+
process.env > schema defaults > remote config
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Use this to share non-sensitive config (feature flags, service URLs) across deployments without storing everything in every CI/CD pipeline.
|
|
436
|
+
|
|
437
|
+
---
|
|
477
438
|
|
|
478
|
-
|
|
439
|
+
## Related Packages
|
|
479
440
|
|
|
480
|
-
|
|
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. |
|
|
441
|
+
- [@joinremba/catalog](https://github.com/joinremba/catalog) — Production-ready logging and error event layer on Pino.
|
|
442
|
+
- [@joinremba/gate](https://github.com/joinremba/gate) — API safety layer: validation, responses, idempotency, rate limiting, and API keys.
|
|
486
443
|
|
|
487
444
|
---
|
|
488
445
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joinremba/beacon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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",
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
"bun": ">=1.3.1"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@joinremba/core": "^0.4.0",
|
|
72
71
|
"zod": "^4.4.2"
|
|
73
72
|
},
|
|
74
73
|
"devDependencies": {
|
|
74
|
+
"@joinremba/core": "^0.4.0",
|
|
75
75
|
"@types/bun": "latest",
|
|
76
76
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
77
77
|
"@typescript-eslint/parser": "^7.18.0",
|
package/src/cli-config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
1
2
|
import type { FeatureGate, SchemaEntry } from "./types";
|
|
2
3
|
import { typeToSchema, type SchemaField } from "./schema";
|
|
3
4
|
|
|
@@ -147,9 +148,10 @@ export async function runCheck(
|
|
|
147
148
|
const field = entry as { required?: boolean; default?: unknown };
|
|
148
149
|
required = field.required !== false;
|
|
149
150
|
hasDefault = field.default !== undefined;
|
|
150
|
-
} else {
|
|
151
|
-
const f = entry as { required?: boolean };
|
|
151
|
+
} else if ("schema" in entry) {
|
|
152
|
+
const f = entry as { required?: boolean; schema: z.ZodType<unknown> };
|
|
152
153
|
required = f.required !== false;
|
|
154
|
+
hasDefault = f.schema.safeParse(undefined).success;
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
if (raw === undefined || raw === "") {
|
|
@@ -164,13 +166,16 @@ export async function runCheck(
|
|
|
164
166
|
continue;
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
const isSecret =
|
|
169
|
+
const isSecret = (entry as { secret?: boolean }).secret ?? false;
|
|
168
170
|
const display = isSecret ? SECRET_CENSOR : raw;
|
|
169
171
|
|
|
170
172
|
try {
|
|
171
173
|
if (isField) {
|
|
172
174
|
const schema = typeToSchema(entry as SchemaField);
|
|
173
175
|
schema.parse(raw);
|
|
176
|
+
} else if ("schema" in entry) {
|
|
177
|
+
const f = entry as { schema: z.ZodType<unknown> };
|
|
178
|
+
f.schema.parse(raw);
|
|
174
179
|
}
|
|
175
180
|
results.push({
|
|
176
181
|
key,
|
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
|
|
|
@@ -170,9 +171,7 @@ async function handleInit(args: ParsedArgs) {
|
|
|
170
171
|
: [args.profile];
|
|
171
172
|
|
|
172
173
|
for (const profile of profilesToGenerate) {
|
|
173
|
-
const output = profile
|
|
174
|
-
? args.output || `.env.example.${profile}`
|
|
175
|
-
: args.output || ".env.example";
|
|
174
|
+
const output = profile ? `.env.example.${profile}` : args.output || ".env.example";
|
|
176
175
|
const example = generateEnvExample(config, profile);
|
|
177
176
|
await Bun.write(output, example);
|
|
178
177
|
console.log(` ${icon.pass} Generated ${color.bold(output)}`);
|
|
@@ -206,6 +205,29 @@ async function handleCheck(args: ParsedArgs) {
|
|
|
206
205
|
|
|
207
206
|
process.stdout.write(formatCheckResult(result.results));
|
|
208
207
|
|
|
208
|
+
if (config.features) {
|
|
209
|
+
const featureEntries = Object.entries(config.features);
|
|
210
|
+
if (featureEntries.length > 0) {
|
|
211
|
+
process.stdout.write(`\n ${color.bold("Feature Gates")}\n\n`);
|
|
212
|
+
for (const [name, gate] of featureEntries) {
|
|
213
|
+
const envName = `FEATURE_${name
|
|
214
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
215
|
+
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
216
|
+
.toUpperCase()}`;
|
|
217
|
+
const envVal = process.env[envName];
|
|
218
|
+
const enabled =
|
|
219
|
+
envVal !== undefined && envVal !== ""
|
|
220
|
+
? envVal === "true" || envVal === "1" || envVal === "yes"
|
|
221
|
+
: gate.enabled;
|
|
222
|
+
const icon_ = enabled ? icon.pass : icon.fail;
|
|
223
|
+
const status = enabled ? color.green("enabled") : color.red("disabled");
|
|
224
|
+
const desc = gate.description ? ` ${color.dim(`— ${gate.description}`)}` : "";
|
|
225
|
+
process.stdout.write(` ${icon_} ${color.bold(name)}: ${status}${desc}\n`);
|
|
226
|
+
}
|
|
227
|
+
process.stdout.write("\n");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
209
231
|
if (result.errors.length > 0) {
|
|
210
232
|
for (const err of result.errors) {
|
|
211
233
|
const suggestions = suggestKeys(
|
|
@@ -343,13 +365,24 @@ async function handleDrift(args: ParsedArgs) {
|
|
|
343
365
|
}
|
|
344
366
|
|
|
345
367
|
if (isField) {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
368
|
+
const schema = typeToSchema(entry as SchemaField);
|
|
369
|
+
const result = schema.safeParse(raw);
|
|
370
|
+
if (!result.success) {
|
|
371
|
+
const field = entry as { type?: string; values?: readonly string[] };
|
|
372
|
+
if (field.type === "enum" && field.values) {
|
|
373
|
+
driftResults.push({
|
|
374
|
+
key,
|
|
375
|
+
expected: `one of: ${field.values.join(" | ")}`,
|
|
376
|
+
actual: raw,
|
|
377
|
+
});
|
|
378
|
+
} else {
|
|
379
|
+
const issue = result.error.issues[0];
|
|
380
|
+
driftResults.push({
|
|
381
|
+
key,
|
|
382
|
+
expected: issue?.message ?? "valid value",
|
|
383
|
+
actual: raw,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
353
386
|
}
|
|
354
387
|
}
|
|
355
388
|
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { typeToSchema } from "./schema";
|
|
|
14
14
|
|
|
15
15
|
export type {
|
|
16
16
|
BeaconOptions,
|
|
17
|
+
EnsureOptions,
|
|
17
18
|
FeatureGate,
|
|
18
19
|
FieldDefinition,
|
|
19
20
|
FieldDefinitionWithSchema,
|
|
@@ -178,6 +179,13 @@ export function createBeacon(
|
|
|
178
179
|
return validated[key] as T;
|
|
179
180
|
},
|
|
180
181
|
|
|
182
|
+
getAll(): Record<string, unknown> {
|
|
183
|
+
if (validated === null) {
|
|
184
|
+
throw new ConfigError("beacon", "Call beacon.ensure() before accessing config values");
|
|
185
|
+
}
|
|
186
|
+
return { ...validated };
|
|
187
|
+
},
|
|
188
|
+
|
|
181
189
|
get secret(): Record<string, boolean> {
|
|
182
190
|
const map: Record<string, boolean> = {};
|
|
183
191
|
for (const key of secretKeys) {
|
|
@@ -186,13 +194,6 @@ export function createBeacon(
|
|
|
186
194
|
return map;
|
|
187
195
|
},
|
|
188
196
|
|
|
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
|
-
|
|
196
197
|
isKilled(feature: string): boolean {
|
|
197
198
|
const envName = `KILL_${feature
|
|
198
199
|
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
package/src/schema.ts
CHANGED
|
@@ -27,6 +27,9 @@ export function typeToSchema(field: SchemaField): z.ZodType<unknown> {
|
|
|
27
27
|
case "boolean":
|
|
28
28
|
base = z
|
|
29
29
|
.string()
|
|
30
|
+
.refine((v) => ["true", "false", "1", "0", "yes", "no"].includes(v), {
|
|
31
|
+
message: "Expected one of: true, false, 1, 0, yes, no",
|
|
32
|
+
})
|
|
30
33
|
.transform((v) => v === "true" || v === "1" || v === "yes")
|
|
31
34
|
.pipe(z.boolean());
|
|
32
35
|
break;
|
package/src/types.ts
CHANGED
|
@@ -53,10 +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
|
-
/** Returns all validated entries as a flat key-value record.
|
|
57
|
-
* Secrets are included — callers should redact as needed. */
|
|
58
56
|
getAll(): Record<string, unknown>;
|
|
59
57
|
readonly secret: Record<string, boolean>;
|
|
60
|
-
isEnabled(feature: string): boolean;
|
|
61
58
|
isKilled(feature: string): boolean;
|
|
59
|
+
isEnabled(feature: string): boolean;
|
|
62
60
|
}
|