DR McCullochFLIGHTDECK Β· AWS COMMAND SURFACETAKE THE KEYBOARD ↓HOMESHOPABOUT

A COMMAND LINE, REBUILT Β· NOTHING HERE REACHES AWS

Flightdeck

Seven commands I wrote to stand up and move AWS environments from a laptop, and the blast radius of each one read off the code.

It's a TypeScript CLI that drives Amplify, AmplifyBackend, CloudFormation, DynamoDB, SSM and Cognito against a live account through single sign-on. A command line is close to impossible to show, so I rebuilt it here: every prompt string, every choice list, every default and every line the program prints is quoted from the source, and the only thing I made up is what AWS would have said back.

46
TypeScript files
2,877
lines
37
SDK call sites
27
command types
25
prompts
13
yes/no questions
0
tests
none
version control

SIMULATEDNothing on this page opens a connection, and every value AWS would have returned is invented and printed in violet.

REDACTEDThe project was never put under version control and its config folder still holds long-lived credentials in plaintext, which is why nothing identifying the account or the client reaches this page. The account id, the two role ARNs, the SSO start URL, the company and tenant domains, the Bitbucket workspace, the app and parameter names and the vendor products are replaced with visible tokens in the data file itself rather than at render time, so you can see the tokens sitting inside the prompts and the log lines.

walk the tree ↓

01 Β· THE GUARDS

Five of the seven commands never ask.

Every yes-or-no question in the program is built by one function, confirm({ message: query, default: true }), so Inquirer renders it as (Y/n) and pressing Enter without typing anything answers yes. Create New Amplify App and Create New Amplify Environment ask along the way; the other five go from the menu straight to the writes, and the two heaviest writers are both in that group: a full DynamoDB table copy that replaces items key for key and a pass across the Amplify apps the region lists back, writing each of their environment variables into SSM with Overwrite: true.

YES/NO QUESTIONS ON EACH COMMAND’S PATH

6 CONFIRMS

Create New Amplify App

src/commands/create-new-app.ts

six on the ordinary path, eight if a build limit sends it through JobCheckAndWait

4 CONFIRMS

Create New Amplify Environment

src/commands/create-amplify-environment.ts

four on the clone path, one if you answer no to the first

NO CONFIRM

Create New Customer Environment

src/commands/create-new-customer.ts

no yes/no question anywhere on its path

NO CONFIRM

Copy Environment DB

src/commands/copy-env-db.ts

no yes/no question anywhere on its path

NO CONFIRM

Copy Environment Variables to SSM

src/commands/copy-environment-variables-to-ssm.ts

no yes/no question, and no prompt of any kind

NO CONFIRM

Copy cognito users between pools

src/commands/copy-cognito-pool-users.ts

no yes/no question anywhere on its path

NO CONFIRM

Copy Cognito Users to Another Environment

src/commands/copy-amplify-users.ts

no yes/no question anywhere on its path

Create New Customer Environment, the one that provisions for a paying customer, hardcodes preSelectedEnvironmentType: EnvironmentType.PRODUCTION with skipEnvironmentType: true, so it can only ever be pointed at production; it never says so, and between the domain you type and a live branch cut from the head of production there is no question at all.

02 Β· THE CONSOLE

The tree is the program’s own, node for node.

Arrow keys move the cursor, Enter commits and a committed prompt collapses the way Inquirer collapses it. The manifest beside the terminal fills with the AWS calls that route issues, in call order, each traced to the file and line where the command object is constructed and sorted into reads, creates or overwrites.

SIMULATED Β· NOTHING HERE TALKS TO AWSno command selected

click or tap the terminal to take the keyboard

  • a line the program really prints, quoted from the source
  • invented: what AWS or a library would have said back
  • a value withheld before this page was built

03 Β· THE THREE CONFIGURATIONS

Where the environments differ and where they don’t.

