@push.rocks/qenv 7.0.0 → 8.1.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/qenv.classes.invalidqenvfileerror.d.ts +21 -0
- package/dist_ts/qenv.classes.invalidqenvfileerror.js +25 -0
- package/dist_ts/qenv.classes.missingqenvfileerror.d.ts +16 -0
- package/dist_ts/qenv.classes.missingqenvfileerror.js +19 -0
- package/dist_ts/qenv.classes.missingrequiredenvvarserror.d.ts +18 -0
- package/dist_ts/qenv.classes.missingrequiredenvvarserror.js +19 -0
- package/dist_ts/qenv.classes.qenv.d.ts +56 -7
- package/dist_ts/qenv.classes.qenv.js +140 -49
- package/package.json +5 -5
- package/readme.md +120 -15
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +3 -0
- package/ts/qenv.classes.invalidqenvfileerror.ts +43 -0
- package/ts/qenv.classes.missingqenvfileerror.ts +28 -0
- package/ts/qenv.classes.missingrequiredenvvarserror.ts +30 -0
- package/ts/qenv.classes.qenv.ts +194 -65
package/readme.md
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
✅ **Flexible Formats** - Supports `.yml`, `.yaml`, and `.json` configuration files
|
|
11
11
|
✅ **Docker Ready** - Built-in support for Docker secrets and secret.json files
|
|
12
12
|
✅ **Async & Sync** - Both synchronous and asynchronous variable retrieval
|
|
13
|
-
✅ **
|
|
13
|
+
✅ **Enforced Requirements** - `required:` in `qenv.yml` is checked while constructing, with a typed error
|
|
14
|
+
✅ **Strict Mode** - Optional strict getter that throws for a missing variable
|
|
14
15
|
✅ **Base64 Objects** - Handle complex configuration objects with automatic encoding/decoding
|
|
15
16
|
✅ **Dynamic Resolution** - Support for async functions as environment variable sources
|
|
16
17
|
|
|
@@ -32,7 +33,8 @@ yarn add @push.rocks/qenv
|
|
|
32
33
|
```typescript
|
|
33
34
|
import { Qenv } from '@push.rocks/qenv';
|
|
34
35
|
|
|
35
|
-
// Create a new Qenv instance
|
|
36
|
+
// Create a new Qenv instance. Every name listed under `required:` in qenv.yml is resolved right
|
|
37
|
+
// here, and a missing one throws a QenvMissingRequiredEnvVarsError.
|
|
36
38
|
const qenv = new Qenv('./', './', true);
|
|
37
39
|
|
|
38
40
|
// Access environment variables
|
|
@@ -61,6 +63,28 @@ required:
|
|
|
61
63
|
- LOG_LEVEL
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
Every listed name is resolved while the `Qenv` instance is constructed. What is found lands in
|
|
67
|
+
`availableEnvVars` and, as a string, in `keyValueObject`; what is missing lands in `missingEnvVars`
|
|
68
|
+
and, unless you pass `failOnMissing: false`, makes the constructor throw.
|
|
69
|
+
|
|
70
|
+
With `failOnMissing` left at its default `true`, the `qenv.yml` itself is part of what is enforced:
|
|
71
|
+
if there is none at the given path, the constructor throws `QenvMissingQenvFileError` and names the
|
|
72
|
+
path it looked at. Without the file there is no `required:` list, so every check would pass by
|
|
73
|
+
accident - a container image that forgot to ship its `qenv.yml` would look healthy while enforcing
|
|
74
|
+
nothing. **Ship `qenv.yml` with your deployable**, next to the code that reads it. A `qenv.yml`
|
|
75
|
+
whose `required:` cannot be read as a list of names - a scalar instead of a list, or an entry that
|
|
76
|
+
is not a name - is refused the same way, with `QenvInvalidQenvFileError`.
|
|
77
|
+
|
|
78
|
+
A `qenv.yml` that requires nothing is legitimate and constructs: write `required: []`, or leave the
|
|
79
|
+
`required:` key out altogether. Only the file's absence and an unreadable list are refused.
|
|
80
|
+
|
|
81
|
+
The CLI or library case that reads from wherever it happens to run passes `failOnMissing: false`:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// no qenv.yml expected here - resolve what is there, enforce nothing
|
|
85
|
+
const qenv = new Qenv(process.cwd(), undefined, false);
|
|
86
|
+
```
|
|
87
|
+
|
|
64
88
|
#### 2. Provide Values (`env.yml` or `env.json`)
|
|
65
89
|
|
|
66
90
|
For local development, create an `env.yml` or `env.json` file:
|
|
@@ -97,6 +121,10 @@ Qenv loads variables in this order (first found wins):
|
|
|
97
121
|
3. **Docker secrets** - From `/run/secrets/`
|
|
98
122
|
4. **Docker secret JSON** - From `/run/secrets/secret.json`
|
|
99
123
|
|
|
124
|
+
All four sources are synchronous, so `getEnvVarOnDemand` and `getEnvVarOnDemandSync` resolve a name
|
|
125
|
+
identically. A `required:` entry is always a name; an async resolver function is a per-call source
|
|
126
|
+
and never takes part in the requirement check.
|
|
127
|
+
|
|
100
128
|
### Handling Complex Objects
|
|
101
129
|
|
|
102
130
|
Store and retrieve complex configuration objects:
|
|
@@ -134,6 +162,9 @@ const fetchFromVault = async () => {
|
|
|
134
162
|
const secret = await qenv.getEnvVarOnDemand(fetchFromVault);
|
|
135
163
|
```
|
|
136
164
|
|
|
165
|
+
The function form of `TEnvVarRef` is honoured by `getEnvVarOnDemand` and `getEnvVarOnDemandStrict`
|
|
166
|
+
only. `getEnvVarOnDemandSync` cannot await, so it accepts names and takes no resolver function.
|
|
167
|
+
|
|
137
168
|
### Working with Docker
|
|
138
169
|
|
|
139
170
|
Qenv seamlessly integrates with Docker secrets:
|
|
@@ -168,9 +199,18 @@ const dbPassword = await qenv.getEnvVarOnDemand('db_password');
|
|
|
168
199
|
Control how your application handles missing environment variables:
|
|
169
200
|
|
|
170
201
|
```typescript
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
202
|
+
import { Qenv, QenvMissingRequiredEnvVarsError } from '@push.rocks/qenv';
|
|
203
|
+
|
|
204
|
+
// Fail fast (default behaviour): the constructor throws
|
|
205
|
+
try {
|
|
206
|
+
const qenvStrict = new Qenv('./', './', true);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error instanceof QenvMissingRequiredEnvVarsError) {
|
|
209
|
+
console.error('Missing variables:', error.missingEnvVars);
|
|
210
|
+
console.error('Declared in:', error.qenvFilePathAbsolute);
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
174
214
|
|
|
175
215
|
// Graceful handling
|
|
176
216
|
const qenvRelaxed = new Qenv('./', './', false);
|
|
@@ -183,6 +223,16 @@ if (qenvRelaxed.missingEnvVars.length > 0) {
|
|
|
183
223
|
}
|
|
184
224
|
```
|
|
185
225
|
|
|
226
|
+
qenv never ends the process: the caller decides what an incomplete environment means. Where two
|
|
227
|
+
copies of qenv can end up in one dependency tree, `instanceof` is unreliable - match on the code
|
|
228
|
+
instead:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
if (error instanceof Error && 'code' in error && error.code === 'QENV_MISSING_REQUIRED_ENV_VARS') {
|
|
232
|
+
// handle the incomplete environment
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
186
236
|
### Strict Mode for Critical Variables
|
|
187
237
|
|
|
188
238
|
Use the new strict getter when you absolutely need a variable:
|
|
@@ -191,15 +241,19 @@ Use the new strict getter when you absolutely need a variable:
|
|
|
191
241
|
try {
|
|
192
242
|
// This will throw if TOKEN is not set
|
|
193
243
|
const token = await qenv.getEnvVarOnDemandStrict('TOKEN');
|
|
194
|
-
|
|
244
|
+
|
|
195
245
|
// You can also check multiple fallback names
|
|
196
246
|
const db = await qenv.getEnvVarOnDemandStrict(['DATABASE_URL', 'DB_CONNECTION']);
|
|
197
247
|
} catch (error) {
|
|
248
|
+
// the message names every reference that could not be resolved
|
|
198
249
|
console.error('Critical configuration missing:', error.message);
|
|
199
|
-
|
|
250
|
+
throw error;
|
|
200
251
|
}
|
|
201
252
|
```
|
|
202
253
|
|
|
254
|
+
A name that `qenv.yml` already lists under `required:` is resolved at construction time, so the
|
|
255
|
+
strict getter is for variables you look up on demand.
|
|
256
|
+
|
|
203
257
|
## 🏗️ CI/CD Integration
|
|
204
258
|
|
|
205
259
|
### GitHub Actions
|
|
@@ -276,12 +330,12 @@ qenv.logger.log('info', 'Custom log message');
|
|
|
276
330
|
Here's how you might use qenv in a production Node.js application:
|
|
277
331
|
|
|
278
332
|
```typescript
|
|
279
|
-
import { Qenv } from '@push.rocks/qenv';
|
|
333
|
+
import { Qenv, QenvMissingRequiredEnvVarsError } from '@push.rocks/qenv';
|
|
280
334
|
import { createServer } from './server';
|
|
281
335
|
import { connectDatabase } from './database';
|
|
282
336
|
|
|
283
337
|
async function bootstrap() {
|
|
284
|
-
// Initialize environment
|
|
338
|
+
// Initialize environment: throws when qenv.yml requires something no source provides
|
|
285
339
|
const qenv = new Qenv();
|
|
286
340
|
|
|
287
341
|
// Load critical configuration
|
|
@@ -304,7 +358,12 @@ async function bootstrap() {
|
|
|
304
358
|
}
|
|
305
359
|
|
|
306
360
|
bootstrap().catch(error => {
|
|
307
|
-
|
|
361
|
+
if (error instanceof QenvMissingRequiredEnvVarsError) {
|
|
362
|
+
console.error('Incomplete environment, missing:', error.missingEnvVars.join(', '));
|
|
363
|
+
} else {
|
|
364
|
+
console.error('Failed to start application:', error);
|
|
365
|
+
}
|
|
366
|
+
// exiting is the application's decision - qenv itself never calls process.exit
|
|
308
367
|
process.exit(1);
|
|
309
368
|
});
|
|
310
369
|
```
|
|
@@ -318,18 +377,24 @@ bootstrap().catch(error => {
|
|
|
318
377
|
new Qenv(
|
|
319
378
|
qenvFileBasePathArg?: string, // Path to qenv.yml (default: process.cwd())
|
|
320
379
|
envFileBasePathArg?: string, // Path to env.yml/json (default: same as qenv)
|
|
321
|
-
failOnMissing?: boolean //
|
|
380
|
+
failOnMissing?: boolean // Throw on missing required vars (default: true)
|
|
322
381
|
)
|
|
323
382
|
```
|
|
324
383
|
|
|
384
|
+
With `failOnMissing` true the constructor throws `QenvMissingQenvFileError` when there is no
|
|
385
|
+
`qenv.yml` at `qenvFileBasePathArg`, `QenvInvalidQenvFileError` when its `required:` cannot be read
|
|
386
|
+
as a list of names, and `QenvMissingRequiredEnvVarsError` when a name listed under `required:` is
|
|
387
|
+
not provided by any source. With `failOnMissing` false the constructor never throws: a missing
|
|
388
|
+
`qenv.yml` is accepted and an unreadable `required:` is logged as a warning.
|
|
389
|
+
|
|
325
390
|
#### Methods
|
|
326
391
|
|
|
327
392
|
| Method | Description | Returns |
|
|
328
393
|
|--------|-------------|---------|
|
|
329
|
-
| `getEnvVarOnDemand(name)` | Get environment variable value | `Promise<string \| undefined>` |
|
|
394
|
+
| `getEnvVarOnDemand(name)` | Get environment variable value, resolver functions included | `Promise<string \| undefined>` |
|
|
330
395
|
| `getEnvVarOnDemandStrict(name)` | Get variable or throw error | `Promise<string>` |
|
|
331
|
-
| `getEnvVarOnDemandSync(name)` | Synchronously get variable | `string \| undefined` |
|
|
332
|
-
| `getEnvVarOnDemandAsObject(name)` | Get variable as decoded object | `Promise<
|
|
396
|
+
| `getEnvVarOnDemandSync(name)` | Synchronously get variable, names only | `string \| undefined` |
|
|
397
|
+
| `getEnvVarOnDemandAsObject(name)` | Get variable as decoded object | `Promise<unknown>` |
|
|
333
398
|
|
|
334
399
|
#### Properties
|
|
335
400
|
|
|
@@ -338,7 +403,47 @@ new Qenv(
|
|
|
338
403
|
| `requiredEnvVars` | `string[]` | List of required variable names |
|
|
339
404
|
| `availableEnvVars` | `string[]` | List of found variable names |
|
|
340
405
|
| `missingEnvVars` | `string[]` | List of missing variable names |
|
|
341
|
-
| `keyValueObject` | `
|
|
406
|
+
| `keyValueObject` | `Record<string, string>` | Every available required variable as a resolved string |
|
|
407
|
+
| `qenvFilePathAbsolute` | `string` | Absolute path of the qenv.yml in use |
|
|
408
|
+
| `envFilePathAbsolute` | `string \| undefined` | Absolute path of the env file in use |
|
|
409
|
+
|
|
410
|
+
### Class: `QenvMissingRequiredEnvVarsError`
|
|
411
|
+
|
|
412
|
+
Thrown by the constructor when `failOnMissing` is true and a required variable has no source.
|
|
413
|
+
|
|
414
|
+
| Member | Type | Description |
|
|
415
|
+
|--------|------|-------------|
|
|
416
|
+
| `code` | `'QENV_MISSING_REQUIRED_ENV_VARS'` | Stable identifier, safe across duplicate installs |
|
|
417
|
+
| `missingEnvVars` | `string[]` | The required names no source provided |
|
|
418
|
+
| `requiredEnvVars` | `string[]` | Every name listed under `required:` |
|
|
419
|
+
| `qenvFilePathAbsolute` | `string` | The qenv.yml that declared them |
|
|
420
|
+
|
|
421
|
+
### Class: `QenvMissingQenvFileError`
|
|
422
|
+
|
|
423
|
+
Thrown by the constructor when `failOnMissing` is true and there is no `qenv.yml` to read the
|
|
424
|
+
`required:` list from.
|
|
425
|
+
|
|
426
|
+
| Member | Type | Description |
|
|
427
|
+
|--------|------|-------------|
|
|
428
|
+
| `code` | `'QENV_MISSING_QENV_FILE'` | Stable identifier, safe across duplicate installs |
|
|
429
|
+
| `qenvFilePathAbsolute` | `string` | The qenv.yml path that was looked at |
|
|
430
|
+
| `qenvDir` | `string` | The resolved directory that should hold it |
|
|
431
|
+
|
|
432
|
+
### Class: `QenvInvalidQenvFileError`
|
|
433
|
+
|
|
434
|
+
Thrown by the constructor when `failOnMissing` is true and `required:` is not a list of names;
|
|
435
|
+
logged as a warning with the same message when `failOnMissing` is false.
|
|
436
|
+
|
|
437
|
+
| Member | Type | Description |
|
|
438
|
+
|--------|------|-------------|
|
|
439
|
+
| `code` | `'QENV_INVALID_QENV_FILE'` | Stable identifier, safe across duplicate installs |
|
|
440
|
+
| `reason` | `'requiredNotAnArray' \| 'requiredEntryNotAString'` | Which rule the file broke |
|
|
441
|
+
| `invalidValueTypeName` | `string` | The type of the offending value, never the value itself |
|
|
442
|
+
| `qenvFilePathAbsolute` | `string` | The qenv.yml that declared it |
|
|
443
|
+
|
|
444
|
+
## Issue Reporting and Security
|
|
445
|
+
|
|
446
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
342
447
|
|
|
343
448
|
## License and Legal Information
|
|
344
449
|
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/index.ts
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** which rule of the `required:` list a qenv.yml broke */
|
|
2
|
+
export type TQenvInvalidQenvFileReason = 'requiredNotAnArray' | 'requiredEntryNotAString';
|
|
3
|
+
|
|
4
|
+
export interface IQenvInvalidQenvFileErrorOptions {
|
|
5
|
+
qenvFilePathAbsolute: string;
|
|
6
|
+
reason: TQenvInvalidQenvFileReason;
|
|
7
|
+
/** the type name of the offending value, never the value itself */
|
|
8
|
+
invalidValueTypeName: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const reasonDescriptions: Record<TQenvInvalidQenvFileReason, (typeNameArg: string) => string> = {
|
|
12
|
+
requiredNotAnArray: (typeNameArg) =>
|
|
13
|
+
`"required:" is of type ${typeNameArg} instead of a list of environment variable names`,
|
|
14
|
+
requiredEntryNotAString: (typeNameArg) =>
|
|
15
|
+
`"required:" contains an entry of type ${typeNameArg} instead of an environment variable name`,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when qenv.yml declares a `required:` that qenv cannot read as a list of names. Such a
|
|
20
|
+
* declaration silently enforced nothing, which is the same fail-open as a missing qenv.yml.
|
|
21
|
+
* A file without any `required:` key, and one with an empty list, legitimately require nothing
|
|
22
|
+
* and never reach this error.
|
|
23
|
+
*/
|
|
24
|
+
export class QenvInvalidQenvFileError extends Error {
|
|
25
|
+
public readonly code = 'QENV_INVALID_QENV_FILE';
|
|
26
|
+
public readonly qenvFilePathAbsolute: string;
|
|
27
|
+
public readonly reason: TQenvInvalidQenvFileReason;
|
|
28
|
+
public readonly invalidValueTypeName: string;
|
|
29
|
+
|
|
30
|
+
constructor(optionsArg: IQenvInvalidQenvFileErrorOptions) {
|
|
31
|
+
super(
|
|
32
|
+
`The qenv.yml at ${optionsArg.qenvFilePathAbsolute} declares an unreadable requirement: ` +
|
|
33
|
+
`${reasonDescriptions[optionsArg.reason](optionsArg.invalidValueTypeName)}. ` +
|
|
34
|
+
`Write "required:" as a list of environment variable names, or omit it to require nothing.`
|
|
35
|
+
);
|
|
36
|
+
this.name = 'QenvInvalidQenvFileError';
|
|
37
|
+
this.qenvFilePathAbsolute = optionsArg.qenvFilePathAbsolute;
|
|
38
|
+
this.reason = optionsArg.reason;
|
|
39
|
+
// only the type is carried and rendered: a malformed qenv.yml can hold a value where a name
|
|
40
|
+
// belongs, and a value may be a secret
|
|
41
|
+
this.invalidValueTypeName = optionsArg.invalidValueTypeName;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface IQenvMissingQenvFileErrorOptions {
|
|
2
|
+
qenvFilePathAbsolute: string;
|
|
3
|
+
qenvDir: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when a Qenv that is set to fail on missing variables finds no qenv.yml at all.
|
|
8
|
+
* Without the file there is no `required:` list to enforce, so every check would pass by
|
|
9
|
+
* accident - a deployable that forgot to ship its qenv.yml would look healthy. `code` identifies
|
|
10
|
+
* the refusal even when two copies of qenv end up in one tree, which makes `instanceof` unreliable.
|
|
11
|
+
*/
|
|
12
|
+
export class QenvMissingQenvFileError extends Error {
|
|
13
|
+
public readonly code = 'QENV_MISSING_QENV_FILE';
|
|
14
|
+
public readonly qenvFilePathAbsolute: string;
|
|
15
|
+
public readonly qenvDir: string;
|
|
16
|
+
|
|
17
|
+
constructor(optionsArg: IQenvMissingQenvFileErrorOptions) {
|
|
18
|
+
super(
|
|
19
|
+
`qenv found no qenv.yml at ${optionsArg.qenvFilePathAbsolute}. It was constructed to fail ` +
|
|
20
|
+
`on missing required environment variables, and without qenv.yml there is no "required:" ` +
|
|
21
|
+
`list to enforce, so nothing would be checked. Ship qenv.yml with your deployable, or ` +
|
|
22
|
+
`construct Qenv with failOnMissing set to false.`
|
|
23
|
+
);
|
|
24
|
+
this.name = 'QenvMissingQenvFileError';
|
|
25
|
+
this.qenvFilePathAbsolute = optionsArg.qenvFilePathAbsolute;
|
|
26
|
+
this.qenvDir = optionsArg.qenvDir;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface IQenvMissingRequiredEnvVarsErrorOptions {
|
|
2
|
+
missingEnvVars: string[];
|
|
3
|
+
requiredEnvVars: string[];
|
|
4
|
+
qenvFilePathAbsolute: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when a variable listed under `required:` in qenv.yml is not provided by any source.
|
|
9
|
+
* The names are carried as data so a caller can report or branch on them without parsing the
|
|
10
|
+
* message, and `code` identifies the refusal even when two copies of qenv end up in one tree,
|
|
11
|
+
* which makes `instanceof` unreliable.
|
|
12
|
+
*/
|
|
13
|
+
export class QenvMissingRequiredEnvVarsError extends Error {
|
|
14
|
+
public readonly code = 'QENV_MISSING_REQUIRED_ENV_VARS';
|
|
15
|
+
public readonly missingEnvVars: string[];
|
|
16
|
+
public readonly requiredEnvVars: string[];
|
|
17
|
+
public readonly qenvFilePathAbsolute: string;
|
|
18
|
+
|
|
19
|
+
constructor(optionsArg: IQenvMissingRequiredEnvVarsErrorOptions) {
|
|
20
|
+
super(
|
|
21
|
+
`qenv is missing required environment variables: ${optionsArg.missingEnvVars.join(', ')}. ` +
|
|
22
|
+
`They are listed under "required:" in ${optionsArg.qenvFilePathAbsolute} and were found ` +
|
|
23
|
+
`neither in the process environment, nor in the env file, nor in the Docker secrets.`
|
|
24
|
+
);
|
|
25
|
+
this.name = 'QenvMissingRequiredEnvVarsError';
|
|
26
|
+
this.missingEnvVars = [...optionsArg.missingEnvVars];
|
|
27
|
+
this.requiredEnvVars = [...optionsArg.requiredEnvVars];
|
|
28
|
+
this.qenvFilePathAbsolute = optionsArg.qenvFilePathAbsolute;
|
|
29
|
+
}
|
|
30
|
+
}
|