< Summary - Envilder CLI

Information
Class: src/envilder/core/infrastructure/azure/AzureKeyVaultSecretProvider.ts
Assembly: Default
File(s): src/envilder/core/infrastructure/azure/AzureKeyVaultSecretProvider.ts
Tag: 427_29134414720
Line coverage
97%
Covered lines: 36
Uncovered lines: 1
Coverable lines: 37
Total lines: 117
Line coverage: 97.2%
Branch coverage
95%
Covered branches: 23
Total branches: 24
Branch coverage: 95.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/envilder/core/infrastructure/azure/AzureKeyVaultSecretProvider.ts

#LineLine coverage
 1import type { SecretClient } from '@azure/keyvault-secrets';
 2import { injectable } from 'inversify';
 3import { EnvironmentVariable } from '../../domain/EnvironmentVariable.js';
 4import {
 5  InvalidArgumentError,
 6  SecretOperationError,
 7} from '../../domain/errors/DomainErrors.js';
 8import type { ISecretProvider } from '../../domain/ports/ISecretProvider.js';
 9import { describeError } from '../describeError.js';
 10
 911@injectable()
 912export class AzureKeyVaultSecretProvider implements ISecretProvider {
 13  private client: SecretClient;
 3914  private normalizedNameRegistry = new Map<string, string>();
 15
 16  constructor(client: SecretClient) {
 3917    this.client = client;
 18  }
 19
 20  async getSecret(name: string): Promise<string | undefined> {
 2721    const secretName = this.resolveSecretName(name);
 2722    try {
 2723      const secret = await this.client.getSecret(secretName);
 1624      return secret?.value ?? undefined;
 25    } catch (error) {
 526      if (
 27        typeof error === 'object' &&
 28        error !== null &&
 29        'statusCode' in error &&
 30        error.statusCode === 404
 31      ) {
 232        return undefined;
 33      }
 334      throw new SecretOperationError(
 35        `${EnvironmentVariable.maskSecretPath(name)}: ${describeError(error)}`,
 36      );
 37    }
 38  }
 39
 40  async setSecret(name: string, value: string): Promise<void> {
 941    const secretName = this.resolveSecretName(name);
 942    await this.client.setSecret(secretName, value);
 43  }
 44
 45  /**
 46   * Validates that the secret name meets Azure Key Vault naming constraints.
 47   * @see https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#objects-identifiers-
 48   */
 49  private validateSecretName(name: string): void {
 3650    if (name.trim().length === 0) {
 151      throw new InvalidArgumentError(
 52        'Invalid secret name: name cannot be empty or whitespace-only.',
 53      );
 54    }
 3555    if (/[^a-zA-Z0-9\-_/]/.test(name)) {
 256      throw new InvalidArgumentError(
 57        `Invalid secret name '${name}': contains characters not allowed` +
 58          ' by Azure Key Vault. Only alphanumeric characters,' +
 59          ' hyphens, slashes, and underscores are accepted.',
 60      );
 61    }
 62  }
 63
 64  private resolveSecretName(originalName: string): string {
 3665    this.validateSecretName(originalName);
 3666    const normalized = this.normalizeSecretName(originalName);
 3667    if (normalized.length > 127) {
 168      throw new InvalidArgumentError(
 69        `Invalid secret name '${originalName}': normalized name '${normalized}' exceeds the 127-character limit for Azur
 70      );
 71    }
 3272    const existing = this.normalizedNameRegistry.get(normalized);
 3273    if (existing !== undefined && existing !== originalName) {
 374      throw new SecretOperationError(
 75        `Secret name collision: '${originalName}' and '${existing}' ` +
 76          `both normalize to '${normalized}'. Use distinct ` +
 77          'Key Vault-compatible names in your map file ' +
 78          'when targeting Azure.',
 79      );
 80    }
 2981    this.normalizedNameRegistry.set(normalized, originalName);
 2982    return normalized;
 83  }
 84
 85  // Azure Key Vault secret names: 1-127 chars, alphanumeric + hyphens, start with letter
 86  private normalizeSecretName(name: string): string {
 87    // Remove leading slashes
 3388    let normalized = name.replace(/^\/+/, '');
 89
 90    // Replace slashes and underscores with hyphens
 3391    normalized = normalized.replace(/[/_]/g, '-');
 92
 93    // Lowercase to match Azure Key Vault case-insensitivity
 3394    normalized = normalized.toLowerCase();
 95
 96    // Remove invalid characters
 3397    normalized = normalized.replace(/[^a-zA-Z0-9-]/g, '');
 98
 99    // Remove consecutive hyphens
 33100    normalized = normalized.replace(/-+/g, '-');
 101
 102    // Remove leading/trailing hyphens
 33103    normalized = normalized.replace(/^-+|-+$/g, '');
 104
 105    // Ensure starts with a letter
 33106    if (normalized.length > 0 && !/^[a-zA-Z]/.test(normalized)) {
 1107      normalized = `secret-${normalized}`;
 108    }
 109
 110    // Default name if empty
 33111    if (normalized.length === 0) {
 0112      normalized = 'secret';
 113    }
 114
 33115    return normalized;
 116  }
 117}