< Summary - Envilder Core (TypeScript)

Information
Class: src/envilder/core/infrastructure/azure/AzureKeyVaultSecretProvider.ts
Assembly: Default
File(s): src/envilder/core/infrastructure/azure/AzureKeyVaultSecretProvider.ts
Tag: 151_24479375065
Line coverage
97%
Covered lines: 37
Uncovered lines: 1
Coverable lines: 38
Total lines: 118
Line coverage: 97.3%
Branch coverage
96%
Covered branches: 25
Total branches: 26
Branch coverage: 96.1%
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';
 9
 910@injectable()
 911export class AzureKeyVaultSecretProvider implements ISecretProvider {
 12  private client: SecretClient;
 3813  private normalizedNameRegistry = new Map<string, string>();
 14
 15  constructor(client: SecretClient) {
 3816    this.client = client;
 17  }
 18
 19  async getSecret(name: string): Promise<string | undefined> {
 2620    const secretName = this.resolveSecretName(name);
 2621    try {
 2622      const secret = await this.client.getSecret(secretName);
 1623      return secret?.value ?? undefined;
 24    } catch (error) {
 425      if (
 26        typeof error === 'object' &&
 27        error !== null &&
 28        'statusCode' in error &&
 29        error.statusCode === 404
 30      ) {
 231        return undefined;
 32      }
 33      const errorMessage =
 234        error instanceof Error ? error.message : String(error);
 435      throw new SecretOperationError(
 36        `Failed to get secret ${EnvironmentVariable.maskSecretPath(name)}: ${errorMessage}`,
 37      );
 38    }
 39  }
 40
 41  async setSecret(name: string, value: string): Promise<void> {
 942    const secretName = this.resolveSecretName(name);
 943    await this.client.setSecret(secretName, value);
 44  }
 45
 46  /**
 47   * Validates that the secret name meets Azure Key Vault naming constraints.
 48   * @see https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#objects-identifiers-
 49   */
 50  private validateSecretName(name: string): void {
 3551    if (name.trim().length === 0) {
 152      throw new InvalidArgumentError(
 53        'Invalid secret name: name cannot be empty or whitespace-only.',
 54      );
 55    }
 3456    if (/[^a-zA-Z0-9\-_/]/.test(name)) {
 257      throw new InvalidArgumentError(
 58        `Invalid secret name '${name}': contains characters not allowed` +
 59          ' by Azure Key Vault. Only alphanumeric characters,' +
 60          ' hyphens, slashes, and underscores are accepted.',
 61      );
 62    }
 63  }
 64
 65  private resolveSecretName(originalName: string): string {
 3566    this.validateSecretName(originalName);
 3567    const normalized = this.normalizeSecretName(originalName);
 3568    if (normalized.length > 127) {
 169      throw new InvalidArgumentError(
 70        `Invalid secret name '${originalName}': normalized name '${normalized}' exceeds the 127-character limit for Azur
 71      );
 72    }
 3173    const existing = this.normalizedNameRegistry.get(normalized);
 3174    if (existing !== undefined && existing !== originalName) {
 375      throw new SecretOperationError(
 76        `Secret name collision: '${originalName}' and '${existing}' ` +
 77          `both normalize to '${normalized}'. Use distinct ` +
 78          'Key Vault-compatible names in your map file ' +
 79          'when targeting Azure.',
 80      );
 81    }
 2882    this.normalizedNameRegistry.set(normalized, originalName);
 2883    return normalized;
 84  }
 85
 86  // Azure Key Vault secret names: 1-127 chars, alphanumeric + hyphens, start with letter
 87  private normalizeSecretName(name: string): string {
 88    // Remove leading slashes
 3289    let normalized = name.replace(/^\/+/, '');
 90
 91    // Replace slashes and underscores with hyphens
 3292    normalized = normalized.replace(/[/_]/g, '-');
 93
 94    // Lowercase to match Azure Key Vault case-insensitivity
 3295    normalized = normalized.toLowerCase();
 96
 97    // Remove invalid characters
 3298    normalized = normalized.replace(/[^a-zA-Z0-9-]/g, '');
 99
 100    // Remove consecutive hyphens
 32101    normalized = normalized.replace(/-+/g, '-');
 102
 103    // Remove leading/trailing hyphens
 32104    normalized = normalized.replace(/^-+|-+$/g, '');
 105
 106    // Ensure starts with a letter
 32107    if (normalized.length > 0 && !/^[a-zA-Z]/.test(normalized)) {
 1108      normalized = `secret-${normalized}`;
 109    }
 110
 111    // Default name if empty
 32112    if (normalized.length === 0) {
 0113      normalized = 'secret';
 114    }
 115
 32116    return normalized;
 117  }
 118}