< Summary - Envilder CLI

Information
Class: src/envilder/core/infrastructure/aws/AwsSsmSecretProvider.ts
Assembly: Default
File(s): src/envilder/core/infrastructure/aws/AwsSsmSecretProvider.ts
Tag: 427_29134414720
Line coverage
98%
Covered lines: 52
Uncovered lines: 1
Coverable lines: 53
Total lines: 160
Line coverage: 98.1%
Branch coverage
92%
Covered branches: 24
Total branches: 26
Branch coverage: 92.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/envilder/core/infrastructure/aws/AwsSsmSecretProvider.ts

#LineLine coverage
 1import {
 2  GetParameterCommand,
 3  PutParameterCommand,
 4  type SSM,
 5} from '@aws-sdk/client-ssm';
 6import { GetCallerIdentityCommand, type STS } from '@aws-sdk/client-sts';
 7import { injectable } from 'inversify';
 8import pc from 'picocolors';
 9import { EnvironmentVariable } from '../../domain/EnvironmentVariable.js';
 10import {
 11  ExpiredCredentialsError,
 12  SecretOperationError,
 13  SsoSessionExpiredError,
 14} from '../../domain/errors/DomainErrors.js';
 15import type { ILogger } from '../../domain/ports/ILogger.js';
 16import type { ISecretProvider } from '../../domain/ports/ISecretProvider.js';
 17import { describeError } from '../describeError.js';
 18import { isExpiredCredentialsError } from './isExpiredCredentialsError.js';
 19import { isSsoSessionExpiredError } from './isSsoSessionExpiredError.js';
 20
 921@injectable()
 922export class AwsSsmSecretProvider implements ISecretProvider {
 23  private ssm: SSM;
 24  private logger: ILogger;
 25  private sts: STS;
 26  private profile?: string;
 5327  private identityLogged = false;
 28
 29  constructor(ssm: SSM, logger: ILogger, sts: STS, profile?: string) {
 5330    this.ssm = ssm;
 5331    this.logger = logger;
 5332    this.sts = sts;
 5333    this.profile = profile;
 34  }
 35
 36  async getSecret(name: string): Promise<string | undefined> {
 1537    await this.logIdentity();
 1538    try {
 1539      const command = new GetParameterCommand({
 40        Name: name,
 41        WithDecryption: true,
 42      });
 1543      const { Parameter } = await this.ssm.send(command);
 844      return Parameter?.Value;
 45    } catch (error) {
 746      if (
 47        typeof error === 'object' &&
 48        error !== null &&
 49        'name' in error &&
 50        error.name === 'ParameterNotFound'
 51      ) {
 252        return undefined;
 53      }
 554      if (isSsoSessionExpiredError(error)) {
 155        throw new SsoSessionExpiredError(this.profile, error);
 56      }
 457      if (isExpiredCredentialsError(error)) {
 158        throw new ExpiredCredentialsError(error);
 59      }
 360      throw new SecretOperationError(
 61        `${EnvironmentVariable.maskSecretPath(name)}: ${describeError(error)}`,
 62      );
 63    }
 64  }
 65
 66  async setSecret(name: string, value: string): Promise<void> {
 667    await this.logIdentity();
 668    const command = new PutParameterCommand({
 69      Name: name,
 70      Value: value,
 71      Type: 'SecureString',
 72      Overwrite: true,
 73    });
 674    try {
 675      await this.ssm.send(command);
 76    } catch (error) {
 377      if (isSsoSessionExpiredError(error)) {
 178        throw new SsoSessionExpiredError(this.profile, error);
 79      }
 280      if (isExpiredCredentialsError(error)) {
 181        throw new ExpiredCredentialsError(error);
 82      }
 183      throw new SecretOperationError(
 84        `${EnvironmentVariable.maskSecretPath(name)}: ${describeError(error)}`,
 85      );
 86    }
 87  }
 88
 89  async logIdentity(): Promise<void> {
 2190    if (this.identityLogged) {
 191      return;
 92    }
 2093    this.identityLogged = true;
 2094    const [region, account] = await Promise.all([
 95      this.resolveRegion(),
 96      this.resolveAccount(),
 97    ]);
 2098    const profile = this.profile ?? 'default';
 2199    this.logger.info(this.formatIdentityBanner(account, region, profile));
 100  }
 101
 102  private formatIdentityBanner(
 103    account: string,
 104    region: string,
 105    profile: string,
 106  ): string {
 20107    const sep = pc.dim(' · ');
 108    const accountValue =
 20109      account === 'unknown' ? pc.red(account) : pc.green(account);
 110    const regionValue =
 20111      region === 'unknown' ? pc.red(region) : pc.yellow(region);
 20112    return (
 113      '\n' +
 114      pc.bold(pc.cyan('☁ AWS identity')) +
 115      sep +
 116      pc.dim('account=') +
 117      accountValue +
 118      sep +
 119      pc.dim('region=') +
 120      regionValue +
 121      sep +
 122      pc.dim('profile=') +
 123      pc.magenta(profile)
 124    );
 125  }
 126
 127  private async resolveRegion(): Promise<string> {
 20128    try {
 20129      return await this.ssm.config.region();
 130    } catch {
 0131      return 'unknown';
 132    }
 133  }
 134
 135  private async resolveAccount(): Promise<string> {
 20136    const fromCredentials = await this.tryCredentialsAccount();
 20137    if (fromCredentials) {
 14138      return fromCredentials;
 139    }
 6140    return this.tryStsAccount();
 141  }
 142
 143  private async tryCredentialsAccount(): Promise<string | undefined> {
 20144    try {
 20145      const credentials = await this.ssm.config.credentials();
 19146      return credentials.accountId;
 147    } catch {
 1148      return undefined;
 149    }
 150  }
 151
 152  private async tryStsAccount(): Promise<string> {
 6153    try {
 6154      const identity = await this.sts.send(new GetCallerIdentityCommand({}));
 5155      return identity.Account ?? 'unknown';
 156    } catch {
 1157      return 'unknown';
 158    }
 159  }
 160}