@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/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
- ✅ **Strict Mode** - Optional strict mode that throws errors for missing variables
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
- // Fail fast (default behavior)
172
- const qenvStrict = new Qenv('./', './', true);
173
- // Application exits if required variables are missing
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
- process.exit(1);
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
- console.error('Failed to start application:', error);
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 // Exit on missing vars (default: true)
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<any>` |
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` | `object` | All loaded variables as key-value pairs |
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
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/qenv',
6
- version: '7.0.0',
6
+ version: '8.1.0',
7
7
  description: 'A module for easily handling environment variables in Node.js projects with support for .yml and .json configuration.'
8
8
  }
package/ts/index.ts CHANGED
@@ -1 +1,4 @@
1
1
  export * from './qenv.classes.qenv.js';
2
+ export * from './qenv.classes.invalidqenvfileerror.js';
3
+ export * from './qenv.classes.missingqenvfileerror.js';
4
+ export * from './qenv.classes.missingrequiredenvvarserror.js';
@@ -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
+ }