@push.rocks/qenv 6.1.5 → 8.0.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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/qenv',
6
- version: '6.1.5',
6
+ version: '8.0.0',
7
7
  description: 'A module for easily handling environment variables in Node.js projects with support for .yml and .json configuration.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxrQkFBa0I7SUFDeEIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHVIQUF1SDtDQUNySSxDQUFBIn0=
@@ -1 +1,2 @@
1
1
  export * from './qenv.classes.qenv.js';
2
+ export * from './qenv.classes.missingrequiredenvvarserror.js';
package/dist_ts/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './qenv.classes.qenv.js';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHdCQUF3QixDQUFDIn0=
2
+ export * from './qenv.classes.missingrequiredenvvarserror.js';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsK0NBQStDLENBQUMifQ==
@@ -0,0 +1,18 @@
1
+ export interface IQenvMissingRequiredEnvVarsErrorOptions {
2
+ missingEnvVars: string[];
3
+ requiredEnvVars: string[];
4
+ qenvFilePathAbsolute: string;
5
+ }
6
+ /**
7
+ * Thrown when a variable listed under `required:` in qenv.yml is not provided by any source.
8
+ * The names are carried as data so a caller can report or branch on them without parsing the
9
+ * message, and `code` identifies the refusal even when two copies of qenv end up in one tree,
10
+ * which makes `instanceof` unreliable.
11
+ */
12
+ export declare class QenvMissingRequiredEnvVarsError extends Error {
13
+ readonly code = "QENV_MISSING_REQUIRED_ENV_VARS";
14
+ readonly missingEnvVars: string[];
15
+ readonly requiredEnvVars: string[];
16
+ readonly qenvFilePathAbsolute: string;
17
+ constructor(optionsArg: IQenvMissingRequiredEnvVarsErrorOptions);
18
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Thrown when a variable listed under `required:` in qenv.yml is not provided by any source.
3
+ * The names are carried as data so a caller can report or branch on them without parsing the
4
+ * message, and `code` identifies the refusal even when two copies of qenv end up in one tree,
5
+ * which makes `instanceof` unreliable.
6
+ */
7
+ export class QenvMissingRequiredEnvVarsError extends Error {
8
+ constructor(optionsArg) {
9
+ super(`qenv is missing required environment variables: ${optionsArg.missingEnvVars.join(', ')}. ` +
10
+ `They are listed under "required:" in ${optionsArg.qenvFilePathAbsolute} and were found ` +
11
+ `neither in the process environment, nor in the env file, nor in the Docker secrets.`);
12
+ this.code = 'QENV_MISSING_REQUIRED_ENV_VARS';
13
+ this.name = 'QenvMissingRequiredEnvVarsError';
14
+ this.missingEnvVars = [...optionsArg.missingEnvVars];
15
+ this.requiredEnvVars = [...optionsArg.requiredEnvVars];
16
+ this.qenvFilePathAbsolute = optionsArg.qenvFilePathAbsolute;
17
+ }
18
+ }
19
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicWVudi5jbGFzc2VzLm1pc3NpbmdyZXF1aXJlZGVudnZhcnNlcnJvci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3FlbnYuY2xhc3Nlcy5taXNzaW5ncmVxdWlyZWRlbnZ2YXJzZXJyb3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBTUE7Ozs7O0dBS0c7QUFDSCxNQUFNLE9BQU8sK0JBQWdDLFNBQVEsS0FBSztJQU14RCxZQUFZLFVBQW1EO1FBQzdELEtBQUssQ0FDSCxtREFBbUQsVUFBVSxDQUFDLGNBQWMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUk7WUFDekYsd0NBQXdDLFVBQVUsQ0FBQyxvQkFBb0Isa0JBQWtCO1lBQ3pGLHFGQUFxRixDQUN4RixDQUFDO1FBVlksU0FBSSxHQUFHLGdDQUFnQyxDQUFDO1FBV3RELElBQUksQ0FBQyxJQUFJLEdBQUcsaUNBQWlDLENBQUM7UUFDOUMsSUFBSSxDQUFDLGNBQWMsR0FBRyxDQUFDLEdBQUcsVUFBVSxDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBQ3JELElBQUksQ0FBQyxlQUFlLEdBQUcsQ0FBQyxHQUFHLFVBQVUsQ0FBQyxlQUFlLENBQUMsQ0FBQztRQUN2RCxJQUFJLENBQUMsb0JBQW9CLEdBQUcsVUFBVSxDQUFDLG9CQUFvQixDQUFDO0lBQzlELENBQUM7Q0FDRiJ9
@@ -1,34 +1,71 @@
1
- import { CloudlyAdapter } from './qenv.classes.configvaultadapter.js';
2
1
  import * as plugins from './qenv.plugins.js';
2
+ /**
3
+ * a reference to an environment variable: its name, or an async function that produces the value.
4
+ * The function form is only honoured by the async getters; the synchronous getter accepts names.
5
+ */
3
6
  export type TEnvVarRef = string | (() => Promise<string>);
4
- type TKeyValueObject = Record<string, any>;
7
+ /** the resolved value of every available required env var, always a string */
8
+ export type TEnvVarValueMap = Record<string, string>;
5
9
  export declare class Qenv {
10
+ /** the names listed under `required:` in qenv.yml */
6
11
  requiredEnvVars: string[];
12
+ /** the required names that a source provided */
7
13
  availableEnvVars: string[];
14
+ /** the required names that no source provided */
8
15
  missingEnvVars: string[];
9
- keyValueObject: TKeyValueObject;
16
+ /** the resolved value of every available required name */
17
+ keyValueObject: TEnvVarValueMap;
10
18
  logger: plugins.smartlog.ConsoleLog;
11
- cloudlyAdapter: CloudlyAdapter;
12
19
  qenvFilePathAbsolute: string;
13
20
  envFilePathAbsolute?: string;
21
+ /**
22
+ * Resolves every name listed under `required:` in qenv.yml while constructing.
23
+ * @param qenvFileBasePathArg directory that holds qenv.yml
24
+ * @param envFileBasePathArg directory that holds env.json, env.yml or env.yaml
25
+ * @param failOnMissing throws a QenvMissingRequiredEnvVarsError when a variable listed under
26
+ * `required:` in qenv.yml is not provided by any source. Pass false to only record the names in
27
+ * `missingEnvVars` and continue.
28
+ * @throws QenvMissingRequiredEnvVarsError
29
+ */
14
30
  constructor(qenvFileBasePathArg?: string, envFileBasePathArg?: string, failOnMissing?: boolean);
15
31
  private initializeFilePaths;
16
32
  private loadRequiredEnvVars;
17
33
  private loadAvailableEnvVars;
18
34
  private checkForMissingEnvVars;
35
+ /**
36
+ * Resolves an env var from the process environment, the env file and the Docker secrets, in that
37
+ * order. An array is tried left to right and the first defined value wins.
38
+ * @param envVarNameOrNames a name, an async resolver function, or a list of either
39
+ */
19
40
  getEnvVarOnDemand(envVarNameOrNames: TEnvVarRef | TEnvVarRef[]): Promise<string | undefined>;
20
41
  /**
21
- * Like getEnvVarOnDemand, but throws an error if the env var is not set.
22
- * @param envVarNameOrNames
23
- * @returns
42
+ * Like getEnvVarOnDemand, but throws when no source provides a value.
43
+ * @param envVarNameOrNames a name, an async resolver function, or a list of either
24
44
  */
25
45
  getEnvVarOnDemandStrict(envVarNameOrNames: TEnvVarRef | TEnvVarRef[]): Promise<string>;
46
+ /**
47
+ * The synchronous counterpart of getEnvVarOnDemand. It resolves names only: an async resolver
48
+ * function cannot be awaited here, so the function form of TEnvVarRef is not accepted.
49
+ * @param envVarNameOrNames a name or a list of names
50
+ */
26
51
  getEnvVarOnDemandSync(envVarNameOrNames: string | string[]): string | undefined;
27
- getEnvVarOnDemandAsObject(envVarNameOrNames: string | string[]): Promise<any>;
52
+ /**
53
+ * Resolves an env var whose value was stored as a base64 encoded object and decodes it. A plain
54
+ * value is returned as the string it is, so the caller narrows what it gets.
55
+ * @param envVarNameOrNames a name or a list of names
56
+ */
57
+ getEnvVarOnDemandAsObject(envVarNameOrNames: string | string[]): Promise<unknown>;
28
58
  private tryGetEnvVar;
29
59
  private tryGetEnvVarSync;
60
+ /** renders env var references for an error message, naming a resolver function where it has one */
61
+ private describeEnvVarRefs;
30
62
  private getFromEnvironmentVariable;
31
63
  private getFromEnvYamlOrJsonFile;
64
+ /**
65
+ * the directory Docker mounts secrets into. It is a method so a test can point both secret
66
+ * readers at a directory it is allowed to create; a process can never write /run/secrets itself.
67
+ */
68
+ protected getDockerSecretsDirectoryPath(): string;
32
69
  private getFromDockerSecret;
33
70
  private getFromDockerSecretJson;
34
71
  private encodeBase64;
@@ -37,4 +74,3 @@ export declare class Qenv {
37
74
  private directoryExists;
38
75
  private readObjectFromFile;
39
76
  }
40
- export {};
@@ -1,14 +1,26 @@
1
- import { CloudlyAdapter } from './qenv.classes.configvaultadapter.js';
2
1
  import * as plugins from './qenv.plugins.js';
2
+ import { QenvMissingRequiredEnvVarsError } from './qenv.classes.missingrequiredenvvarserror.js';
3
3
  export class Qenv {
4
+ /**
5
+ * Resolves every name listed under `required:` in qenv.yml while constructing.
6
+ * @param qenvFileBasePathArg directory that holds qenv.yml
7
+ * @param envFileBasePathArg directory that holds env.json, env.yml or env.yaml
8
+ * @param failOnMissing throws a QenvMissingRequiredEnvVarsError when a variable listed under
9
+ * `required:` in qenv.yml is not provided by any source. Pass false to only record the names in
10
+ * `missingEnvVars` and continue.
11
+ * @throws QenvMissingRequiredEnvVarsError
12
+ */
4
13
  constructor(qenvFileBasePathArg = process.cwd(), envFileBasePathArg, failOnMissing = true) {
14
+ /** the names listed under `required:` in qenv.yml */
5
15
  this.requiredEnvVars = [];
16
+ /** the required names that a source provided */
6
17
  this.availableEnvVars = [];
18
+ /** the required names that no source provided */
7
19
  this.missingEnvVars = [];
20
+ /** the resolved value of every available required name */
8
21
  this.keyValueObject = {};
9
22
  this.logger = new plugins.smartlog.ConsoleLog();
10
23
  this.qenvFilePathAbsolute = '';
11
- this.cloudlyAdapter = new CloudlyAdapter();
12
24
  this.initializeFilePaths(qenvFileBasePathArg, envFileBasePathArg);
13
25
  this.loadRequiredEnvVars();
14
26
  this.loadAvailableEnvVars();
@@ -40,20 +52,25 @@ export class Qenv {
40
52
  }
41
53
  }
42
54
  loadRequiredEnvVars() {
43
- if (this.fileExists(this.qenvFilePathAbsolute)) {
44
- const qenvFile = this.readObjectFromFile(this.qenvFilePathAbsolute);
45
- const requiredEnvVars = qenvFile.required;
46
- if (Array.isArray(requiredEnvVars)) {
47
- this.requiredEnvVars.push(...requiredEnvVars.filter((envVar) => typeof envVar === 'string'));
48
- }
49
- else {
50
- this.logger.log('warn', 'qenv.yml does not contain a "required" Array!');
51
- }
55
+ if (!this.fileExists(this.qenvFilePathAbsolute)) {
56
+ return;
52
57
  }
58
+ const qenvFile = this.readObjectFromFile(this.qenvFilePathAbsolute);
59
+ const declaredRequirements = qenvFile['required'];
60
+ if (!Array.isArray(declaredRequirements)) {
61
+ this.logger.log('warn', 'qenv.yml does not contain a "required" Array!');
62
+ return;
63
+ }
64
+ const declaredEntries = declaredRequirements;
65
+ this.requiredEnvVars.push(...declaredEntries.filter((entry) => typeof entry === 'string'));
53
66
  }
54
67
  loadAvailableEnvVars() {
68
+ // resolved synchronously: a required env var is always a name, and every source a name can come
69
+ // from - process environment, env file, Docker secrets - is a synchronous read. Resolving it
70
+ // here as a promise would store a pending promise instead of a value and make the check below
71
+ // pass for every name.
55
72
  for (const envVar of this.requiredEnvVars) {
56
- const value = this.getEnvVarOnDemand(envVar);
73
+ const value = this.tryGetEnvVarSync(envVar);
57
74
  if (value !== undefined) {
58
75
  this.availableEnvVars.push(envVar);
59
76
  this.keyValueObject[envVar] = value;
@@ -62,18 +79,24 @@ export class Qenv {
62
79
  }
63
80
  checkForMissingEnvVars(failOnMissing) {
64
81
  this.missingEnvVars = this.requiredEnvVars.filter((envVar) => !this.availableEnvVars.includes(envVar));
65
- if (this.missingEnvVars.length > 0) {
66
- console.info('Required Env Vars are:', this.requiredEnvVars);
67
- console.error('Missing Env Vars:', this.missingEnvVars);
68
- if (failOnMissing) {
69
- this.logger.log('error', 'Exiting due to missing env vars!');
70
- process.exit(1);
71
- }
72
- else {
73
- this.logger.log('warn', 'qenv is not set to fail on missing environment variables');
74
- }
82
+ if (this.missingEnvVars.length === 0) {
83
+ return;
75
84
  }
85
+ if (failOnMissing) {
86
+ // a library must not end the process: the caller decides what an incomplete environment means
87
+ throw new QenvMissingRequiredEnvVarsError({
88
+ missingEnvVars: this.missingEnvVars,
89
+ requiredEnvVars: this.requiredEnvVars,
90
+ qenvFilePathAbsolute: this.qenvFilePathAbsolute,
91
+ });
92
+ }
93
+ this.logger.log('warn', `qenv is not set to fail on missing environment variables. Missing: ${this.missingEnvVars.join(', ')}`);
76
94
  }
95
+ /**
96
+ * Resolves an env var from the process environment, the env file and the Docker secrets, in that
97
+ * order. An array is tried left to right and the first defined value wins.
98
+ * @param envVarNameOrNames a name, an async resolver function, or a list of either
99
+ */
77
100
  async getEnvVarOnDemand(envVarNameOrNames) {
78
101
  if (Array.isArray(envVarNameOrNames)) {
79
102
  for (const envVarName of envVarNameOrNames) {
@@ -89,19 +112,22 @@ export class Qenv {
89
112
  }
90
113
  }
91
114
  /**
92
- * Like getEnvVarOnDemand, but throws an error if the env var is not set.
93
- * @param envVarNameOrNames
94
- * @returns
115
+ * Like getEnvVarOnDemand, but throws when no source provides a value.
116
+ * @param envVarNameOrNames a name, an async resolver function, or a list of either
95
117
  */
96
118
  async getEnvVarOnDemandStrict(envVarNameOrNames) {
97
119
  const value = await this.getEnvVarOnDemand(envVarNameOrNames);
98
120
  if (value === undefined) {
99
- throw new Error(`Env var ${envVarNameOrNames} is not set!`);
121
+ throw new Error(`Env var ${this.describeEnvVarRefs(envVarNameOrNames)} is not set!`);
100
122
  }
101
123
  return value;
102
124
  }
125
+ /**
126
+ * The synchronous counterpart of getEnvVarOnDemand. It resolves names only: an async resolver
127
+ * function cannot be awaited here, so the function form of TEnvVarRef is not accepted.
128
+ * @param envVarNameOrNames a name or a list of names
129
+ */
103
130
  getEnvVarOnDemandSync(envVarNameOrNames) {
104
- console.warn('requesting env var sync leaves out potentially important async env sources.');
105
131
  if (Array.isArray(envVarNameOrNames)) {
106
132
  for (const envVarName of envVarNameOrNames) {
107
133
  const value = this.tryGetEnvVarSync(envVarName);
@@ -115,6 +141,11 @@ export class Qenv {
115
141
  return this.tryGetEnvVarSync(envVarNameOrNames);
116
142
  }
117
143
  }
144
+ /**
145
+ * Resolves an env var whose value was stored as a base64 encoded object and decodes it. A plain
146
+ * value is returned as the string it is, so the caller narrows what it gets.
147
+ * @param envVarNameOrNames a name or a list of names
148
+ */
118
149
  async getEnvVarOnDemandAsObject(envVarNameOrNames) {
119
150
  const rawValue = await this.getEnvVarOnDemand(envVarNameOrNames);
120
151
  if (rawValue && rawValue.startsWith('base64Object:')) {
@@ -127,33 +158,36 @@ export class Qenv {
127
158
  if (typeof envVarRefArg === 'function') {
128
159
  return await envVarRefArg();
129
160
  }
130
- const sources = [
131
- this.getFromEnvironmentVariable(envVarRefArg),
132
- this.getFromEnvYamlOrJsonFile(envVarRefArg),
133
- this.getFromDockerSecret(envVarRefArg),
134
- this.getFromDockerSecretJson(envVarRefArg)
135
- ];
136
- for (const value of sources) {
137
- if (value !== undefined) {
138
- return value;
139
- }
140
- }
141
- return undefined;
161
+ // a name resolves from synchronous sources only, so both getters share one resolution order
162
+ return this.tryGetEnvVarSync(envVarRefArg);
142
163
  }
143
164
  tryGetEnvVarSync(envVarName) {
165
+ // read lazily: a source is only touched once every earlier one came back undefined, so a name
166
+ // the process environment answers never opens a secret file, and a malformed secret.json only
167
+ // ever affects the names that actually reach it
144
168
  const sources = [
145
- this.getFromEnvironmentVariable(envVarName),
146
- this.getFromEnvYamlOrJsonFile(envVarName),
147
- this.getFromDockerSecret(envVarName),
148
- this.getFromDockerSecretJson(envVarName)
169
+ () => this.getFromEnvironmentVariable(envVarName),
170
+ () => this.getFromEnvYamlOrJsonFile(envVarName),
171
+ () => this.getFromDockerSecret(envVarName),
172
+ () => this.getFromDockerSecretJson(envVarName),
149
173
  ];
150
- for (const value of sources) {
174
+ for (const readSource of sources) {
175
+ const value = readSource();
151
176
  if (value !== undefined) {
152
177
  return value;
153
178
  }
154
179
  }
155
180
  return undefined;
156
181
  }
182
+ /** renders env var references for an error message, naming a resolver function where it has one */
183
+ describeEnvVarRefs(envVarNameOrNames) {
184
+ const envVarRefs = Array.isArray(envVarNameOrNames) ? envVarNameOrNames : [envVarNameOrNames];
185
+ return envVarRefs
186
+ .map((envVarRef) => typeof envVarRef === 'function'
187
+ ? `${envVarRef.name || 'anonymous'}()`
188
+ : envVarRef)
189
+ .join(', ');
190
+ }
157
191
  getFromEnvironmentVariable(envVarName) {
158
192
  return process.env[envVarName];
159
193
  }
@@ -176,19 +210,27 @@ export class Qenv {
176
210
  return undefined;
177
211
  }
178
212
  }
213
+ /**
214
+ * the directory Docker mounts secrets into. It is a method so a test can point both secret
215
+ * readers at a directory it is allowed to create; a process can never write /run/secrets itself.
216
+ */
217
+ getDockerSecretsDirectoryPath() {
218
+ return '/run/secrets';
219
+ }
179
220
  getFromDockerSecret(envVarName) {
180
- const secretPath = `/run/secrets/${envVarName}`;
221
+ const secretPath = plugins.path.join(this.getDockerSecretsDirectoryPath(), envVarName);
181
222
  if (this.fileExists(secretPath)) {
182
223
  return plugins.fs.readFileSync(secretPath, 'utf8');
183
224
  }
184
225
  return undefined;
185
226
  }
186
227
  getFromDockerSecretJson(envVarName) {
187
- if (this.directoryExists('/run/secrets')) {
188
- const availableSecrets = plugins.fs.readdirSync('/run/secrets');
228
+ const secretsDirectoryPath = this.getDockerSecretsDirectoryPath();
229
+ if (this.directoryExists(secretsDirectoryPath)) {
230
+ const availableSecrets = plugins.fs.readdirSync(secretsDirectoryPath);
189
231
  for (const secret of availableSecrets) {
190
232
  if (secret.includes('secret.json')) {
191
- const secretObject = this.readObjectFromFile(`/run/secrets/${secret}`);
233
+ const secretObject = this.readObjectFromFile(plugins.path.join(secretsDirectoryPath, secret));
192
234
  const value = secretObject[envVarName];
193
235
  if (value === undefined) {
194
236
  continue;
@@ -229,7 +271,9 @@ export class Qenv {
229
271
  const parsedObject = filePath.endsWith('.json')
230
272
  ? JSON.parse(fileString)
231
273
  : plugins.yaml.parse(fileString);
232
- return typeof parsedObject === 'object' && parsedObject !== null ? parsedObject : {};
274
+ return typeof parsedObject === 'object' && parsedObject !== null
275
+ ? parsedObject
276
+ : {};
233
277
  }
234
278
  }
235
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicWVudi5jbGFzc2VzLnFlbnYuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9xZW52LmNsYXNzZXMucWVudi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sc0NBQXNDLENBQUM7QUFDdEUsT0FBTyxLQUFLLE9BQU8sTUFBTSxtQkFBbUIsQ0FBQztBQUs3QyxNQUFNLE9BQU8sSUFBSTtJQVlmLFlBQ0Usc0JBQThCLE9BQU8sQ0FBQyxHQUFHLEVBQUUsRUFDM0Msa0JBQTJCLEVBQzNCLGdCQUF5QixJQUFJO1FBZHhCLG9CQUFlLEdBQWEsRUFBRSxDQUFDO1FBQy9CLHFCQUFnQixHQUFhLEVBQUUsQ0FBQztRQUNoQyxtQkFBYyxHQUFhLEVBQUUsQ0FBQztRQUM5QixtQkFBYyxHQUFvQixFQUFFLENBQUM7UUFDckMsV0FBTSxHQUFHLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQztRQUkzQyx5QkFBb0IsR0FBRyxFQUFFLENBQUM7UUFRL0IsSUFBSSxDQUFDLGNBQWMsR0FBRyxJQUFJLGNBQWMsRUFBRSxDQUFDO1FBQzNDLElBQUksQ0FBQyxtQkFBbUIsQ0FBQyxtQkFBbUIsRUFBRSxrQkFBa0IsQ0FBQyxDQUFDO1FBQ2xFLElBQUksQ0FBQyxtQkFBbUIsRUFBRSxDQUFDO1FBQzNCLElBQUksQ0FBQyxvQkFBb0IsRUFBRSxDQUFDO1FBQzVCLElBQUksQ0FBQyxzQkFBc0IsQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUM3QyxDQUFDO0lBRU8sbUJBQW1CLENBQUMsbUJBQTJCLEVBQUUsa0JBQTJCO1FBQ2xGLElBQUksQ0FBQyxvQkFBb0IsR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FDM0MsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsbUJBQW1CLENBQUMsRUFDekMsVUFBVSxDQUNYLENBQUM7UUFFRixJQUFJLGtCQUFrQixFQUFFLENBQUM7WUFDdkIs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
279
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicWVudi5jbGFzc2VzLnFlbnYuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9xZW52LmNsYXNzZXMucWVudi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLG1CQUFtQixDQUFDO0FBQzdDLE9BQU8sRUFBRSwrQkFBK0IsRUFBRSxNQUFNLCtDQUErQyxDQUFDO0FBY2hHLE1BQU0sT0FBTyxJQUFJO0lBa0JmOzs7Ozs7OztPQVFHO0lBQ0gsWUFDRSxzQkFBOEIsT0FBTyxDQUFDLEdBQUcsRUFBRSxFQUMzQyxrQkFBMkIsRUFDM0IsZ0JBQXlCLElBQUk7UUE3Qi9CLHFEQUFxRDtRQUM5QyxvQkFBZSxHQUFhLEVBQUUsQ0FBQztRQUV0QyxnREFBZ0Q7UUFDekMscUJBQWdCLEdBQWEsRUFBRSxDQUFDO1FBRXZDLGlEQUFpRDtRQUMxQyxtQkFBYyxHQUFhLEVBQUUsQ0FBQztRQUVyQywwREFBMEQ7UUFDbkQsbUJBQWMsR0FBb0IsRUFBRSxDQUFDO1FBRXJDLFdBQU0sR0FBRyxJQUFJLE9BQU8sQ0FBQyxRQUFRLENBQUMsVUFBVSxFQUFFLENBQUM7UUFFM0MseUJBQW9CLEdBQUcsRUFBRSxDQUFDO1FBaUIvQixJQUFJLENBQUMsbUJBQW1CLENBQUMsbUJBQW1CLEVBQUUsa0JBQWtCLENBQUMsQ0FBQztRQUNsRSxJQUFJLENBQUMsbUJBQW1CLEVBQUUsQ0FBQztRQUMzQixJQUFJLENBQUMsb0JBQW9CLEVBQUUsQ0FBQztRQUM1QixJQUFJLENBQUMsc0JBQXNCLENBQUMsYUFBYSxDQUFDLENBQUM7SUFDN0MsQ0FBQztJQUVPLG1CQUFtQixDQUFDLG1CQUEyQixFQUFFLGtCQUEyQjtRQUNsRixJQUFJLENBQUMsb0JBQW9CLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQzNDLE9BQU8sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLEVBQ3pDLFVBQVUsQ0FDWCxDQUFDO1FBRUYs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
@@ -1,11 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'path';
3
3
  export { fs, path };
4
- import * as typedrequest from '@api.global/typedrequest';
5
- export { typedrequest, };
6
4
  import * as smartlog from '@push.rocks/smartlog';
7
5
  export { smartlog };
8
- import * as configvaultInterfaces from '@configvault.io/interfaces';
9
- export { configvaultInterfaces };
10
6
  import * as yaml from 'yaml';
11
7
  export { yaml };
@@ -2,16 +2,10 @@
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'path';
4
4
  export { fs, path };
5
- // @api.global scope
6
- import * as typedrequest from '@api.global/typedrequest';
7
- export { typedrequest, };
8
5
  // @pushrocks scope
9
6
  import * as smartlog from '@push.rocks/smartlog';
10
7
  export { smartlog };
11
- // @configvault.io scope
12
- import * as configvaultInterfaces from '@configvault.io/interfaces';
13
- export { configvaultInterfaces };
14
8
  // third party scope
15
9
  import * as yaml from 'yaml';
16
10
  export { yaml };
17
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicWVudi5wbHVnaW5zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvcWVudi5wbHVnaW5zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFNBQVM7QUFDVCxPQUFPLEtBQUssRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM5QixPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUU3QixPQUFPLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxDQUFDO0FBRXBCLG9CQUFvQjtBQUNwQixPQUFPLEtBQUssWUFBWSxNQUFNLDBCQUEwQixDQUFDO0FBRXpELE9BQU8sRUFDTCxZQUFZLEdBQ2IsQ0FBQTtBQUVELG1CQUFtQjtBQUNuQixPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBRWpELE9BQU8sRUFBRSxRQUFRLEVBQUUsQ0FBQztBQUVwQix3QkFBd0I7QUFDeEIsT0FBTyxLQUFLLHFCQUFxQixNQUFNLDRCQUE0QixDQUFDO0FBRXBFLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxDQUFDO0FBRWpDLG9CQUFvQjtBQUNwQixPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUU3QixPQUFPLEVBQUUsSUFBSSxFQUFFLENBQUMifQ==
11
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicWVudi5wbHVnaW5zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvcWVudi5wbHVnaW5zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFNBQVM7QUFDVCxPQUFPLEtBQUssRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM5QixPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUU3QixPQUFPLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxDQUFDO0FBRXBCLG1CQUFtQjtBQUNuQixPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBRWpELE9BQU8sRUFBRSxRQUFRLEVBQUUsQ0FBQztBQUVwQixvQkFBb0I7QUFDcEIsT0FBTyxLQUFLLElBQUksTUFBTSxNQUFNLENBQUM7QUFFN0IsT0FBTyxFQUFFLElBQUksRUFBRSxDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/qenv",
3
- "version": "6.1.5",
3
+ "version": "8.0.0",
4
4
  "private": false,
5
5
  "description": "A module for easily handling environment variables in Node.js projects with support for .yml and .json configuration.",
6
6
  "main": "dist_ts/index.js",
@@ -22,18 +22,16 @@
22
22
  "author": "Task Venture Capital GmbH <hello@task.vc>",
23
23
  "license": "MIT",
24
24
  "bugs": {
25
- "url": "https://gitlab.com/pushrocks/qenv/issues"
25
+ "url": "https://code.foss.global/push.rocks/qenv/issues"
26
26
  },
27
27
  "homepage": "https://code.foss.global/push.rocks/qenv",
28
28
  "devDependencies": {
29
- "@git.zone/tsbuild": "^4.4.2",
29
+ "@git.zone/tsbuild": "^4.5.0",
30
30
  "@git.zone/tsrun": "^2.0.6",
31
- "@git.zone/tstest": "^4.0.0",
32
- "@types/node": "^26.4.1"
31
+ "@git.zone/tstest": "^6.1.1",
32
+ "@types/node": "^26.5.1"
33
33
  },
34
34
  "dependencies": {
35
- "@api.global/typedrequest": "^8.0.3",
36
- "@configvault.io/interfaces": "^1.0.17",
37
35
  "@push.rocks/smartlog": "^3.2.2",
38
36
  "@push.rocks/smartpath": "^6.0.0",
39
37
  "yaml": "^2.8.3"
@@ -56,7 +54,7 @@
56
54
  "last 1 chrome versions"
57
55
  ],
58
56
  "scripts": {
59
- "test": "(tstest test/ --verbose --testlog --timeout 20)",
57
+ "test": "(tstest test/ --verbose --logfile --timeout 20)",
60
58
  "build": "tsbuild --web",
61
59
  "format": "gitzone format",
62
60
  "buildDocs": "tsdoc"
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,10 @@ 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
+
64
70
  #### 2. Provide Values (`env.yml` or `env.json`)
65
71
 
66
72
  For local development, create an `env.yml` or `env.json` file:
@@ -97,6 +103,10 @@ Qenv loads variables in this order (first found wins):
97
103
  3. **Docker secrets** - From `/run/secrets/`
98
104
  4. **Docker secret JSON** - From `/run/secrets/secret.json`
99
105
 
106
+ All four sources are synchronous, so `getEnvVarOnDemand` and `getEnvVarOnDemandSync` resolve a name
107
+ identically. A `required:` entry is always a name; an async resolver function is a per-call source
108
+ and never takes part in the requirement check.
109
+
100
110
  ### Handling Complex Objects
101
111
 
102
112
  Store and retrieve complex configuration objects:
@@ -134,6 +144,9 @@ const fetchFromVault = async () => {
134
144
  const secret = await qenv.getEnvVarOnDemand(fetchFromVault);
135
145
  ```
136
146
 
147
+ The function form of `TEnvVarRef` is honoured by `getEnvVarOnDemand` and `getEnvVarOnDemandStrict`
148
+ only. `getEnvVarOnDemandSync` cannot await, so it accepts names and takes no resolver function.
149
+
137
150
  ### Working with Docker
138
151
 
139
152
  Qenv seamlessly integrates with Docker secrets:
@@ -168,9 +181,18 @@ const dbPassword = await qenv.getEnvVarOnDemand('db_password');
168
181
  Control how your application handles missing environment variables:
169
182
 
170
183
  ```typescript
171
- // Fail fast (default behavior)
172
- const qenvStrict = new Qenv('./', './', true);
173
- // Application exits if required variables are missing
184
+ import { Qenv, QenvMissingRequiredEnvVarsError } from '@push.rocks/qenv';
185
+
186
+ // Fail fast (default behaviour): the constructor throws
187
+ try {
188
+ const qenvStrict = new Qenv('./', './', true);
189
+ } catch (error) {
190
+ if (error instanceof QenvMissingRequiredEnvVarsError) {
191
+ console.error('Missing variables:', error.missingEnvVars);
192
+ console.error('Declared in:', error.qenvFilePathAbsolute);
193
+ }
194
+ throw error;
195
+ }
174
196
 
175
197
  // Graceful handling
176
198
  const qenvRelaxed = new Qenv('./', './', false);
@@ -183,6 +205,16 @@ if (qenvRelaxed.missingEnvVars.length > 0) {
183
205
  }
184
206
  ```
185
207
 
208
+ qenv never ends the process: the caller decides what an incomplete environment means. Where two
209
+ copies of qenv can end up in one dependency tree, `instanceof` is unreliable - match on the code
210
+ instead:
211
+
212
+ ```typescript
213
+ if (error instanceof Error && 'code' in error && error.code === 'QENV_MISSING_REQUIRED_ENV_VARS') {
214
+ // handle the incomplete environment
215
+ }
216
+ ```
217
+
186
218
  ### Strict Mode for Critical Variables
187
219
 
188
220
  Use the new strict getter when you absolutely need a variable:
@@ -191,15 +223,19 @@ Use the new strict getter when you absolutely need a variable:
191
223
  try {
192
224
  // This will throw if TOKEN is not set
193
225
  const token = await qenv.getEnvVarOnDemandStrict('TOKEN');
194
-
226
+
195
227
  // You can also check multiple fallback names
196
228
  const db = await qenv.getEnvVarOnDemandStrict(['DATABASE_URL', 'DB_CONNECTION']);
197
229
  } catch (error) {
230
+ // the message names every reference that could not be resolved
198
231
  console.error('Critical configuration missing:', error.message);
199
- process.exit(1);
232
+ throw error;
200
233
  }
201
234
  ```
202
235
 
236
+ A name that `qenv.yml` already lists under `required:` is resolved at construction time, so the
237
+ strict getter is for variables you look up on demand.
238
+
203
239
  ## 🏗️ CI/CD Integration
204
240
 
205
241
  ### GitHub Actions
@@ -276,12 +312,12 @@ qenv.logger.log('info', 'Custom log message');
276
312
  Here's how you might use qenv in a production Node.js application:
277
313
 
278
314
  ```typescript
279
- import { Qenv } from '@push.rocks/qenv';
315
+ import { Qenv, QenvMissingRequiredEnvVarsError } from '@push.rocks/qenv';
280
316
  import { createServer } from './server';
281
317
  import { connectDatabase } from './database';
282
318
 
283
319
  async function bootstrap() {
284
- // Initialize environment
320
+ // Initialize environment: throws when qenv.yml requires something no source provides
285
321
  const qenv = new Qenv();
286
322
 
287
323
  // Load critical configuration
@@ -304,7 +340,12 @@ async function bootstrap() {
304
340
  }
305
341
 
306
342
  bootstrap().catch(error => {
307
- console.error('Failed to start application:', error);
343
+ if (error instanceof QenvMissingRequiredEnvVarsError) {
344
+ console.error('Incomplete environment, missing:', error.missingEnvVars.join(', '));
345
+ } else {
346
+ console.error('Failed to start application:', error);
347
+ }
348
+ // exiting is the application's decision - qenv itself never calls process.exit
308
349
  process.exit(1);
309
350
  });
310
351
  ```
@@ -318,18 +359,21 @@ bootstrap().catch(error => {
318
359
  new Qenv(
319
360
  qenvFileBasePathArg?: string, // Path to qenv.yml (default: process.cwd())
320
361
  envFileBasePathArg?: string, // Path to env.yml/json (default: same as qenv)
321
- failOnMissing?: boolean // Exit on missing vars (default: true)
362
+ failOnMissing?: boolean // Throw on missing required vars (default: true)
322
363
  )
323
364
  ```
324
365
 
366
+ Throws `QenvMissingRequiredEnvVarsError` when `failOnMissing` is true and a name listed under
367
+ `required:` is not provided by any source.
368
+
325
369
  #### Methods
326
370
 
327
371
  | Method | Description | Returns |
328
372
  |--------|-------------|---------|
329
- | `getEnvVarOnDemand(name)` | Get environment variable value | `Promise<string \| undefined>` |
373
+ | `getEnvVarOnDemand(name)` | Get environment variable value, resolver functions included | `Promise<string \| undefined>` |
330
374
  | `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>` |
375
+ | `getEnvVarOnDemandSync(name)` | Synchronously get variable, names only | `string \| undefined` |
376
+ | `getEnvVarOnDemandAsObject(name)` | Get variable as decoded object | `Promise<unknown>` |
333
377
 
334
378
  #### Properties
335
379
 
@@ -338,7 +382,24 @@ new Qenv(
338
382
  | `requiredEnvVars` | `string[]` | List of required variable names |
339
383
  | `availableEnvVars` | `string[]` | List of found variable names |
340
384
  | `missingEnvVars` | `string[]` | List of missing variable names |
341
- | `keyValueObject` | `object` | All loaded variables as key-value pairs |
385
+ | `keyValueObject` | `Record<string, string>` | Every available required variable as a resolved string |
386
+ | `qenvFilePathAbsolute` | `string` | Absolute path of the qenv.yml in use |
387
+ | `envFilePathAbsolute` | `string \| undefined` | Absolute path of the env file in use |
388
+
389
+ ### Class: `QenvMissingRequiredEnvVarsError`
390
+
391
+ Thrown by the constructor when `failOnMissing` is true and a required variable has no source.
392
+
393
+ | Member | Type | Description |
394
+ |--------|------|-------------|
395
+ | `code` | `'QENV_MISSING_REQUIRED_ENV_VARS'` | Stable identifier, safe across duplicate installs |
396
+ | `missingEnvVars` | `string[]` | The required names no source provided |
397
+ | `requiredEnvVars` | `string[]` | Every name listed under `required:` |
398
+ | `qenvFilePathAbsolute` | `string` | The qenv.yml that declared them |
399
+
400
+ ## Issue Reporting and Security
401
+
402
+ 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
403
 
343
404
  ## License and Legal Information
344
405
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/qenv',
6
- version: '6.1.5',
6
+ version: '8.0.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,2 @@
1
1
  export * from './qenv.classes.qenv.js';
2
+ export * from './qenv.classes.missingrequiredenvvarserror.js';
@@ -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
+ }
@@ -1,27 +1,50 @@
1
- import { CloudlyAdapter } from './qenv.classes.configvaultadapter.js';
2
1
  import * as plugins from './qenv.plugins.js';
2
+ import { QenvMissingRequiredEnvVarsError } from './qenv.classes.missingrequiredenvvarserror.js';
3
3
 
4
+ /**
5
+ * a reference to an environment variable: its name, or an async function that produces the value.
6
+ * The function form is only honoured by the async getters; the synchronous getter accepts names.
7
+ */
4
8
  export type TEnvVarRef = string | (() => Promise<string>);
5
- type TKeyValueObject = Record<string, any>;
9
+
10
+ /** the resolved value of every available required env var, always a string */
11
+ export type TEnvVarValueMap = Record<string, string>;
12
+
13
+ /** a parsed qenv.yml, env file or secret.json, whose values are whatever the file declared */
14
+ type TParsedFileObject = Record<string, unknown>;
6
15
 
7
16
  export class Qenv {
17
+ /** the names listed under `required:` in qenv.yml */
8
18
  public requiredEnvVars: string[] = [];
19
+
20
+ /** the required names that a source provided */
9
21
  public availableEnvVars: string[] = [];
22
+
23
+ /** the required names that no source provided */
10
24
  public missingEnvVars: string[] = [];
11
- public keyValueObject: TKeyValueObject = {};
12
- public logger = new plugins.smartlog.ConsoleLog();
13
25
 
14
- public cloudlyAdapter: CloudlyAdapter;
26
+ /** the resolved value of every available required name */
27
+ public keyValueObject: TEnvVarValueMap = {};
28
+
29
+ public logger = new plugins.smartlog.ConsoleLog();
15
30
 
16
31
  public qenvFilePathAbsolute = '';
17
32
  public envFilePathAbsolute?: string;
18
33
 
34
+ /**
35
+ * Resolves every name listed under `required:` in qenv.yml while constructing.
36
+ * @param qenvFileBasePathArg directory that holds qenv.yml
37
+ * @param envFileBasePathArg directory that holds env.json, env.yml or env.yaml
38
+ * @param failOnMissing throws a QenvMissingRequiredEnvVarsError when a variable listed under
39
+ * `required:` in qenv.yml is not provided by any source. Pass false to only record the names in
40
+ * `missingEnvVars` and continue.
41
+ * @throws QenvMissingRequiredEnvVarsError
42
+ */
19
43
  constructor(
20
44
  qenvFileBasePathArg: string = process.cwd(),
21
45
  envFileBasePathArg?: string,
22
46
  failOnMissing: boolean = true
23
47
  ) {
24
- this.cloudlyAdapter = new CloudlyAdapter();
25
48
  this.initializeFilePaths(qenvFileBasePathArg, envFileBasePathArg);
26
49
  this.loadRequiredEnvVars();
27
50
  this.loadAvailableEnvVars();
@@ -33,18 +56,18 @@ export class Qenv {
33
56
  plugins.path.resolve(qenvFileBasePathArg),
34
57
  'qenv.yml'
35
58
  );
36
-
59
+
37
60
  if (envFileBasePathArg) {
38
61
  const envFileBasePath = plugins.path.resolve(envFileBasePathArg);
39
-
62
+
40
63
  const envFileJsonPath = plugins.path.join(envFileBasePath, 'env.json');
41
64
  const envFileYmlPath = plugins.path.join(envFileBasePath, 'env.yml');
42
65
  const envFileYamlPath = plugins.path.join(envFileBasePath, 'env.yaml');
43
-
66
+
44
67
  const envFileJsonExists = this.fileExists(envFileJsonPath);
45
68
  const envFileYmlExists = this.fileExists(envFileYmlPath);
46
69
  const envFileYamlExists = this.fileExists(envFileYamlPath);
47
-
70
+
48
71
  if (envFileJsonExists && (envFileYmlExists || envFileYamlExists)) {
49
72
  this.logger.log('warn', 'Both env.json and env.yml files exist! Using env.json');
50
73
  this.envFilePathAbsolute = envFileJsonPath;
@@ -59,22 +82,28 @@ export class Qenv {
59
82
  }
60
83
 
61
84
  private loadRequiredEnvVars() {
62
- if (this.fileExists(this.qenvFilePathAbsolute)) {
63
- const qenvFile = this.readObjectFromFile(this.qenvFilePathAbsolute);
64
- const requiredEnvVars = qenvFile.required;
65
- if (Array.isArray(requiredEnvVars)) {
66
- this.requiredEnvVars.push(
67
- ...requiredEnvVars.filter((envVar): envVar is string => typeof envVar === 'string')
68
- );
69
- } else {
70
- this.logger.log('warn', 'qenv.yml does not contain a "required" Array!');
71
- }
85
+ if (!this.fileExists(this.qenvFilePathAbsolute)) {
86
+ return;
72
87
  }
88
+ const qenvFile = this.readObjectFromFile(this.qenvFilePathAbsolute);
89
+ const declaredRequirements = qenvFile['required'];
90
+ if (!Array.isArray(declaredRequirements)) {
91
+ this.logger.log('warn', 'qenv.yml does not contain a "required" Array!');
92
+ return;
93
+ }
94
+ const declaredEntries: unknown[] = declaredRequirements;
95
+ this.requiredEnvVars.push(
96
+ ...declaredEntries.filter((entry): entry is string => typeof entry === 'string')
97
+ );
73
98
  }
74
99
 
75
100
  private loadAvailableEnvVars() {
101
+ // resolved synchronously: a required env var is always a name, and every source a name can come
102
+ // from - process environment, env file, Docker secrets - is a synchronous read. Resolving it
103
+ // here as a promise would store a pending promise instead of a value and make the check below
104
+ // pass for every name.
76
105
  for (const envVar of this.requiredEnvVars) {
77
- const value = this.getEnvVarOnDemand(envVar);
106
+ const value = this.tryGetEnvVarSync(envVar);
78
107
  if (value !== undefined) {
79
108
  this.availableEnvVars.push(envVar);
80
109
  this.keyValueObject[envVar] = value;
@@ -87,18 +116,30 @@ export class Qenv {
87
116
  (envVar) => !this.availableEnvVars.includes(envVar)
88
117
  );
89
118
 
90
- if (this.missingEnvVars.length > 0) {
91
- console.info('Required Env Vars are:', this.requiredEnvVars);
92
- console.error('Missing Env Vars:', this.missingEnvVars);
93
- if (failOnMissing) {
94
- this.logger.log('error', 'Exiting due to missing env vars!');
95
- process.exit(1);
96
- } else {
97
- this.logger.log('warn', 'qenv is not set to fail on missing environment variables');
98
- }
119
+ if (this.missingEnvVars.length === 0) {
120
+ return;
121
+ }
122
+
123
+ if (failOnMissing) {
124
+ // a library must not end the process: the caller decides what an incomplete environment means
125
+ throw new QenvMissingRequiredEnvVarsError({
126
+ missingEnvVars: this.missingEnvVars,
127
+ requiredEnvVars: this.requiredEnvVars,
128
+ qenvFilePathAbsolute: this.qenvFilePathAbsolute,
129
+ });
99
130
  }
131
+
132
+ this.logger.log(
133
+ 'warn',
134
+ `qenv is not set to fail on missing environment variables. Missing: ${this.missingEnvVars.join(', ')}`
135
+ );
100
136
  }
101
137
 
138
+ /**
139
+ * Resolves an env var from the process environment, the env file and the Docker secrets, in that
140
+ * order. An array is tried left to right and the first defined value wins.
141
+ * @param envVarNameOrNames a name, an async resolver function, or a list of either
142
+ */
102
143
  public async getEnvVarOnDemand(
103
144
  envVarNameOrNames: TEnvVarRef | TEnvVarRef[]
104
145
  ): Promise<string | undefined> {
@@ -116,23 +157,25 @@ export class Qenv {
116
157
  }
117
158
 
118
159
  /**
119
- * Like getEnvVarOnDemand, but throws an error if the env var is not set.
120
- * @param envVarNameOrNames
121
- * @returns
160
+ * Like getEnvVarOnDemand, but throws when no source provides a value.
161
+ * @param envVarNameOrNames a name, an async resolver function, or a list of either
122
162
  */
123
163
  public async getEnvVarOnDemandStrict(
124
164
  envVarNameOrNames: TEnvVarRef | TEnvVarRef[]
125
165
  ): Promise<string> {
126
166
  const value = await this.getEnvVarOnDemand(envVarNameOrNames);
127
167
  if (value === undefined) {
128
- throw new Error(`Env var ${envVarNameOrNames} is not set!`);
168
+ throw new Error(`Env var ${this.describeEnvVarRefs(envVarNameOrNames)} is not set!`);
129
169
  }
130
170
  return value;
131
171
  }
132
172
 
173
+ /**
174
+ * The synchronous counterpart of getEnvVarOnDemand. It resolves names only: an async resolver
175
+ * function cannot be awaited here, so the function form of TEnvVarRef is not accepted.
176
+ * @param envVarNameOrNames a name or a list of names
177
+ */
133
178
  public getEnvVarOnDemandSync(envVarNameOrNames: string | string[]): string | undefined {
134
- console.warn('requesting env var sync leaves out potentially important async env sources.');
135
-
136
179
  if (Array.isArray(envVarNameOrNames)) {
137
180
  for (const envVarName of envVarNameOrNames) {
138
181
  const value = this.tryGetEnvVarSync(envVarName);
@@ -146,7 +189,14 @@ export class Qenv {
146
189
  }
147
190
  }
148
191
 
149
- public async getEnvVarOnDemandAsObject(envVarNameOrNames: string | string[]): Promise<any> {
192
+ /**
193
+ * Resolves an env var whose value was stored as a base64 encoded object and decodes it. A plain
194
+ * value is returned as the string it is, so the caller narrows what it gets.
195
+ * @param envVarNameOrNames a name or a list of names
196
+ */
197
+ public async getEnvVarOnDemandAsObject(
198
+ envVarNameOrNames: string | string[]
199
+ ): Promise<unknown> {
150
200
  const rawValue = await this.getEnvVarOnDemand(envVarNameOrNames);
151
201
  if (rawValue && rawValue.startsWith('base64Object:')) {
152
202
  const base64Part = rawValue.split('base64Object:')[1];
@@ -160,39 +210,43 @@ export class Qenv {
160
210
  return await envVarRefArg();
161
211
  }
162
212
 
163
- const sources = [
164
- this.getFromEnvironmentVariable(envVarRefArg),
165
- this.getFromEnvYamlOrJsonFile(envVarRefArg),
166
- this.getFromDockerSecret(envVarRefArg),
167
- this.getFromDockerSecretJson(envVarRefArg)
168
- ];
169
-
170
- for (const value of sources) {
171
- if (value !== undefined) {
172
- return value;
173
- }
174
- }
175
-
176
- return undefined;
213
+ // a name resolves from synchronous sources only, so both getters share one resolution order
214
+ return this.tryGetEnvVarSync(envVarRefArg);
177
215
  }
178
216
 
179
217
  private tryGetEnvVarSync(envVarName: string): string | undefined {
180
- const sources = [
181
- this.getFromEnvironmentVariable(envVarName),
182
- this.getFromEnvYamlOrJsonFile(envVarName),
183
- this.getFromDockerSecret(envVarName),
184
- this.getFromDockerSecretJson(envVarName)
218
+ // read lazily: a source is only touched once every earlier one came back undefined, so a name
219
+ // the process environment answers never opens a secret file, and a malformed secret.json only
220
+ // ever affects the names that actually reach it
221
+ const sources: Array<() => string | undefined> = [
222
+ () => this.getFromEnvironmentVariable(envVarName),
223
+ () => this.getFromEnvYamlOrJsonFile(envVarName),
224
+ () => this.getFromDockerSecret(envVarName),
225
+ () => this.getFromDockerSecretJson(envVarName),
185
226
  ];
186
-
187
- for (const value of sources) {
227
+
228
+ for (const readSource of sources) {
229
+ const value = readSource();
188
230
  if (value !== undefined) {
189
231
  return value;
190
232
  }
191
233
  }
192
-
234
+
193
235
  return undefined;
194
236
  }
195
237
 
238
+ /** renders env var references for an error message, naming a resolver function where it has one */
239
+ private describeEnvVarRefs(envVarNameOrNames: TEnvVarRef | TEnvVarRef[]): string {
240
+ const envVarRefs = Array.isArray(envVarNameOrNames) ? envVarNameOrNames : [envVarNameOrNames];
241
+ return envVarRefs
242
+ .map((envVarRef) =>
243
+ typeof envVarRef === 'function'
244
+ ? `${envVarRef.name || 'anonymous'}()`
245
+ : envVarRef
246
+ )
247
+ .join(', ');
248
+ }
249
+
196
250
  private getFromEnvironmentVariable(envVarName: string): string | undefined {
197
251
  return process.env[envVarName];
198
252
  }
@@ -216,8 +270,16 @@ export class Qenv {
216
270
  }
217
271
  }
218
272
 
273
+ /**
274
+ * the directory Docker mounts secrets into. It is a method so a test can point both secret
275
+ * readers at a directory it is allowed to create; a process can never write /run/secrets itself.
276
+ */
277
+ protected getDockerSecretsDirectoryPath(): string {
278
+ return '/run/secrets';
279
+ }
280
+
219
281
  private getFromDockerSecret(envVarName: string): string | undefined {
220
- const secretPath = `/run/secrets/${envVarName}`;
282
+ const secretPath = plugins.path.join(this.getDockerSecretsDirectoryPath(), envVarName);
221
283
  if (this.fileExists(secretPath)) {
222
284
  return plugins.fs.readFileSync(secretPath, 'utf8');
223
285
  }
@@ -225,11 +287,14 @@ export class Qenv {
225
287
  }
226
288
 
227
289
  private getFromDockerSecretJson(envVarName: string): string | undefined {
228
- if (this.directoryExists('/run/secrets')) {
229
- const availableSecrets = plugins.fs.readdirSync('/run/secrets');
290
+ const secretsDirectoryPath = this.getDockerSecretsDirectoryPath();
291
+ if (this.directoryExists(secretsDirectoryPath)) {
292
+ const availableSecrets = plugins.fs.readdirSync(secretsDirectoryPath);
230
293
  for (const secret of availableSecrets) {
231
294
  if (secret.includes('secret.json')) {
232
- const secretObject = this.readObjectFromFile(`/run/secrets/${secret}`);
295
+ const secretObject = this.readObjectFromFile(
296
+ plugins.path.join(secretsDirectoryPath, secret)
297
+ );
233
298
  const value = secretObject[envVarName];
234
299
  if (value === undefined) {
235
300
  continue;
@@ -244,12 +309,12 @@ export class Qenv {
244
309
  return undefined;
245
310
  }
246
311
 
247
- private encodeBase64(data: any): string {
312
+ private encodeBase64(data: unknown): string {
248
313
  const jsonString = JSON.stringify(data);
249
314
  return Buffer.from(jsonString).toString('base64');
250
315
  }
251
316
 
252
- private decodeBase64(encodedString: string): any {
317
+ private decodeBase64(encodedString: string): unknown {
253
318
  const decodedString = Buffer.from(encodedString, 'base64').toString('utf-8');
254
319
  return JSON.parse(decodedString);
255
320
  }
@@ -269,11 +334,13 @@ export class Qenv {
269
334
  }
270
335
  }
271
336
 
272
- private readObjectFromFile(filePath: string): TKeyValueObject {
337
+ private readObjectFromFile(filePath: string): TParsedFileObject {
273
338
  const fileString = plugins.fs.readFileSync(filePath, 'utf8');
274
- const parsedObject = filePath.endsWith('.json')
339
+ const parsedObject: unknown = filePath.endsWith('.json')
275
340
  ? JSON.parse(fileString)
276
341
  : plugins.yaml.parse(fileString);
277
- return typeof parsedObject === 'object' && parsedObject !== null ? parsedObject : {};
342
+ return typeof parsedObject === 'object' && parsedObject !== null
343
+ ? (parsedObject as TParsedFileObject)
344
+ : {};
278
345
  }
279
346
  }
@@ -4,23 +4,11 @@ import * as path from 'path';
4
4
 
5
5
  export { fs, path };
6
6
 
7
- // @api.global scope
8
- import * as typedrequest from '@api.global/typedrequest';
9
-
10
- export {
11
- typedrequest,
12
- }
13
-
14
7
  // @pushrocks scope
15
8
  import * as smartlog from '@push.rocks/smartlog';
16
9
 
17
10
  export { smartlog };
18
11
 
19
- // @configvault.io scope
20
- import * as configvaultInterfaces from '@configvault.io/interfaces';
21
-
22
- export { configvaultInterfaces };
23
-
24
12
  // third party scope
25
13
  import * as yaml from 'yaml';
26
14
 
@@ -1,6 +0,0 @@
1
- import * as plugins from './qenv.plugins.js';
2
- export declare class CloudlyAdapter {
3
- configVaultUrl?: string;
4
- constructor(configVaultUrl?: string);
5
- getConfigBundle(): Promise<plugins.configvaultInterfaces.data.IEnvBundle | null>;
6
- }
@@ -1,24 +0,0 @@
1
- import * as plugins from './qenv.plugins.js';
2
- export class CloudlyAdapter {
3
- constructor(configVaultUrl) {
4
- this.configVaultUrl = configVaultUrl;
5
- }
6
- async getConfigBundle() {
7
- if (this.configVaultUrl) {
8
- console.log(`ConfigVault specified through constructor`);
9
- }
10
- else if (process.env['CONFIGVAULT_URL']) {
11
- this.configVaultUrl = process.env['CONFIGVAULT_URL'];
12
- }
13
- else {
14
- return null;
15
- }
16
- const parsedUrl = new URL(this.configVaultUrl);
17
- const tr = new plugins.typedrequest.TypedRequest(`${parsedUrl.host}/typedrequest`, 'getEnvBundle');
18
- const response = await tr.fire({
19
- authorization: parsedUrl.pathname.replace('/', ''),
20
- });
21
- return response.envBundle;
22
- }
23
- }
24
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicWVudi5jbGFzc2VzLmNvbmZpZ3ZhdWx0YWRhcHRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3FlbnYuY2xhc3Nlcy5jb25maWd2YXVsdGFkYXB0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxtQkFBbUIsQ0FBQztBQUU3QyxNQUFNLE9BQU8sY0FBYztJQUd6QixZQUFZLGNBQXVCO1FBQ2pDLElBQUksQ0FBQyxjQUFjLEdBQUcsY0FBYyxDQUFDO0lBQ3ZDLENBQUM7SUFFTSxLQUFLLENBQUMsZUFBZTtRQUMxQixJQUFJLElBQUksQ0FBQyxjQUFjLEVBQUUsQ0FBQztZQUN4QixPQUFPLENBQUMsR0FBRyxDQUFDLDJDQUEyQyxDQUFDLENBQUE7UUFDMUQsQ0FBQzthQUFNLElBQUksT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxFQUFFLENBQUM7WUFDMUMsSUFBSSxDQUFDLGNBQWMsR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLGlCQUFpQixDQUFDLENBQUM7UUFDdkQsQ0FBQzthQUFNLENBQUM7WUFDTixPQUFPLElBQUksQ0FBQztRQUNkLENBQUM7UUFFRCxNQUFNLFNBQVMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxJQUFJLENBQUMsY0FBYyxDQUFDLENBQUM7UUFFL0MsTUFBTSxFQUFFLEdBQ04sSUFBSSxPQUFPLENBQUMsWUFBWSxDQUFDLFlBQVksQ0FDbkMsR0FBRyxTQUFTLENBQUMsSUFBSSxlQUFlLEVBQ2hDLGNBQWMsQ0FDZixDQUFDO1FBQ0osTUFBTSxRQUFRLEdBQUcsTUFBTSxFQUFFLENBQUMsSUFBSSxDQUFDO1lBQzdCLGFBQWEsRUFBRSxTQUFTLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDO1NBQ25ELENBQUMsQ0FBQTtRQUNGLE9BQU8sUUFBUSxDQUFDLFNBQVMsQ0FBQztJQUM1QixDQUFDO0NBQ0YifQ==
package/readme.hints.md DELETED
@@ -1 +0,0 @@
1
-
@@ -1,31 +0,0 @@
1
- import * as plugins from './qenv.plugins.js';
2
-
3
- export class CloudlyAdapter {
4
- public configVaultUrl?: string;
5
-
6
- constructor(configVaultUrl?: string) {
7
- this.configVaultUrl = configVaultUrl;
8
- }
9
-
10
- public async getConfigBundle(): Promise<plugins.configvaultInterfaces.data.IEnvBundle | null> {
11
- if (this.configVaultUrl) {
12
- console.log(`ConfigVault specified through constructor`)
13
- } else if (process.env['CONFIGVAULT_URL']) {
14
- this.configVaultUrl = process.env['CONFIGVAULT_URL'];
15
- } else {
16
- return null;
17
- }
18
-
19
- const parsedUrl = new URL(this.configVaultUrl);
20
-
21
- const tr =
22
- new plugins.typedrequest.TypedRequest<plugins.configvaultInterfaces.requests.IReq_GetEnvBundle>(
23
- `${parsedUrl.host}/typedrequest`,
24
- 'getEnvBundle'
25
- );
26
- const response = await tr.fire({
27
- authorization: parsedUrl.pathname.replace('/', ''),
28
- })
29
- return response.envBundle;
30
- }
31
- }