@agilecustoms/envctl 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,8 +24,8 @@ npm view @agilecustoms/envctl version # show latest version available (without i
24
24
  2. Repository access: `envctl` only
25
25
 
26
26
  ## History/motivation
27
- dev-env is a microservice hosted in 'maintenance' account and working as garbage collector: every environment first
28
- created in dev-env and then 'managed' by dev-env: it deletes env when it is not in use anymore OR can extend lifetime.
27
+ `env-api` is a microservice hosted in 'maintenance' account and working as garbage collector: every environment first
28
+ created in `env-api` and then 'managed' by `env-api`: it deletes env when it is not in use anymore OR can extend lifetime.
29
29
  Creation API yields unique ID, so you can safely manage env (delete, extend lifetime) via this ID. But creation API
30
30
  need to be secured. There are two main use cases:
31
31
  1. create environment from CI (mainly ephemeral envs)
@@ -35,7 +35,7 @@ I (Alex C) chosen IAM authorization as common denominator:
35
35
  1. on CI - use OIDC to assume role `/ci/deployer`
36
36
  2. on dev machine - use SSO and profile chaining to assume role `/ci/deployer`
37
37
 
38
- Then as `/ci/deployer` --call--> dev-env HTTP API (exposed with API Gateway with IAM authorizer)
38
+ Then as `/ci/deployer` --call--> `env-api` HTTP API (exposed with API Gateway with IAM authorizer)
39
39
 
40
40
  Now problem is: any request needs to be signed with AWS signature v4. Originally I planned to use bash scripts, but it
41
41
  quickly became bulky and hard to maintain. Then I thought about Node.js - it is available on dev machines and
@@ -1,3 +1,4 @@
1
- export { ephemeral } from './createEphemeral.js';
1
+ export { ephemeral } from './create-ephemeral.js';
2
2
  export { register } from './register.js';
3
3
  export { deleteIt } from './delete.js';
4
+ export { terraformInit } from './terraform-init.js';
@@ -0,0 +1,31 @@
1
+ import { execFile } from 'child_process';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { Command } from 'commander';
5
+ import { wrap } from '../exceptions.js';
6
+ export function terraformInit(program) {
7
+ program
8
+ .command('terraform-init')
9
+ .description('Wrapper for terraform init, to initialize a development environment')
10
+ .argument('<string>', 'key used to create/register the environment')
11
+ .action(wrap(handler));
12
+ }
13
+ async function handler(key) {
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const scriptPath = path.join(__dirname, '../../scripts/terraform-init.sh');
16
+ const child = execFile(scriptPath, [key], (error, stdout, stderr) => {
17
+ if (error) {
18
+ console.error(`Script failed: ${error.message}`);
19
+ return;
20
+ }
21
+ if (stdout)
22
+ console.log(stdout);
23
+ if (stderr)
24
+ console.error(stderr);
25
+ });
26
+ child.on('exit', (code) => {
27
+ if (code !== 0) {
28
+ console.log(`Shell script exited with code ${code}`);
29
+ }
30
+ });
31
+ }
@@ -4,6 +4,16 @@ export class KnownException extends Error {
4
4
  this.name = 'KnownException';
5
5
  }
6
6
  }
