| | | 1 | | import type { SecretClient } from '@azure/keyvault-secrets'; |
| | | 2 | | import type { ISecretProvider } from '../../domain/ports/secret-provider.js'; |
| | | 3 | | |
| | | 4 | | /** |
| | | 5 | | * {@link ISecretProvider} backed by Azure Key Vault. |
| | | 6 | | * |
| | | 7 | | * Secrets are fetched in parallel. Secrets that return HTTP 404 |
| | | 8 | | * are treated as missing and silently omitted from the result. |
| | | 9 | | */ |
| | | 10 | | export class AzureKeyVaultSecretProvider implements ISecretProvider { |
| | | 11 | | private readonly secretClient: SecretClient; |
| | | 12 | | |
| | | 13 | | constructor(secretClient: SecretClient) { |
| | 8 | 14 | | if (!secretClient) { |
| | 1 | 15 | | throw new Error('secretClient cannot be null'); |
| | | 16 | | } |
| | 7 | 17 | | this.secretClient = secretClient; |
| | | 18 | | } |
| | | 19 | | |
| | | 20 | | async getSecrets(names: string[]): Promise<Map<string, string>> { |
| | 8 | 21 | | const result = new Map<string, string>(); |
| | 8 | 22 | | if (names.length === 0) { |
| | 1 | 23 | | return result; |
| | | 24 | | } |
| | | 25 | | |
| | 7 | 26 | | for (const name of names) { |
| | 12 | 27 | | if (!name?.trim()) { |
| | 1 | 28 | | throw new Error('Secret name cannot be null or empty'); |
| | | 29 | | } |
| | | 30 | | } |
| | | 31 | | |
| | 6 | 32 | | const entries = await Promise.all( |
| | | 33 | | names.map(async (name) => { |
| | 10 | 34 | | const value = await this.fetchSecret(name); |
| | 9 | 35 | | return [name, value] as const; |
| | | 36 | | }), |
| | | 37 | | ); |
| | | 38 | | |
| | 5 | 39 | | for (const [name, value] of entries) { |
| | 9 | 40 | | if (value !== null) { |
| | 7 | 41 | | result.set(name, value); |
| | | 42 | | } |
| | | 43 | | } |
| | | 44 | | |
| | 5 | 45 | | return result; |
| | | 46 | | } |
| | | 47 | | |
| | | 48 | | private async fetchSecret(name: string): Promise<string | null> { |
| | 10 | 49 | | try { |
| | 10 | 50 | | const response = await this.secretClient.getSecret(name); |
| | 7 | 51 | | return response.value ?? null; |
| | | 52 | | } catch (error: unknown) { |
| | 3 | 53 | | if (isNotFound(error)) { |
| | 2 | 54 | | return null; |
| | | 55 | | } |
| | 1 | 56 | | throw error; |
| | | 57 | | } |
| | | 58 | | } |
| | | 59 | | } |
| | | 60 | | |
| | | 61 | | function isNotFound(error: unknown): boolean { |
| | 3 | 62 | | return ( |
| | | 63 | | typeof error === 'object' && |
| | | 64 | | error !== null && |
| | | 65 | | 'statusCode' in error && |
| | | 66 | | (error as { statusCode: number }).statusCode === 404 |
| | | 67 | | ); |
| | | 68 | | } |