The environment type you pick at the first prompt selects a set of JSON files by name, and those files are spread wholesale into the Amplify request. The keys are quoted from src/config/*.json with the domains and image names taken out, and several of them turn out to hold the same value in all three. The environment.default.*.json files are each the two characters {}, placeholders nobody ever filled in.

keyfiledevelopmentdemoproduction
AMPLIFY_DESTRUCTIVE_UPDATESapp.default.*.json Β· environmentVariables"true""true""false"
AMPLIFY_ENABLE_DEBUG_OUTPUTapp.default.*.json Β· environmentVariables"false""false""false"
BUILD_IDapp.default.*.json Β· environmentVariables"0.1.0""0.1.0"not set
enableBranchAutoBuildapp.default.*.jsontruetruetrue
enableBranchAutoDeletionapp.default.*.jsontruefalsefalse
autoBranchCreationPatternsapp.default.*.json<redacted:branch-pattern> Γ— 4["BRANCH_CREATION_PATTERNS"]["BRANCH_CREATION_PATTERNS"]
autoBranchCreationConfig.stageapp.default.*.json"DEVELOPMENT""DEVELOPMENT""PRODUCTION"
branchNamebranch.default.*.json"dev""demo""stage"
stagebranch.default.*.json"PRODUCTION""PRODUCTION""PRODUCTION"
subDomainSettings[0].branchNamedomain.default.*.json"dev""demo""stage"
the whole fileenvironment.default.*.json{}{}{}

AMPLIFY_DESTRUCTIVE_UPDATES is "true" in development and demo and "false" in production, which is the way round you would want. The branch files hold two keys each and every one of them stages as PRODUCTION, including development, while the production file’s branch is called stage.

04 Β· THE SOURCE ITSELF

Eighteen things I found reading my own code back.

Each of these is a claim about the source and carries the file and line to check it against. I have not guessed at what AWS does with any of it; where the reading depends on a service behaviour I have left the behaviour out and quoted the code instead.

  1. 01

    The first thing it does is rewrite your AWS config

    Before the menu appears, updateProfileInConfig reads ~/.aws/config, matches the profile block with new RegExp(`\\[profile ${AWS_PROFILE}\\][\\s\\S]*?(?=\\[|$)`, 'g') and writes the whole file back. Anything of yours that the regex swallowed is gone.

    src/aws/login.ts:45

  2. 02

    The role is always the widest one

    Login has a narrower default parameter, and runCLI calls Login('AdministratorAccess'), so the role written into the profile is administrator every time.

    src/index.ts:59

  3. 03

    Every yes/no question is pre-answered yes

    All thirteen confirmations go through one function: confirm({ message: query, default: true }). Inquirer renders that as (Y/n) and a bare Enter answers yes.

    src/helpers/command-line-inputs.ts:14

  4. 04

    The DynamoDB copy runs twice

    amplifyCopyDatabase performs the whole transfer at line 22. The command then recomputes both suffixes and calls DynamodbTransfer again at line 42 with no await, so the second pass runs outside the try/catch.

    src/commands/copy-env-db.ts:42

  5. 05

    A log line says deleted where the code writes the word null

    clearEmptyIndexes sets a missing or empty index attribute to the literal string 'null' and then logs "Deleted empty index attribute …". Nothing is deleted and the destination now holds the four characters n-u-l-l.

    src/helpers/dynamodb-transfer.ts:60

  6. 06

    The suffix is discovered rather than configured

    GetBackendEnvironment gives the stack name, DescribeStackResources finds the API resource, the physical id is regex-stripped back to a stack name, DescribeStacks reads GraphQLAPIIdOutput and the env parameter, and the suffix comes back as `-${graphqlId}-${envName}`. Every table whose name ends with that string is in scope.

    src/helpers/amplify-get-env-dynamodb-suffix.ts:56

  7. 07

    The environment name is truncated and never padded

    The name is built as region code plus environment short code plus the cleaned customer name, then cut with .slice(0, 10). A comment says "If the name is shorter than 2 characters, pad it with x" and the padding was never written.

    src/helpers/amplify-create-environment-name.ts:23

  8. 08

    The app list is only ever one page

    Both fetchers send ListAppsCommand({}) and never follow nextToken, while listAllApps in the same codebase does paginate. Whichever apps fell off the first page do not exist as far as these commands are concerned.

    src/helpers/amplify-fetch-region-apps.ts:8

  9. 09

    A secret string is cached under itself

    SecureString encrypts the Bitbucket credential pair with AES-256-GCM and PBKDF2 at 100,000 iterations, then caches the instance in a Map keyed by the plaintext, so the plaintext stays resident for the life of the process.

    src/helpers/secure-request.ts:20

  10. 10

    An HTTP error page parses as a token response

    secureHttpsRequest resolves on the response body without ever reading the status code and sets no timeout.

    src/helpers/secure-request.ts:78

  11. 11

    Three calls are never awaited

    The user migration, the second database transfer and the third-party redirect-URI POST are all fired without await, so each one escapes the try/catch that was written to contain it.

    src/commands/copy-amplify-users.ts:24

  12. 12

    Two functions disagree about one string

    AmplifyWaitForJob only ends its loop on COMPLETED or FAILED and its caller only proceeds on SUCCEED (src/commands/create-new-app.ts:346), so the database clone at the end of Create New Amplify App cannot be reached.

    src/helpers/amplify-wait-for-job.ts:35

  13. 13

    The compiled binary reads its config from the source folder

    Config paths are built with path.join(path.resolve(), 'src/config/…'), so dist/index.js looks for src/config relative to whatever directory you happen to be standing in.

    src/helpers/read-json-file.ts:3

  14. 14

    Two filenames are misspelled and imported that way

    The files are called ampify-create-env-with-branch.ts and amplify-clone-envrionment.ts, and both are imported under those spellings, so both work.

    src/helpers/

  15. 15

    There is no history and no tests

    npm test is echo "Error: no test specified" && exit 1. There is no .git directory, no README and no documentation, and all 46 source files carry the same modification timestamp. It does type-check clean under strict.

    package.json

  16. 16

    Two entry points nothing imports

    There is a HeadlessCreateDevelopmentEnvironment and a second copy of CloneEnvironment with the prompts stripped out, and nothing in the repository imports either of them. The first also passes region.code where every other call site passes region.awsRegion.

    src/headless/

  17. 17

    One dependency is never used and one used dependency is never declared

    zx is a declared dependency and is imported nowhere. @aws-sdk/credential-provider-sso is imported by the client provider and is not in the dependency list, so it resolves only as a transitive install.

    package.json

  18. 18

    A client that is built and never called

    The provider builds six AWS clients and caches them per ClassName-region, and getSecretsManagerClient is defined but called from nowhere.

    src/aws/aws-client-provider.ts:52

05 Β· WHAT IS NOT HERE

What I took out and how much of it the source holds.

Every one of these was replaced with a token in the JSON before the page was built, so the raw value has no path to the browser. On every row except the last three the count is how many times that string occurs in src/ and the package manifests, which is why the package scope is counting package.json and its lock file rather than any TypeScript. The three rows marked never read, never copied are the live credentials sitting in the untracked config/ folder, and their number counts separate secret values rather than occurrences. The third-party products are described by what they do rather than named, because a named vendor sitting next to everything else on this page would give the client away.

whathow manywhat you see instead
AWS account id6<redacted:aws-account-id>
npm package scope3<redacted:npm-scope>
SSO profile name2<redacted:aws-profile>
SSO start URL1<redacted:sso-start-url>
IAM service role ARN3<redacted:iam-role-arn>
Cognito import role ARN2<redacted:service-role-arn>
Company domain7<redacted:company-domain>
Tenant directory domain3<redacted:tenant-domain>
Bitbucket workspace4<redacted:bitbucket-workspace>
Repository slug4<redacted:repo>
Amplify app-name template2<redacted:app-name>
Hardcoded SSM parameter path2<redacted:ssm-app-path>
Amplify resource logical ids2<redacted:resource-id>
PreSignup allow-list parameter2<redacted:presignup-parameter>
Container registry image3<redacted:container-image>
Auto-branch creation patterns4<redacted:branch-pattern>
Third-party vendor names44described, never named
Long-lived AWS access key pair2never read, never copied
Third-party API keys and secrets4never read, never copied
Analytics container id1never read, never copied

The credentials sitting in the config folder should be rotated whether or not this page exists, and there is no git history to scrub because there is no git.

The thing itself

@<redacted:npm-scope>/flightdeck v1.0.0: 46 TypeScript files, 2,877 lines and 15 JSON config files. It builds with tsc and type-checks clean under strict, and npm test is echo "Error: no test specified" && exit 1.

How faithful the terminal is

The prompt strings, choice names, descriptions, separators, defaults, short codes and printed lines are quoted from the source. The tree is a hand-built state machine that mirrors the call graph: 56 nodes over 7 commands, walking 43 of the 47 call sites in the manifest. Those 47 are the 37 AWS SDK constructions less the one sitting in the headless file nothing imports, plus the eleven calls that are not SDK commands at all: two shell-outs, the config rewrite, four Bitbucket requests, the email-vendor POST and the three Cognito steps. The remaining 4 sit on branches this tree does not model.

What is invented

App names, environment names, table names, DynamoDB suffixes, job ids, pool ids, counts and every response body. All of it comes from a deliberately fictional vocabulary, prints in violet and never leaves this page.

The data

One hand-authored file, redacted at authoring time and read at build time: command-surface.json. It is the only source the terminal has.

Source: the flightdeck TypeScript, its JSON config files and its package manifests, read directly.← back home