7
+ export class HttpException extends Error {
8
+ statusCode;
9
+ responseBody;
10
+ constructor(statusCode, responseBody) {
11
+ super(`Received ${statusCode} response`);
12
+ this.statusCode = statusCode;
13
+ this.responseBody = responseBody;
14
+ this.name = 'HttpException';
15
+ }
16
+ }
7
17
  export function wrap(callable) {
8
18
  return async (...args) => {
9
19
  let result;
@@ -14,6 +24,14 @@ export function wrap(callable) {
14
24
  if (error instanceof KnownException) {
15
25
  console.error(error.message);
16
26
  }
27
+ else if (error instanceof HttpException) {
28
+ if (error.statusCode === 422) {
29
+ console.error(`Validation error: ${error.responseBody}`);
30
+ }
31
+ else {
32
+ console.error(error.message);
33
+ }
34
+ }
17
35
  else {
18
36
  console.error('Unknown error:', error);
19
37
  }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'module';
3
3
  import { Command } from 'commander';
4
4
  import updateNotifier from 'update-notifier';
5
- import { deleteIt, ephemeral, register } from './commands/index.js';
5
+ import { deleteIt, ephemeral, register, terraformInit } from './commands/index.js';
6
6
  const require = createRequire(import.meta.url);
7
7
  const pkg = require('../package.json');
8
8
  updateNotifier({ pkg, updateCheckInterval: 0 }).notify();
@@ -14,4 +14,5 @@ program
14
14
  register(program);
15
15
  ephemeral(program);
16
16
  deleteIt(program);
17
+ terraformInit(program);
17
18
  program.parse(process.argv);
@@ -1,3 +1,4 @@
1
+ import { HttpException, KnownException } from '../exceptions.js';
1
2
  import { HttpClient } from './HttpClient.js';
2
3
  export class DevEnvClient {
3
4
  httpClient;
@@ -11,9 +12,19 @@ export class DevEnvClient {
11
12
  return await this.send('POST', '/ci/env', env);
12
13
  }
13
14
  async delete(envId) {
14
- await this.httpClient.fetch(`/ci/env/${envId}`, {
15
- method: 'DELETE'
16
- });
15
+ try {
16
+ await this.httpClient.fetch(`/ci/env/${envId}`, {
17
+ method: 'DELETE'
18
+ });
19
+ }
20
+ catch (error) {
21
+ if (error instanceof HttpException) {
22
+ if (error.statusCode === 404) {
23
+ throw new KnownException(`Environment with key ${envId} is not found`);
24
+ }
25
+ throw error;
26
+ }
27
+ }
17
28
  }
18
29
  async send(method, path, env) {
19
30
  return this.httpClient.fetch(path, {
@@ -20,6 +20,9 @@ export class EnvCtl {
20
20
  if (message.startsWith('The SSO session token') || message.startsWith('Token is expired')) {
21
21
  throw new KnownException(error.message);
22
22
  }
23
+ if (message.startsWith('ForbiddenException: No access')) {
24
+ throw new KnownException(`Original error: ${message}. Could be wrong sso_account_id in ~/.aws/config for the profile`);
25
+ }
23
26
  }
24
27
  throw new Error('Error fetching account', { cause: error });
25
28
  }
@@ -1,6 +1,6 @@
1
1
  import { fromEnv, fromSSO } from '@aws-sdk/credential-providers';
2
2
  import aws4 from 'aws4';
3
- import { KnownException } from '../exceptions.js';
3
+ import { HttpException } from '../exceptions.js';
4
4
  const HOST = 'env-api.maintenance.agilecustoms.com';
5
5
  export class HttpClient {
6
6
  async fetch(path, options) {
@@ -26,10 +26,7 @@ export class HttpClient {
26
26
  }
27
27
  const text = await response.text();
28
28
  if (!response.ok) {
29
- if (response.status === 422) {
30
- throw new KnownException(`Validation error: ${text}`);
31
- }
32
- throw new Error(`Received ${response.status} response: ${text}`);
29
+ throw new HttpException(response.status, text);
33
30
  }
34
31
  return text;
35
32
  }
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@agilecustoms/envctl",
3
3
  "description": "node.js CLI client for manage environments",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "author": "Alex Chekulaev",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "envctl": "dist/index.js"
9
9
  },
10
+ "files": [
11
+ "dist/",
12
+ "scripts/",
13
+ "package.json"
14
+ ],
10
15
  "repository": {
11
16
  "type": "git",
12
17
  "url": "git+https://github.com/agilecustoms/envctl.git"
@@ -23,7 +28,8 @@
23
28
  "run-version": "tsc --sourceMap true; npm run run -- --version",
24
29
  "run-register": "tsc --sourceMap true; npm run run -- register --project tt --env alexc --owner laxa1986",
25
30
  "run-ephemeral": "tsc --sourceMap true; npm run run -- ephemeral -p tt -e tempenv",
26
- "run-delete": "tsc --sourceMap true; npm run run -- delete tt-alexc"
31
+ "run-delete": "tsc --sourceMap true; npm run run -- delete tt-alexc",
32
+ "run-terraform-init": "tsc --sourceMap true; npm run run -- terraform-init test"
27
33
  },
28
34
  "dependencies": {
29
35
  "@aws-sdk/client-sts": "^3.716.0",
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ KEY="${1:?}"
5
+
6
+ echo "KEY: ${KEY}"
7
+
8
+ #echo "time: $(TZ=America/New_York date +"%T") ETD ($(date -u +"%T") UTC)"
9
+ #echo
10
+ #
11
+ ## TF stored in S3 in format: {company}-{acc-alias}-tf-state/{env} where {acc-alias} has form of {project-code}-{env-kind} like tt-dev
12
+ ## Retrieve AWS account information
13
+ #acc_id=$(aws sts get-caller-identity --query "Account" --output text)
14
+ #acc_alias=$(aws organizations list-tags-for-resource --resource-id "$acc_id" --query "Tags[?Key=='Alias'].Value" --output text)
15
+ #state_prefix="agilecustoms-$acc_alias" # like "agilecustoms-tt-dev"
16
+ #
17
+ ## -upgrade - get latest version of providers (mainly hashicorp/aws)
18
+ ## -reconfigure - discard local state, use (or create) remote
19
+ ## added to allow deploy multiple envs from local machine (on CI no local state survive between runs)
20
+ #terraform init -upgrade -reconfigure \
21
+ # -backend-config="bucket=$state_prefix-tf-state" \
22
+ # -backend-config="key=$KEY"
23
+ #
24
+ #echo
25
+ #echo "time: $(TZ=America/New_York date +"%T") ETD ($(date -u +"%T") UTC)"
@@ -1,37 +0,0 @@
1
- name: Client Build and Release
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
-
8
- jobs:
9
- Build:
10
- uses: ./.github/workflows/build.yml
11
- with:
12
- artifacts: true
13
- secrets: inherit
14
-
15
-
16
- Release:
17
- needs:
18
- - Build
19
- runs-on: ubuntu-latest
20
- steps:
21
- - name: Checkout
22
- uses: actions/checkout@v4
23
- with:
24
- token: ${{ secrets.GH_ACTIONS_TOKEN }} # Admin PAT to bypass push protection on 'main' branch
25
-
26
- - name: Download artifacts
27
- uses: actions/download-artifact@v4
28
-
29
- - name: Release
30
- id: release
31
- uses: agilecustoms/gha-release@main
32
- with:
33
- aws-account: ${{ vars.AWS_ACCOUNT_DIST }}
34
- npmjs-token: ${{ secrets.NPMJS_TOKEN }}
35
-
36
- - name: Summary
37
- run: echo "### Released ${{ steps.release.outputs.version }} :pushpin:" >> $GITHUB_STEP_SUMMARY
@@ -1,46 +0,0 @@
1
- name: Client Build
2
-
3
- on:
4
- push:
5
- branches-ignore:
6
- - main
7
- workflow_call:
8
- inputs:
9
- artifacts:
10
- required: false
11
- type: boolean
12
- default: false
13
- description: upload artifacts or not
14
-
15
- jobs:
16
- Build:
17
- runs-on: ubuntu-latest
18
- steps:
19
- - name: Checkout
20
- uses: actions/checkout@v4
21
-
22
- - name: Setup Node
23
- uses: actions/setup-node@v4
24
- with:
25
- node-version: 23
26
- cache: npm
27
-
28
- - name: Install dependencies
29
- run: npm ci
30
-
31
- - name: Lint
32
- run: npm run lint
33
-
34
- - name: Test
35
- run: npm run test
36
-
37
- - name: Build
38
- run: npm run build
39
-
40
- - name: Upload artifacts
41
- if: inputs.artifacts
42
- uses: actions/upload-artifact@v4
43
- with:
44
- path: dist # take everything from dist/ folder
45
- name: dist # and create artifact named dist (so later on the download action will create <repo-root>/dist/ folder)
46
- # if not specify name - it will be available as 'artifact' dir
package/Makefile DELETED
@@ -1,21 +0,0 @@
1
- _aws-login:
2
- @aws sso login
3
-
4
- # npm list --all - show dependency tree
5
- app0-install-deps:
6
- @npm install
7
-
8
- app0-update-deps:
9
- @npm update; npm outdated
10
-
11
- app1-lint:
12
- @npm run lint
13
-
14
- app1-lint-fix:
15
- @npm run lint:fix
16
-
17
- app2-test:
18
- @npm run test
19
-
20
- app3-build:
21
- @npm run build
package/eslint.config.mjs DELETED
@@ -1,34 +0,0 @@
1
- import tseslint from 'typescript-eslint'
2
- import plugin from '@stylistic/eslint-plugin'
3
- import importPlugin from 'eslint-plugin-import';
4
-
5
- export default [
6
- ...tseslint.configs.recommended,
7
- plugin.configs['recommended-flat'],
8
- {
9
- plugins: {
10
- import: importPlugin,
11
- },
12
- rules: {
13
- '@stylistic/brace-style': ['error', '1tbs'], // 'else' keyword on the same line as closing brace
14
- '@stylistic/comma-dangle': 'off', // there are cases when trailing comma desired, and sometimes not
15
- '@typescript-eslint/no-unused-vars': ['error', { // unused var starting from _ is ok
16
- 'argsIgnorePattern': '^_',
17
- 'destructuredArrayIgnorePattern': '^_'
18
- }],
19
-
20
- // Enforce Alphabetical Import Order and Merge Duplicate Imports
21
- 'import/order': ['error', {
22
- 'alphabetize': {'order': 'asc', 'caseInsensitive': true}
23
- }],
24
- 'import/no-duplicates': 'error',
25
- }
26
- },
27
- // Test-specific configuration (glob-based override)
28
- {
29
- files: ['**/*.test.ts', '**/*.test.tsx'],
30
- rules: {
31
- '@typescript-eslint/no-extra-non-null-assertion': 'off', // allow using !! initially to use with .mock.calls[0]!!
32
- },
33
- },
34
- ];
@@ -1,21 +0,0 @@
1
- import { Command } from 'commander'
2
- import { envCtl } from '../container.js'
3
- import { wrap } from '../exceptions.js'
4
-
5
- export function ephemeral(program: Command) {
6
- program
7
- .command('ephemeral')
8
- .description('Create new ephemeral environment')
9
- .requiredOption('-k, --key <key>', 'Key used to store env state (s3 key in case of AWS). Can be git hash, feature-a or {project-code}-{env-name}')
10
- .action(wrap(handler))
11
- }
12
-
13
- type Options = {
14
- key: string
15
- }
16
-
17
- async function handler(options: object): Promise<string> {
18
- const { key } = options as Options
19
-
20
- return await envCtl.createEphemeralEnv(key)
21
- }
@@ -1,15 +0,0 @@
1
- import { Command } from 'commander'
2
- import { envCtl } from '../container.js'
3
- import { wrap } from '../exceptions.js'
4
-
5
- export function deleteIt(program: Command) {
6
- program
7
- .command('delete')
8
- .description('Delete a development environment')
9
- .argument('<string>', 'key used to create/register the environment')
10
- .action(wrap(handler))
11
- }
12
-
13
- async function handler(key: string): Promise<void> {
14
- await envCtl.delete(key)
15
- }
@@ -1,3 +0,0 @@
1
- export { ephemeral } from './createEphemeral.js'
2
- export { register } from './register.js'
3
- export { deleteIt } from './delete.js'
@@ -1,23 +0,0 @@
1
- import { Command } from 'commander'
2
- import { envCtl } from '../container.js'
3
- import { wrap } from '../exceptions.js'
4
-
5
- export function register(program: Command) {
6
- program
7
- .command('register')
8
- .description('Create new or update existing dev environment')
9
- .requiredOption('-k, --key <key>', 'Key used to store env state (s3 key in case of AWS). Can be git hash, feature-a or {project-code}-{env-name}')
10
- .requiredOption('-o, --owner <owner>', 'Environment owner (GH username)')
11
- .action(wrap(handler))
12
- }
13
-
14
- type Options = {
15
- key: string
16
- owner: string
17
- }
18
-
19
- async function handler(options: object): Promise<string> {
20
- const { key, owner } = options as Options
21
-
22
- return await envCtl.registerEnv(key, owner)
23
- }
package/src/container.ts DELETED
@@ -1,9 +0,0 @@
1
- import { STSClient } from '@aws-sdk/client-sts'
2
- import { DevEnvClient, EnvCtl, HttpClient } from './service/index.js'
3
-
4
- const stsClient = new STSClient()
5
- const httpClient = new HttpClient()
6
- const devEnvClient = new DevEnvClient(httpClient)
7
- const envCtl = new EnvCtl(stsClient, devEnvClient)
8
-
9
- export { envCtl }
package/src/exceptions.ts DELETED
@@ -1,29 +0,0 @@
1
- export class KnownException extends Error {
2
- constructor(message: string) {
3
- super(message)
4
- this.name = 'KnownException'
5
- }
6
- }
7
-
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- export function wrap(callable: (...args: any[]) => Promise<string | void>): (...args: any[]) => void | Promise<void> {
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- return async (...args: any[]): Promise<void> => {
12
- let result: string | void
13
- try {
14
- result = await callable(...args)
15
- } catch (error) {
16
- if (error instanceof KnownException) {
17
- console.error(error.message)
18
- } else {
19
- // print stack trace
20
- console.error('Unknown error:', error)
21
- }
22
- process.exit(1)
23
- }
24
- // this is how result is returned back so it can be accessed by calling shell script
25
- if (result !== undefined) {
26
- console.log(result)
27
- }
28
- }
29
- }
package/src/index.ts DELETED
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from 'module'
3
- import { Command } from 'commander'
4
- import updateNotifier from 'update-notifier'
5
- import { deleteIt, ephemeral, register } from './commands/index.js'
6
-
7
- // it is possible to use 'import' to load JSON files, but package.json is level higher, so such trick copies 'src' in /dist :(
8
- const require = createRequire(import.meta.url) // custom 'require' relative to current module’s file URL
9
- const pkg = require('../package.json')
10
-
11
- // Check for updates
12
- updateNotifier({ pkg, updateCheckInterval: 0 }).notify()
13
-
14
- const program = new Command()
15
- program
16
- .name('envctl') // shown when running --help: Usage: envctl [options] [command]
17
- .description('CLI to manage environments')
18
- .version(pkg.version)
19
- register(program)
20
- ephemeral(program)
21
- deleteIt(program)
22
-
23
- program.parse(process.argv)
@@ -1,4 +0,0 @@
1
- export type Environment = {
2
- account: string
3
- key: string
4
- }
@@ -1,5 +0,0 @@
1
- import type { Environment } from './Environment.js'
2
-
3
- export type PersonalEnvironment = Environment & {
4
- owner: string
5
- }
@@ -1,2 +0,0 @@
1
- export type { Environment } from './Environment.js'
2
- export type { PersonalEnvironment } from './PersonalEnvironment.js'
@@ -1,34 +0,0 @@
1
- import type { Environment, PersonalEnvironment } from '../model/index.js'
2
- import { HttpClient } from './HttpClient.js'
3
-
4
- export class DevEnvClient {
5
- private httpClient: HttpClient
6
-
7
- constructor(httpClient: HttpClient) {
8
- this.httpClient = httpClient
9
- }
10
-
11
- public async registerEnv(env: PersonalEnvironment): Promise<string> {
12
- return await this.send('PUT', '/ci/env', env)
13
- }
14
-
15
- public async createEphemeralEnv(env: Environment): Promise<string> {
16
- return await this.send('POST', '/ci/env', env)
17
- }
18
-
19
- public async delete(envId: string): Promise<void> {
20
- await this.httpClient.fetch(`/ci/env/${envId}`, {
21
- method: 'DELETE'
22
- })
23
- }
24
-
25
- private async send(method: string, path: string, env: Environment): Promise<string> {
26
- return this.httpClient.fetch(path, {
27
- method,
28
- body: JSON.stringify(env),
29
- headers: {
30
- 'Content-Type': 'application/json'
31
- }
32
- })
33
- }
34
- }
@@ -1,57 +0,0 @@
1
- import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'
2
- import type { GetCallerIdentityResponse } from '@aws-sdk/client-sts'
3
- import { KnownException } from '../exceptions.js'
4
- import { DevEnvClient } from './DevEnvClient.js'
5
-
6
- export class EnvCtl {
7
- private stsClient: STSClient
8
- private devEnvClient: DevEnvClient
9
-
10
- constructor(
11
- stsClient: STSClient,
12
- devEnvClient: DevEnvClient
13
- ) {
14
- this.stsClient = stsClient
15
- this.devEnvClient = devEnvClient
16
- }
17
-
18
- private async getAccount(): Promise<string> {
19
- const command = new GetCallerIdentityCommand({})
20
- let response: GetCallerIdentityResponse
21
- try {
22
- response = await this.stsClient.send(command)
23
- } catch (error) {
24
- // there is no type to check with instanceof, and also different errors have same .name == 'CredentialsProviderError'
25
- if (error instanceof Error) {
26
- const message = error.message
27
- if (message.startsWith('The SSO session token') || message.startsWith('Token is expired')) {
28
- throw new KnownException(error.message)
29
- }
30
- }
31
- throw new Error('Error fetching account', { cause: error })
32
- }
33
- return response.Account!
34
- }
35
-
36
- public async registerEnv(key: string, owner: string): Promise<string> {
37
- const account = await this.getAccount()
38
- const env = { account, key, owner }
39
- return await this.devEnvClient.registerEnv(env)
40
- }
41
-
42
- public async createEphemeralEnv(key: string): Promise<string> {
43
- const account = await this.getAccount()
44
- const env = { account, key }
45
- return await this.devEnvClient.createEphemeralEnv(env)
46
- }
47
-
48
- /**
49
- * Here client uses secret knowledge on how id is constructed from account and key
50
- * Probably it is OK because client and server are tightly coupled
51
- */
52
- public async delete(key: string): Promise<void> {
53
- const account = await this.getAccount()
54
- const envId = `${account}-${key}`
55
- return this.devEnvClient.delete(envId)
56
- }
57
- }
@@ -1,58 +0,0 @@
1
- import { fromEnv, fromSSO } from '@aws-sdk/credential-providers'
2
- import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smithy/types'
3
- import aws4 from 'aws4'
4
- import type { Credentials, Request as Aws4Request } from 'aws4'
5
- import { KnownException } from '../exceptions.js'
6
-
7
- const HOST = 'env-api.maintenance.agilecustoms.com'
8
-
9
- export class HttpClient {
10
- public async fetch(path: string, options: RequestInit): Promise<string> {
11
- const creds: Credentials = await this.getCredentials()
12
-
13
- const requestOptions: Aws4Request = {
14
- method: options.method,
15
- body: options.body as string | undefined,
16
- headers: options.headers as Record<string, string>,
17
- host: HOST,
18
- service: 'execute-api',
19
- path,
20
- }
21
- const signedOptions: Aws4Request = aws4.sign(requestOptions, creds)
22
- const signedHeaders = signedOptions.headers as Record<string, string>
23
-
24
- const url = `https://${HOST}${path}`
25
- options.headers = signedHeaders
26
-
27
- let response: Response
28
- try {
29
- response = await fetch(url, options)
30
- } catch (error) {
31
- throw new Error('Error (network?) making the request:', { cause: error })
32
- }
33
-
34
- const text = await response.text()
35
- if (!response.ok) {
36
- if (response.status === 422) {
37
- throw new KnownException(`Validation error: ${text}`)
38
- }
39
- throw new Error(`Received ${response.status} response: ${text}`)
40
- }
41
-
42
- return text
43
- }
44
-
45
- private async getCredentials(): Promise<AwsCredentialIdentity> {
46
- let identityProvider: AwsCredentialIdentityProvider
47
- if (process.env.CI) {
48
- identityProvider = fromEnv()
49
- } else {
50
- identityProvider = fromSSO()
51
- }
52
- try {
53
- return await identityProvider()
54
- } catch (error) {
55
- throw new Error('Error fetching credentials:', { cause: error })
56
- }
57
- }
58
- }
@@ -1,3 +0,0 @@
1
- export { DevEnvClient } from './DevEnvClient.js'
2
- export { EnvCtl } from './EnvCtl.js'
3
- export { HttpClient } from './HttpClient.js'
@@ -1,34 +0,0 @@
1
- import { STSClient } from '@aws-sdk/client-sts'
2
- import { beforeEach, describe, expect, it, vi } from 'vitest'
3
- import { DevEnvClient, EnvCtl } from '../../src/service'
4
-
5
- const stsClient = {
6
- send: vi.fn(),
7
- }
8
-
9
- const devEnvClient = {
10
- registerEnv: vi.fn(),
11
- }
12
-
13
- describe('EnvCtl', () => {
14
- let envCtl: EnvCtl
15
- beforeEach(() => {
16
- envCtl = new EnvCtl(
17
- stsClient as unknown as STSClient,
18
- devEnvClient as unknown as DevEnvClient
19
- )
20
- })
21
-
22
- describe('registerEnv', () => {
23
- it('should call stsClient to get account and then call devEnvClient to register env', async () => {
24
- stsClient.send.mockResolvedValue({ Account: 'testAccount' })
25
- devEnvClient.registerEnv.mockResolvedValue('testEnvId')
26
-
27
- const result = await envCtl.registerEnv('env', 'owner')
28
-
29
- expect(result).toBe('testEnvId')
30
- expect(stsClient.send).toHaveBeenCalled()
31
- expect(devEnvClient.registerEnv).toHaveBeenCalled()
32
- })
33
- })
34
- })
package/tsconfig.json DELETED
@@ -1,23 +0,0 @@
1
- // https://www.typescriptlang.org/tsconfig
2
- {
3
- "include": [
4
- "src"
5
- ],
6
- // Runtime is Node 23 (latest as Jan 2025), to be run in CI (action/setup-node) and local machines
7
- "compilerOptions": {
8
- // MAIN SETTINGS (based on runtime)
9
- "target": "es2023", // highest available, fully supported by Node 23
10
- "module": "nodenext", // preferred for modern Node projects
11
- "outDir": "dist", // standard output folder for all emitted files
12
-
13
- // DEFAULTS that are always "good to have" - expect to be the same for all projects
14
- "allowJs": false, // no need to compile JS, all is TS
15
- "noUncheckedIndexedAccess": true,
16
- "removeComments": true,
17
- "skipLibCheck": true, // skip type checking of declaration files (.d.ts) - allows quick hot reload
18
- "strict": true, // enable all strict type-checking options
19
- "verbatimModuleSyntax": true, // force using 'import type' for types
20
-
21
- // ADDITIONAL SETTINGS (vary per project)
22
- }
23
- }
package/vitest.config.ts DELETED
@@ -1,19 +0,0 @@
1
- import { coverageConfigDefaults, defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- coverage: {
6
- exclude: [
7
- '**/index.ts',
8
- 'src/container.ts',
9
- 'src/model/**',
10
- ...coverageConfigDefaults.exclude
11
- ],
12
- reporter: ['text'], // other: 'html', 'clover', 'json'
13
- thresholds: {
14
- lines: 26,
15
- branches: 44,
16
- }
17
- }
18
- }
19
- })