| | | 1 | | import { GetParametersCommand, type SSMClient } from '@aws-sdk/client-ssm'; |
| | | 2 | | import { |
| | | 3 | | ExpiredCredentialsError, |
| | | 4 | | isExpiredCredentialsError, |
| | | 5 | | } from '../../domain/expired-credentials-error.js'; |
| | | 6 | | import type { ISecretProvider } from '../../domain/ports/secret-provider.js'; |
| | | 7 | | import { |
| | | 8 | | isSsoSessionExpiredError, |
| | | 9 | | SsoSessionExpiredError, |
| | | 10 | | } from '../../domain/sso-session-expired-error.js'; |
| | | 11 | | |
| | 4 | 12 | | const SSM_BATCH_SIZE = 10; |
| | | 13 | | |
| | | 14 | | /** |
| | | 15 | | * {@link ISecretProvider} backed by AWS SSM Parameter Store. |
| | | 16 | | * |
| | | 17 | | * Parameters are retrieved with decryption enabled so that |
| | | 18 | | * SecureString values are returned in plain text. |
| | | 19 | | * |
| | | 20 | | * SSM supports fetching up to 10 parameters per request, |
| | | 21 | | * so names are chunked into batches automatically. |
| | | 22 | | */ |
| | | 23 | | export class AwsSsmSecretProvider implements ISecretProvider { |
| | | 24 | | private readonly ssmClient: SSMClient; |
| | | 25 | | private readonly profile?: string; |
| | | 26 | | |
| | | 27 | | constructor(ssmClient: SSMClient, profile?: string) { |
| | 13 | 28 | | if (!ssmClient) { |
| | 1 | 29 | | throw new Error('ssmClient cannot be null'); |
| | | 30 | | } |
| | 12 | 31 | | this.ssmClient = ssmClient; |
| | 12 | 32 | | this.profile = profile; |
| | | 33 | | } |
| | | 34 | | |
| | | 35 | | async getSecrets(names: string[]): Promise<Map<string, string>> { |
| | 13 | 36 | | const result = new Map<string, string>(); |
| | 13 | 37 | | if (names.length === 0) { |
| | 1 | 38 | | return result; |
| | | 39 | | } |
| | | 40 | | |
| | 12 | 41 | | for (const name of names) { |
| | 26 | 42 | | if (!name?.trim()) { |
| | 1 | 43 | | throw new Error('Secret name cannot be null or whitespace'); |
| | | 44 | | } |
| | | 45 | | } |
| | | 46 | | |
| | 11 | 47 | | for (let i = 0; i < names.length; i += SSM_BATCH_SIZE) { |
| | 12 | 48 | | const batch = names.slice(i, i + SSM_BATCH_SIZE); |
| | 12 | 49 | | try { |
| | 12 | 50 | | const response = await this.ssmClient.send( |
| | | 51 | | new GetParametersCommand({ |
| | | 52 | | Names: batch, |
| | | 53 | | WithDecryption: true, |
| | | 54 | | }), |
| | | 55 | | ); |
| | | 56 | | |
| | 7 | 57 | | for (const param of response.Parameters ?? []) { |
| | 5 | 58 | | if (param.Name && param.Value != null) { |
| | 5 | 59 | | result.set(param.Name, param.Value); |
| | | 60 | | } |
| | | 61 | | } |
| | | 62 | | } catch (error) { |
| | 5 | 63 | | if (isSsoSessionExpiredError(error)) { |
| | 1 | 64 | | throw new SsoSessionExpiredError(this.profile, error); |
| | | 65 | | } |
| | 4 | 66 | | if (isExpiredCredentialsError(error)) { |
| | 1 | 67 | | throw new ExpiredCredentialsError(error); |
| | | 68 | | } |
| | 3 | 69 | | throw error; |
| | | 70 | | } |
| | | 71 | | } |
| | | 72 | | |
| | 6 | 73 | | return result; |
| | | 74 | | } |
| | | 75 | | } |