< Summary - Envilder Node.js SDK

Information
Class: src/sdks/nodejs/src/application/envilder.ts
Assembly: Default
File(s): src/sdks/nodejs/src/application/envilder.ts
Tag: 299_25910610327
Line coverage
94%
Covered lines: 50
Uncovered lines: 3
Coverable lines: 53
Total lines: 184
Line coverage: 94.3%
Branch coverage
82%
Covered branches: 23
Total branches: 28
Branch coverage: 82.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/sdks/nodejs/src/application/envilder.ts

#LineLine coverage
 1import { readFile } from 'node:fs/promises';
 2import type { EnvilderOptions } from '../domain/envilder-options.js';
 3import type { SecretProviderType } from '../domain/secret-provider-type.js';
 4import { createSecretProvider } from '../infrastructure/secret-provider-factory.js';
 5import { EnvilderClient } from './envilder-client.js';
 6import { MapFileParser } from './map-file-parser.js';
 7
 8/**
 9 * Facade for loading secrets from cloud providers.
 10 *
 11 * Supports loading from a single map file or from an
 12 * environment-based mapping that routes each environment name
 13 * to its own map file (or `null` to skip).
 14 *
 15 * @example
 16 * ```typescript
 17 * // One-liner — resolve + inject into process.env:
 18 * await Envilder.load('envilder.json');
 19 *
 20 * // Resolve without injecting:
 21 * const secrets = await Envilder.resolveFile('envilder.json');
 22 *
 23 * // Fluent builder with provider override:
 24 * const secrets = await Envilder.fromMapFile('envilder.json')
 25 *   .withProvider(SecretProviderType.Azure)
 26 *   .withVaultUrl('https://my-vault.vault.azure.net')
 27 *   .inject();
 28 * ```
 29 */
 30export class Envilder {
 31  private readonly filePath: string;
 732  private options: EnvilderOptions = {};
 33
 34  private constructor(filePath: string) {
 735    this.filePath = filePath;
 36  }
 37
 38  /**
 39   * Returns a fluent builder bound to the given map file.
 40   *
 41   * Chain `.withProvider()`, `.withVaultUrl()`, or `.withProfile()`
 42   * before calling `.resolve()` or `.inject()`.
 43   */
 44  static fromMapFile(filePath: string): Envilder {
 445    validateFilePath(filePath);
 446    return new Envilder(filePath.trim());
 47  }
 48
 49  /**
 50   * Resolves secrets and injects them into `process.env`.
 51   *
 52   * Can be called in two ways:
 53   * - `load(filePath)` — load from a single map file
 54   * - `load(env, envMapping)` — look up env in the mapping
 55   */
 56  static async load(
 57    filePathOrEnv: string,
 58    envMapping?: Record<string, string | null>,
 59  ): Promise<Map<string, string>> {
 860    if (envMapping !== undefined) {
 561      const source = resolveEnvSource(filePathOrEnv, envMapping);
 562      if (source === null) {
 263        return new Map();
 64      }
 165      return new Envilder(source).inject();
 66    }
 67
 368    validateFilePath(filePathOrEnv);
 369    return new Envilder(filePathOrEnv.trim()).inject();
 70  }
 71
 72  /**
 73   * Resolves secrets without injecting into `process.env`.
 74   *
 75   * Can be called in two ways:
 76   * - `resolveFile(filePath)` — resolve from a single map file
 77   * - `resolveFile(env, envMapping)` — look up env in the mapping
 78   */
 79  static async resolveFile(
 80    filePathOrEnv: string,
 81    envMapping?: Record<string, string | null>,
 82  ): Promise<Map<string, string>> {
 483    if (envMapping !== undefined) {
 384      const source = resolveEnvSource(filePathOrEnv, envMapping);
 385      if (source === null) {
 286        return new Map();
 87      }
 188      return new Envilder(source).resolve();
 89    }
 90
 191    validateFilePath(filePathOrEnv);
 192    return new Envilder(filePathOrEnv.trim()).resolve();
 93  }
 94
 95  /** Override the secret provider (AWS or Azure). */
 96  withProvider(provider: SecretProviderType): Envilder {
 197    this.options.provider = provider;
 198    return this;
 99  }
 100
 101  /** Override the Azure Key Vault URL. */
 102  withVaultUrl(vaultUrl: string): Envilder {
 1103    this.options.vaultUrl = vaultUrl;
 1104    return this;
 105  }
 106
 107  /** Override the AWS named profile. */
 108  withProfile(profile: string): Envilder {
 1109    this.options.profile = profile;
 1110    return this;
 111  }
 112
 113  /** Resolve secrets and return them as a Map. */
 114  async resolve(): Promise<Map<string, string>> {
 7115    const mapFile = await this.parseFile();
 7116    const options = this.buildOptions();
 7117    const provider = createSecretProvider(mapFile.config, options);
 7118    const client = new EnvilderClient(provider);
 7119    return client.resolveSecrets(mapFile);
 120  }
 121
 122  /** Resolve secrets, inject into `process.env`, and return them. */
 123  async inject(): Promise<Map<string, string>> {
 3124    const secrets = await this.resolve();
 3125    EnvilderClient.injectIntoEnvironment(secrets);
 3126    return secrets;
 127  }
 128
 129  private async parseFile() {
 130    let json: string;
 7131    try {
 7132      json = await readFile(this.filePath, 'utf-8');
 133    } catch (err: unknown) {
 0134      if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
 0135        throw new Error(`Map file not found: ${this.filePath}`);
 136      }
 0137      throw err;
 138    }
 7139    return new MapFileParser().parse(json);
 140  }
 141
 142  private buildOptions(): EnvilderOptions | undefined {
 143    const hasOverrides =
 7144      this.options.provider !== undefined ||
 145      this.options.vaultUrl !== undefined ||
 146      this.options.profile !== undefined;
 7147    return hasOverrides ? this.options : undefined;
 148  }
 149}
 150
 151function validateFilePath(filePath: string): void {
 8152  if (!filePath?.trim()) {
 3153    throw new Error('file path cannot be empty');
 154  }
 155}
 156
 157function resolveEnvSource(
 158  env: string,
 159  envMapping: Record<string, string | null>,
 160): string | null {
 8161  if (!env?.trim()) {
 1162    throw new Error('env cannot be empty');
 163  }
 164
 7165  const normalized = env.trim();
 166
 7167  if (!Object.hasOwn(envMapping, normalized)) {
 2168    return null;
 169  }
 170
 5171  const source = envMapping[normalized];
 172
 5173  if (source === null) {
 2174    return null;
 175  }
 176
 3177  if (!source.trim()) {
 1178    throw new Error(
 179      `envMapping contains an empty file path for environment '${normalized}'.`,
 180    );
 181  }
 182
 2183  return source.trim();
 184}