< Summary - Envilder CLI

Information
Class: src/envilder/apps/cli/recovery/SsoLoginRecovery.ts
Assembly: Default
File(s): src/envilder/apps/cli/recovery/SsoLoginRecovery.ts
Tag: 427_29134414720
Line coverage
72%
Covered lines: 37
Uncovered lines: 14
Coverable lines: 51
Total lines: 136
Line coverage: 72.5%
Branch coverage
71%
Covered branches: 15
Total branches: 21
Branch coverage: 71.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/envilder/apps/cli/recovery/SsoLoginRecovery.ts

#LineLine coverage
 1import { spawn } from 'node:child_process';
 2import { createInterface } from 'node:readline';
 3import { SsoSessionExpiredError } from '../../../core/domain/errors/DomainErrors.js';
 4import { presentError } from '../errors/CliErrorPresenter.js';
 5import { SilentExitError } from '../errors/SilentExitError.js';
 6
 27const AWS_CLI_NOT_FOUND = -1;
 28const LOGIN_FAILED = 1;
 9
 10export type RecoveryDeps = {
 11  isInteractive(): boolean;
 12  confirm(question: string): Promise<boolean>;
 13  runLogin(profileName?: string): Promise<number>;
 14  write(message: string): void;
 15};
 16
 17export function buildLoginArgs(profileName?: string): string[] {
 318  if (profileName) {
 219    return ['sso', 'login', '--profile', profileName];
 20  }
 121  return ['sso', 'login'];
 22}
 23
 24function defaultIsInteractive(): boolean {
 025  return [process.stdout.isTTY, process.stdin.isTTY].every(Boolean);
 26}
 27
 28function defaultConfirm(question: string): Promise<boolean> {
 029  const prompt = createInterface({
 30    input: process.stdin,
 31    output: process.stdout,
 32  });
 033  return new Promise((resolve) => {
 034    prompt.question(`${question} `, (answer) => {
 035      prompt.close();
 036      resolve(answer.trim().toLowerCase() === 'y');
 37    });
 38  });
 39}
 40
 41function defaultRunLogin(profileName?: string): Promise<number> {
 042  return new Promise((resolve) => {
 043    const child = spawn('aws', buildLoginArgs(profileName), {
 44      stdio: 'inherit',
 45    });
 046    child.on('error', (error: NodeJS.ErrnoException) => {
 047      resolve(error.code === 'ENOENT' ? AWS_CLI_NOT_FOUND : LOGIN_FAILED);
 48    });
 049    child.on('close', (code) => {
 050      resolve(code ?? LOGIN_FAILED);
 51    });
 52  });
 53}
 54
 55function defaultWrite(message: string): void {
 056  console.error(message);
 57}
 58
 259const defaultDeps: RecoveryDeps = {
 60  isInteractive: defaultIsInteractive,
 61  confirm: defaultConfirm,
 62  runLogin: defaultRunLogin,
 63  write: defaultWrite,
 64};
 65
 66export async function executeWithSsoRecovery(
 67  run: () => Promise<void>,
 68  deps: Partial<RecoveryDeps> = {},
 69): Promise<void> {
 1070  const resolved: RecoveryDeps = { ...defaultDeps, ...deps };
 1071  try {
 1072    return await run();
 73  } catch (error) {
 774    if (!(error instanceof SsoSessionExpiredError)) {
 175      throw error;
 76    }
 677    if (!resolved.isInteractive()) {
 178      throw error;
 79    }
 580    await recoverInteractively(run, error, resolved);
 81  }
 82}
 83
 84async function recoverInteractively(
 85  run: () => Promise<void>,
 86  error: SsoSessionExpiredError,
 87  deps: RecoveryDeps,
 88): Promise<void> {
 589  deps.write(presentError(error));
 90
 591  const command = error.profileName
 92    ? `aws sso login --profile ${error.profileName}`
 93    : 'aws sso login';
 594  const accepted = await deps.confirm(`Run '${command}' now? [y/N]`);
 595  if (!accepted) {
 196    deps.write(
 97      'Skipped. Run the command above when ready, then re-run envilder.',
 98    );
 199    throw new SilentExitError(LOGIN_FAILED);
 100  }
 101
 4102  const exitCode = await deps.runLogin(error.profileName);
 4103  ensureLoginSucceeded(exitCode, deps);
 4104  await retryOnce(run, deps);
 105}
 106
 107function ensureLoginSucceeded(exitCode: number, deps: RecoveryDeps): void {
 4108  if (exitCode === AWS_CLI_NOT_FOUND) {
 1109    deps.write(
 110      'The AWS CLI was not found on your PATH. Install it, then run the command above.',
 111    );
 1112    throw new SilentExitError(LOGIN_FAILED);
 113  }
 3114  if (exitCode !== 0) {
 1115    deps.write('Login did not complete. Try again, then re-run envilder.');
 1116    throw new SilentExitError(LOGIN_FAILED);
 117  }
 118}
 119
 120async function retryOnce(
 121  run: () => Promise<void>,
 122  deps: RecoveryDeps,
 123): Promise<void> {
 2124  deps.write('\uD83C\uDF44 1-UP! SSO session restored.');
 2125  deps.write('\u21bb Warping back for your secrets\u2026');
 2126  try {
 2127    await run();
 1128    deps.write('\u2b50 Level cleared.');
 129  } catch (retryError) {
 1130    if (retryError instanceof SsoSessionExpiredError) {
 1131      deps.write(presentError(retryError));
 1132      throw new SilentExitError(LOGIN_FAILED);
 133    }
 0134    throw retryError;
 135  }
 136}