Repository: pwrdrvr/microapps-core (language: python)

Issue:
Lazy Cache Version Metadata in Edge-to-Origin Lambda
## Motivations
- Each request is re-fetching metadata about versions from the DB
- This data is immutable so it can be lazily cached / saved in memory
- At this point there is not a need to use an LRU as there simply is not enough data about versions to use more than a reasonable amount of memory

## Brainstorming Options
- Denormalize the default version info (url, etc.) into the default rule record
  - This makes it so that all rules do not need to be downloaded
  - Can download only the default rule and embedded info about current default version
  - That enables building a lazy cache of other discovered versions
  - Only 1 record has to be checked when routing an app request

## To-Do
- [x] Save info into `{ [appname: string]: { [semver: string]: { ... } } }` object
- [x] Check the object first before hitting DB


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1c1ccb54..5f6cf368 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -343,7 +343,7 @@ jobs:
             --app-name ${DEMO_APP_NAME} \
             --type lambda-url \
             --startup-type direct \
-            --new-version ${PACKAGE_VERSION} \
+            --new-version 0.0.0 \
             --deployer-lambda-name ${DEPLOYER_LAMBDA_NAME} \
             --app-lambda-name ${DEMO_APP_LAMBDA_NAME} \
             --static-assets-path packages/demo-app/static_files \
@@ -358,18 +358,18 @@ jobs:
           description: 'Passed'
           state: 'success'
           sha: ${{github.event.pull_request.head.sha || github.sha}}
-          target_url: https://${{ steps.getCDKExports.outputs.edgeDomain }}${{ steps.getCDKExports.outputs.prefix }}/${{ env.DEMO_APP_NAME }}/?appver=${{ env.PACKAGE_VERSION }}
+          target_url: https://${{ steps.getCDKExports.outputs.edgeDomain }}${{ steps.getCDKExports.outputs.prefix }}/${{ env.DEMO_APP_NAME }}/?appver=0.0.0
 
       - name: Test Demo App
         run: |
           echo Testing App Frame Loading
           curl -H"accept: text/html" --fail https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/
           echo Testing App HTML Loading
-          curl -H"accept: text/html" --fail https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/${PACKAGE_VERSION}/index.html
+          curl -H"accept: text/html" --fail https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/0.0.0/index.html
           echo Testing App Method Invocation
-          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/${PACKAGE_VERSION}/serverIncrement?currValue=1"
+          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/0.0.0/serverIncrement?currValue=1"
           echo Testing App Method Invocation on _next/data Route
-          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/_next/data/${PACKAGE_VERSION}/serverIncrement?currValue=1"
+          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/_next/data/0.0.0/serverIncrement?currValue=1"
 
       - name: Publish Demo App to MicroApps - To Delete
         run: |
@@ -377,7 +377,7 @@ jobs:
             --app-name ${DEMO_APP_NAME} \
             --type lambda-url \
             --startup-type direct \
-            --new-version 0.0.0-test.1 \
+            --new-version 0.0.0-delete.1 \
             --deployer-lambda-name ${DEPLOYER_LAMBDA_NAME} \
             --app-lambda-name ${DEMO_APP_LAMBDA_NAME} \
             --static-assets-path packages/demo-app/static_files \
@@ -389,7 +389,7 @@ jobs:
         run: |
           npx microapps-publish delete \
             --app-name ${DEMO_APP_NAME} \
-            --new-version 0.0.0-test.1  \
+            --new-version 0.0.0-delete.1  \
             --deployer-lambda-name ${DEPLOYER_LAMBDA_NAME}
 
       - name: Publish Demo App to MicroApps - Root App
@@ -398,7 +398,7 @@ jobs:
             --app-name '[root]' \
             --type lambda-url \
             --startup-type direct \
-            --new-version ${PACKAGE_VERSION} \
+            --new-version 0.0.0 \
             --deployer-lambda-name ${DEPLOYER_LAMBDA_NAME} \
             --app-lambda-name ${DEMO_APP_LAMBDA_NAME} \
             --static-assets-path packages/demo-app/static_files \
@@ -420,11 +420,11 @@ jobs:
           echo Testing App Frame Loading
           curl -H"accept: text/html" --fail https://${EDGE_DOMAIN}${PREFIX}/
           echo Testing App HTML Loading
-          curl -H"accept: text/html" --fail https://${EDGE_DOMAIN}${PREFIX}/${PACKAGE_VERSION}/index.html
+          curl -H"accept: text/html" --fail https://${EDGE_DOMAIN}${PREFIX}/0.0.0/index.html
           echo Testing App Method Invocation
-          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/${PACKAGE_VERSION}/serverIncrement?currValue=1"
+          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/0.0.0/serverIncrement?currValue=1"
           echo Testing App Method Invocation on _next/data Route
-          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/_next/data/${PACKAGE_VERSION}/serverIncrement?currValue=1"
+          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/_next/data/0.0.0/serverIncrement?currValue=1"
 
       - name: Publish Nextjs Demo App to MicroApps
         run: |
@@ -598,6 +598,14 @@ jobs:
           echo Testing App Method Invocation on _next/data Route
           curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/_next/data/0.0.0-child.1/serverIncrement?currValue=1"
 
+      - name: Run Integration Tests
+        env:
+          BASE_URL: https://${{ env.EDGE_DOMAIN }}${{ env.PREFIX }}/
+          DEMO_AND_ROOT_APP_VER: '${{ env.PACKAGE_VERSION }}'
+          NEXTJS_DEMO_APP_VER: '${{ needs.build.outputs.nextjsDemoAppPackageVersion }}'
+        run: |
+          npm run test:integration
+
   build-jsii:
     if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'BUILD-JSII'))
     runs-on: ubuntu-latest
diff --git a/jest.int.config.js b/jest.int.config.js
new file mode 100644
index 00000000..7918b790
--- /dev/null
+++ b/jest.int.config.js
@@ -0,0 +1,216 @@
+/*
+ * For a detailed explanation regarding each configuration property, visit:
+ * https://jestjs.io/docs/en/configuration.html
+ */
+
+/**
+ * @type {import('jest').Config}
+ */
+module.exports = {
+  // All imported modules in your tests should be mocked automatically
+  // automock: false,
+
+  // Stop running tests after `n` failures
+  // bail: 0,
+
+  // The directory where Jest should store its cached dependency information
+  // cacheDirectory: "/private/var/folders/07/183kkj1d7l59l67s4tq0dmyr0000gn/T/jest_dx",
+
+  // Automatically clear mock calls and instances between every test
+  // clearMocks: false,
+
+  // Indicates whether the coverage information should be collected while executing the test
+  // collectCoverage: false,
+  collectCoverage: true,
+
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
+  // collectCoverageFrom: undefined,
+
+  // The directory where Jest should output its coverage files
+  coverageDirectory: 'coverage',
+
+  // An array of regexp pattern strings used to skip coverage collection
+  // coveragePathIgnorePatterns: [
+  //   "/node_modules/"
+  // ],
+  coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
+
+  // Indicates which provider should be used to instrument code for coverage
+  coverageProvider: 'v8',
+
+  // A list of reporter names that Jest uses when writing coverage reports
+  // coverageReporters: [
+  //   "json",
+  //   "text",
+  //   "lcov",
+  //   "clover"
+  // ],
+  coverageReporters: ['html', 'text', 'clover'],
+
+  // An object that configures minimum threshold enforcement for coverage results
+  // coverageThreshold: undefined,
+
+  // A path to a custom dependency extractor
+  // dependencyExtractor: undefined,
+
+  // Make calling deprecated APIs throw helpful error messages
+  // errorOnDeprecated: false,
+
+  // Force coverage collection from ignored files using an array of glob patterns
+  // forceCoverageMatch: [],
+
+  // A path to a module which exports an async function that is triggered once before all test suites
+  // globalSetup: undefined,
+
+  // A path to a module which exports an async function that is triggered once after all test suites
+  // globalTeardown: undefined,
+
+  // A set of global variables that need to be available in all test environments
+  // globals: {},
+
+  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
+  // maxWorkers: "50%",
+
+  // An array of directory names to be searched recursively up from the requiring module's location
+  // moduleDirectories: [
+  //   "node_modules"
+  // ],
+
+  // An array of file extensions your modules use
+  // moduleFileExtensions: [
+  //   "js",
+  //   "json",
+  //   "jsx",
+  //   "ts",
+  //   "tsx",
+  //   "node"
+  // ],
+  moduleFileExtensions: ['ts', 'js'],
+
+  // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
+  // moduleNameMapper: {},
+
+  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
+  // modulePathIgnorePatterns: [],
+
+  // Activates notifications for test results
+  // notify: false,
+
+  // An enum that specifies notification mode. Requires { notify: true }
+  // notifyMode: "failure-change",
+
+  // A preset that is used as a base for Jest's configuration
+  // preset: undefined,
+  preset: 'ts-jest',
+
+  // Run tests from one or more projects
+  // projects: undefined,
+
+  // Use this configuration option to add custom reporters to Jest
+  // reporters: undefined,
+
+  // Automatically reset mock state between every test
+  // resetMocks: false,
+
+  // Reset the module registry before running each individual test
+  // resetModules: false,
+
+  // A path to a custom resolver
+  // resolver: undefined,
+
+  // Automatically restore mock state between every test
+  // restoreMocks: false,
+
+  // The root directory that Jest should scan for tests and modules within
+  // rootDir: undefined,
+
+  // A list of paths to directories that Jest should use to search for files in
+  // roots: [
+  //   "<rootDir>"
+  // ],
+  roots: ['<rootDir>tests/integration/'],
+
+  // Allows you to use a custom runner instead of Jest's default test runner
+  // runner: "jest-runner",
+
+  // The paths to modules that run some code to configure or set up the testing environment before each test
+  // setupFiles: [],
+  // https://github.com/freshollie/jest-dynalite
+  // setupFiles: ['./setupBeforeEnv.js'],
+
+  // A list of paths to modules that run some code to configure or set up the testing framework before each test
+  // setupFilesAfterEnv: [],
+
+  // The number of seconds after which a test is considered as slow and reported as such in the results.
+  // slowTestThreshold: 5,
+
+  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
+  // snapshotSerializers: [],
+
+  // The test environment that will be used for testing
+  testEnvironment: 'node',
+
+  // Options that will be passed to the testEnvironment
+  // testEnvironmentOptions: {},
+
+  // Adds a location field to test results
+  // testLocationInResults: false,
+
+  // The glob patterns Jest uses to detect test files
+  // testMatch: [
+  //   "**/__tests__/**/*.[jt]s?(x)",
+  //   "**/?(*.)+(spec|test).[tj]s?(x)"
+  // ],
+  testMatch: ['**/(*.)+spec.ts?(x)'],
+
+  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
+  // testPathIgnorePatterns: [
+  //   "/node_modules/"
+  // ],
+
+  // The regexp pattern or array of patterns that Jest uses to detect test files
+  // testRegex: [],
+
+  // This option allows the use of a custom results processor
+  // testResultsProcessor: undefined,
+
+  // This option allows use of a custom test runner
+  // testRunner: "jasmine2",
+
+  // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
+  // testURL: "http://localhost",
+
+  // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
+  // timers: "real",
+
+  // A map from regular expressions to paths to transformers
+  // transform: undefined,
+  transform: {
+    '^.+\\.tsx?$': [
+      'ts-jest',
+      {
+        tsconfig: 'tsconfig.packages.json',
+        // Reduces test time from 85 seconds to 20-25 seconds on a 2018 MacBook Pro 13" Core i7
+        isolatedModules: true,
+      },
+    ],
+  },
+
+  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
+  // transformIgnorePatterns: [
+  //   "/node_modules/",
+  //   "\\.pnp\\.[^\\/]+$"
+  // ],
+
+  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
+  // unmockedModulePathPatterns: undefined,
+
+  // Indicates whether each individual test should be reported during the run
+  // verbose: undefined,
+
+  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
+  // watchPathIgnorePatterns: [],
+
+  // Whether to use watchman for file crawling
+  // watchman: true,
+};
diff --git a/package.json b/package.json
index 67f59af4..ed2b7c1e 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,6 @@
     "clean:tsbuildinfo": "npm exec --workspaces -- npx rimraf tsconfig.tsbuildinfo",
     "cloc": "cloc --exclude-dir=node_modules,dist,distb,cdk.out --exclude-ext=json .",
     "format": "prettier --write \"packages/**/*.{js,ts}\"",
-    "start:publish": "node packages/microapps-publish/dist/src/index.js",
-    "start:publish:debug": "ts-node --inspect=5858 --debug-brk --ignore false packages/microapps-publish/src/index.ts",
     "build": "tsc --build tsconfig.json",
     "build:publish": "tsc --build tsconfig.publish.json",
     "esbuild:deployer": "esbuild packages/microapps-deployer/src/index.ts --bundle --minify --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=packages/cdk/dist/microapps-deployer/index.js",
@@ -44,15 +42,17 @@
     "test": "AWS_PROFILE= AWS_EMF_ENVIRONMENT=Local jest",
     "lint": "eslint ./ --ext .ts --ext .tsx",
     "lint-and-fix": "eslint ./ --ext .ts --ext .tsx --fix",
-    "postinstall": "npx patch-package"
+    "postinstall": "npx patch-package",
+    "test:integration": "DEMO_APP_URL=${DEMO_APP_URL:-https://apps.ghpublic.pwrdrvr.com/demo-app} jest --config ./jest.int.config.js"
   },
   "devDependencies": {
-    "@types/jest": "29.4.0",
+    "@types/jest": "29.5.0",
     "@types/node": "16.9.2",
     "@typescript-eslint/eslint-plugin": "5.54.0",
     "@typescript-eslint/parser": "5.54.0",
     "aws-cdk": "^2.24.1",
     "aws-cdk-lib": "2.24.1",
+    "axios": "1.3.4",
     "constructs": "10.0.5",
     "cross-env": "7.0.3",
     "esbuild": "0.17.11",
@@ -63,7 +63,7 @@
     "eslint-plugin-import": "2.27.5",
     "eslint-plugin-prettier": "4.2.1",
     "husky": "8.0.3",
-    "jest": "29.4.3",
+    "jest": "29.5.0",
     "jest-dynalite": "3.6.1",
     "lint-staged": "12.1.2",
     "patch-package": "6.5.1",
diff --git a/packages/cdk/bin/cdk.ts b/packages/cdk/bin/cdk.ts
index f78048e5..dc607ed5 100644
--- a/packages/cdk/bin/cdk.ts
+++ b/packages/cdk/bin/cdk.ts
@@ -4,7 +4,6 @@ import { App, Environment } from 'aws-cdk-lib';
 import { MicroAppsStack } from '../lib/MicroApps';
 import { MicroAppsChildStack } from '../lib/MicroAppsChild';
 import { MicroAppsChildPrivStack } from '../lib/MicroAppsChildPriv';
-import { MicroAppsBuilder } from '../lib/MicroAppsBuilder';
 import { SharedProps } from '../lib/SharedProps';
 
 const app = new App();
@@ -90,9 +89,3 @@ new MicroAppsStack(app, 'microapps-basic-prefix', {
   originRegion: shared.region,
   tableName: `microapps-basic-prefix-ghpublic${shared.envSuffix}${shared.prSuffix}`,
 });
-
-new MicroAppsBuilder(app, 'microapps-builder', {
-  stackName: `microapps-ghpublic-builder${shared.envSuffix}`,
-  shared,
-  env,
-});
diff --git a/packages/cdk/lib/MicroAppsBuilder.ts b/packages/cdk/lib/MicroAppsBuilder.ts
deleted file mode 100644
index 5262d5cf..00000000
--- a/packages/cdk/lib/MicroAppsBuilder.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import { Construct } from 'constructs';
-import * as iam from 'aws-cdk-lib/aws-iam';
-import { Duration, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
-import { SharedProps } from './SharedProps';
-import { SharedTags } from './SharedTags';
-
-/**
- * Properties to initialize an instance of `MicroAppsBuilderStack`.
- */
-interface IMicroAppsBuilderStackProps extends StackProps {
-  readonly shared: SharedProps;
-}
-
-/**
- * Creates an IAM Role with permissions sufficient to deploy
- * {@link MicroAppsStack }
- */
-export class MicroAppsBuilder extends Stack {
-  constructor(scope: Construct, id: string, props?: IMicroAppsBuilderStackProps) {
-    super(scope, id, props);
-
-    if (props === undefined) {
-      throw new Error('props cannot be undefined');
-    }
-    if (props.env === undefined) {
-      throw new Error('props.env cannot be undefined');
-    }
-
-    const { shared } = props;
-
-    SharedTags.addSharedTags(Stack.of(this));
-
-    // This stack is deployed manually once per env, it's never a temp stack
-    SharedTags.addEnvTag(Stack.of(this), shared.env, false);
-
-    const nameRoot = Stack.of(this).stackName;
-
-    //
-    // Create a policy that grants permissions needed to deploy the stack
-    //
-    // const policyBuilder = new iam.Policy(this, 'microapps-builder-policy', {
-    //   policyName: nameRoot,
-    // });
-    // policyBuilder.addStatements(
-    //   new iam.PolicyStatement({
-    //     sid: 's3-create-buckets',
-    //     actions: ['s3:*'],
-    //     resources: [`arn:aws:s3:::*-${nameRoot}*`, `arn:aws:s3:::*-microapps-staging*`],
-    //   }),
-    //   // FIXME: Limit delete by name pattern or condition
-    //   new iam.PolicyStatement({
-    //     sid: 'lambda-create',
-    //     actions: ['lambda:*'],
-    //     resources: ['*'],
-    //   }),
-    //   // FIXME: Limit delete by name pattern or condition
-    //   new iam.PolicyStatement({
-    //     sid: 'apigwy-create',
-    //     actions: ['apigateway:*'],
-    //     resources: ['*'],
-    //   }),
-    //   // FIXME: Limit delete by name pattern or condition
-    //   new iam.PolicyStatement({
-    //     sid: 'cloudfront-create',
-    //     actions: ['cloudfront:*'],
-    //     resources: ['*'],
-    //   }),
-    //   // FIXME: Limit delete by name pattern or condition
-    //   new iam.PolicyStatement({
-    //     sid: 'route53-create',
-    //     actions: ['route53:*'],
-    //     resources: ['*'],
-    //   }),
-    // );
-
-    const builderRole = new iam.Role(this, 'builder-role', {
-      managedPolicies: [
-        iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCertificateManagerReadOnly'),
-        iam.ManagedPolicy.fromAwsManagedPolicyName('CloudFrontFullAccess'),
-        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonAPIGatewayAdministrator'),
-        iam.ManagedPolicy.fromAwsManagedPolicyName('AWSLambda_FullAccess'),
-        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'),
-        iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchFullAccess'),
-        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess'),
-        // FIXME: Limit this to Role and Policy creation only (no users)
-        iam.ManagedPolicy.fromAwsManagedPolicyName('IAMFullAccess'),
-        iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCloudFormationFullAccess'),
-        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonRoute53FullAccess'),
-      ],
-      roleName: `${nameRoot}-builder-role`,
-      assumedBy: new iam.WebIdentityPrincipal(
-        `arn:aws:iam::${props.shared.account}:oidc-provider/token.actions.githubusercontent.com`,
-        {
-          StringEquals: { 'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com' },
-          StringLike: {
-            // Full example here of setting up OIDC via CDK with GitHub:
-            // https://github.com/WojciechMatuszewski/github-oidc-aws-cdk-example
-            // Notice the `ref:refs`. The `s` in the second `ref` is important!
-            // `repo:${yourGitHubUsername}/${yourGitHubRepoName}:ref:refs/heads/${yourGitHubBranchName}`
-            'token.actions.githubusercontent.com:sub': 'repo:pwrdrvr/*',
-          },
-        },
-      ),
-      maxSessionDuration: Duration.hours(1),
-    });
-
-    // Add permissions needed by the TTL construct
-    builderRole.addToPolicy(
-      new iam.PolicyStatement({
-        actions: [
-          'events:PutRule',
-          'events:DeleteRule',
-          'events:DescribeRule',
-          'events:PutTargets',
-          'events:RemoveTargets',
-        ],
-        resources: ['*'],
-      }),
-    );
-
-    // Always destroy this role if the stack is destroyed
-    // This role is not critical
-    builderRole.applyRemovalPolicy(RemovalPolicy.DESTROY);
-  }
-}
diff --git a/packages/microapps-deployer/package.json b/packages/microapps-deployer/package.json
index 1986cbe5..9fa6c526 100644
--- a/packages/microapps-deployer/package.json
+++ b/packages/microapps-deployer/package.json
@@ -48,7 +48,7 @@
     "@types/node": "16.9.2",
     "@types/sinon": "^10.0.0",
     "@types/source-map-support": "0.5.6",
-    "aws-sdk-client-mock": "^0.3.0",
+    "aws-sdk-client-mock": "2.1.0",
     "sinon": "^10.0.0"
   }
 }
diff --git a/packages/microapps-edge-to-origin/package.json b/packages/microapps-edge-to-origin/package.json
index 537a34a3..1fd5f4fc 100644
--- a/packages/microapps-edge-to-origin/package.json
+++ b/packages/microapps-edge-to-origin/package.json
@@ -12,7 +12,7 @@
     "@types/convict-format-with-validator": "^6.0.2",
     "@types/lambda-log": "^2.2.0",
     "@types/node": "16.9.2",
-    "aws-sdk-client-mock": "^0.6.2"
+    "aws-sdk-client-mock": "2.1.0"
   },
   "dependencies": {
     "@aws-crypto/sha256-browser": "4.0.0",
diff --git a/packages/microapps-edge-to-origin/src/index.route.prefix.spec.ts b/packages/microapps-edge-to-origin/src/index.route.prefix.spec.ts
index b78a91ad..e0f816b9 100644
--- a/packages/microapps-edge-to-origin/src/index.route.prefix.spec.ts
+++ b/packages/microapps-edge-to-origin/src/index.route.prefix.spec.ts
@@ -3,6 +3,7 @@ import 'reflect-metadata';
 import 'jest-dynalite/withDb';
 import * as dynamodb from '@aws-sdk/client-dynamodb';
 import { Application, DBManager, Version, Rules } from '@pwrdrvr/microapps-datalib';
+import { AppVersionCache } from '@pwrdrvr/microapps-router-lib/src/app-cache';
 import * as lambda from 'aws-lambda';
 import { Config, IConfig } from './config/config';
 jest.mock('./config/config');
@@ -49,6 +50,9 @@ describe('edge-to-origin - routing - without prefix', () => {
 
     overrideDBManager({ dbManager, dynamoClient });
 
+    const appVersionCache = AppVersionCache.GetInstance({ dbManager });
+    appVersionCache.ClearCache();
+
     // Reset the config that's visible to the handler back to defaults
     Object.keys(origConfig).forEach((key) => {
       (theConfig as { [index: string]: unknown })[key] = (
diff --git a/packages/microapps-edge-to-origin/src/index.route.root.spec.ts b/packages/microapps-edge-to-origin/src/index.route.root.spec.ts
index 87172a5e..d327ba1d 100644
--- a/packages/microapps-edge-to-origin/src/index.route.root.spec.ts
+++ b/packages/microapps-edge-to-origin/src/index.route.root.spec.ts
@@ -4,8 +4,7 @@ import 'jest-dynalite/withDb';
 import * as dynamodb from '@aws-sdk/client-dynamodb';
 import { Application, DBManager, Version, Rules } from '@pwrdrvr/microapps-datalib';
 import * as lambda from 'aws-lambda';
-import { Config, IConfig } from './config/config';
-jest.mock('./config/config');
+import { IConfig } from './config/config';
 type Writeable<T> = { -readonly [P in keyof T]: T[P] };
 const theConfig: Writeable<IConfig> = {
   awsAccountID: '00123456',
@@ -17,15 +16,6 @@ const theConfig: Writeable<IConfig> = {
   tableName: '',
   rootPathPrefix: '',
 };
-const origConfig = { ...theConfig };
-Object.defineProperty(Config, 'instance', {
-  configurable: false,
-  enumerable: false,
-  get: jest.fn((): IConfig => {
-    return theConfig;
-  }),
-});
-import { handler, overrideDBManager } from './index';
 
 let dynamoClient: dynamodb.DynamoDBClient;
 let dbManager: DBManager;
@@ -46,812 +36,171 @@ describe('edge-to-origin - routing - without prefix', () => {
 
   beforeEach(() => {
     jest.resetModules(); // Most important - it clears the cache
-
-    overrideDBManager({ dbManager, dynamoClient });
-
-    // Reset the config that's visible to the handler back to defaults
-    Object.keys(origConfig).forEach((key) => {
-      (theConfig as { [index: string]: unknown })[key] = (
-        origConfig as { [index: string]: unknown }
-      )[key];
-    });
   });
 
   it('should serve appframe for [root] with version and default file substitued', async () => {
-    const app = new Application({
-      AppName: 'Bat',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: 'Bat',
-      DefaultFile: 'bat.html',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await version.Save(dbManager);
-
-    const rules = new Rules({
-      AppName: 'Bat',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
-
-    const rootApp = new Application({
-      AppName: '[root]',
-      DisplayName: 'Root App',
-    });
-    await rootApp.Save(dbManager);
-
-    const versionRoot = new Version({
-      AppName: '[root]',
-      DefaultFile: 'someOtherFile.html',
-      IntegrationID: 'abcd',
-      SemVer: '1.2.3',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await versionRoot.Save(dbManager);
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
 
-    const rulesRoot = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
-    });
-    await rulesRoot.Save(dbManager);
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/',
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toBeDefined();
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse?.status).toBe('200');
-    expect(responseResponse).toHaveProperty('body');
-    expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/1.2.3/someOtherFile.html" seamless');
-  });
+      const app = new Application({
+        AppName: 'Bat',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
 
-  it('should route `iframe` app request with [root]/version to origin', async () => {
-    theConfig.replaceHostHeader = true;
+      const version = new Version({
+        AppName: 'Bat',
+        DefaultFile: 'bat.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await version.Save(dbManager);
 
-    const app = new Application({
-      AppName: 'Bat',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: 'Bat',
-      IntegrationID: 'abcd',
-      SemVer: '4.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
-    });
-    await version.Save(dbManager);
+      const rules = new Rules({
+        AppName: 'Bat',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
 
-    const rules = new Rules({
-      AppName: 'Bat',
-      Version: 0,
-      RuleSet: { default: { SemVer: '4.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
+      const rootApp = new Application({
+        AppName: '[root]',
+        DisplayName: 'Root App',
+      });
+      await rootApp.Save(dbManager);
 
-    const rootApp = new Application({
-      AppName: '[root]',
-      DisplayName: 'Root App',
-    });
-    await rootApp.Save(dbManager);
-
-    const versionRoot = new Version({
-      AppName: '[root]',
-      IntegrationID: 'abcd',
-      SemVer: '1.2.3',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
-    });
-    await versionRoot.Save(dbManager);
+      const versionRoot = new Version({
+        AppName: '[root]',
+        DefaultFile: 'someOtherFile.html',
+        IntegrationID: 'abcd',
+        SemVer: '1.2.3',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await versionRoot.Save(dbManager);
 
-    const rulesRoot = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
-    });
-    await rulesRoot.Save(dbManager);
+      const rulesRoot = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
+      });
+      await rulesRoot.Save(dbManager);
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {
-                  host: [
-                    {
-                      key: 'Host',
-                      value: 'zyz.cloudfront.net',
-                    },
-                  ],
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
                 },
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/1.2.3',
-                origin: {
-                  custom: {
-                    customHeaders: {},
-                    domainName: 'zyz.cloudfront.net',
-                    keepaliveTimeout: 5,
-                    path: '',
-                    port: 443,
-                    protocol: 'https',
-                    readTimeout: 30,
-                    sslProtocols: ['TLSv1.2'],
-                  },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/',
                 },
               },
             },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const requestResponse = response as lambda.CloudFrontRequest;
-    expect(requestResponse).toBeDefined();
-    expect(requestResponse).not.toHaveProperty('status');
-    expect(requestResponse).not.toHaveProperty('body');
-    expect(requestResponse).toHaveProperty('headers');
-    expect(requestResponse.headers).toHaveProperty('host');
-    expect(requestResponse.headers.host).toHaveLength(1);
-    expect(requestResponse.headers.host[0].key).toBe('Host');
-    expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
-    expect(requestResponse).toHaveProperty('origin');
-    expect(requestResponse.origin).toHaveProperty('custom');
-    expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
-    expect(requestResponse?.origin?.custom?.domainName).toBe(
-      'abc123456.lambda-url.us-east-1.on.aws',
-    );
-  });
-
-  it('should route `iframe` /someMethod request for [root] to origin', async () => {
-    theConfig.replaceHostHeader = true;
-
-    const app = new Application({
-      AppName: 'Bat',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: 'Bat',
-      IntegrationID: 'abcd',
-      SemVer: '4.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
-    });
-    await version.Save(dbManager);
-
-    const rules = new Rules({
-      AppName: 'Bat',
-      Version: 0,
-      RuleSet: { default: { SemVer: '4.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
-
-    const rootApp = new Application({
-      AppName: '[root]',
-      DisplayName: 'Root App',
-    });
-    await rootApp.Save(dbManager);
-
-    const versionRoot = new Version({
-      AppName: '[root]',
-      IntegrationID: 'abcd',
-      SemVer: '1.2.3',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
-    });
-    await versionRoot.Save(dbManager);
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
 
-    const rulesRoot = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toBeDefined();
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse?.status).toBe('200');
+      expect(responseResponse).toHaveProperty('body');
+      expect(responseResponse.body?.length).toBeGreaterThan(80);
+      expect(responseResponse.body).toContain('<iframe src="/1.2.3/someOtherFile.html" seamless');
     });
-    await rulesRoot.Save(dbManager);
-
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {
-                  host: [
-                    {
-                      key: 'Host',
-                      value: 'zyz.cloudfront.net',
-                    },
-                  ],
-                },
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/1.2.3/someMethod',
-                origin: {
-                  custom: {
-                    customHeaders: {},
-                    domainName: 'zyz.cloudfront.net',
-                    keepaliveTimeout: 5,
-                    path: '',
-                    port: 443,
-                    protocol: 'https',
-                    readTimeout: 30,
-                    sslProtocols: ['TLSv1.2'],
-                  },
-                },
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const requestResponse = response as lambda.CloudFrontRequest;
-    expect(requestResponse).toBeDefined();
-    expect(requestResponse).not.toHaveProperty('status');
-    expect(requestResponse).not.toHaveProperty('body');
-    expect(requestResponse).toHaveProperty('headers');
-    expect(requestResponse.headers).toHaveProperty('host');
-    expect(requestResponse.headers.host).toHaveLength(1);
-    expect(requestResponse.headers.host[0].key).toBe('Host');
-    expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
-    expect(requestResponse).toHaveProperty('origin');
-    expect(requestResponse.origin).toHaveProperty('custom');
-    expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
-    expect(requestResponse?.origin?.custom?.domainName).toBe(
-      'abc123456.lambda-url.us-east-1.on.aws',
-    );
   });
 
-  it('should route `direct` app request with appName to origin', async () => {
-    theConfig.replaceHostHeader = true;
-
-    const app = new Application({
-      AppName: 'BatDirect',
-      DisplayName: 'Direct Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: 'BatDirect',
-      SemVer: '1.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'direct',
-      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
-    });
-    await version.Save(dbManager);
-
-    const rules = new Rules({
-      AppName: 'BatDirect',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
-
-    const rootApp = new Application({
-      AppName: '[root]',
-      DisplayName: 'Root App',
-    });
-    await rootApp.Save(dbManager);
-
-    const versionRoot = new Version({
-      AppName: '[root]',
-      IntegrationID: 'abcd',
-      SemVer: '1.2.3',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'direct',
-      URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
-    });
-    await versionRoot.Save(dbManager);
-
-    const rulesRoot = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
-    });
-    await rulesRoot.Save(dbManager);
+  it('should route `iframe` app request with [root]/version to origin', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {
-                  host: [
-                    {
-                      key: 'Host',
-                      value: 'zyz.cloudfront.net',
-                    },
-                  ],
-                },
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/',
-                origin: {
-                  custom: {
-                    customHeaders: {},
-                    domainName: 'zyz.cloudfront.net',
-                    keepaliveTimeout: 5,
-                    path: '',
-                    port: 443,
-                    protocol: 'https',
-                    readTimeout: 30,
-                    sslProtocols: ['TLSv1.2'],
-                  },
-                },
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const requestResponse = response as lambda.CloudFrontRequest;
-    expect(requestResponse).toBeDefined();
-    expect(requestResponse).not.toHaveProperty('status');
-    expect(requestResponse).not.toHaveProperty('body');
-    expect(requestResponse).toHaveProperty('headers');
-    expect(requestResponse.headers).toHaveProperty('host');
-    expect(requestResponse.headers.host).toHaveLength(1);
-    expect(requestResponse.headers.host[0].key).toBe('Host');
-    expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
-    expect(requestResponse).toHaveProperty('origin');
-    expect(requestResponse.origin).toHaveProperty('custom');
-    expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
-    expect(requestResponse?.origin?.custom?.domainName).toBe(
-      'abc123456.lambda-url.us-east-1.on.aws',
-    );
-  });
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
 
-  it('should route `direct` _next/data request for [root] and version to origin', async () => {
-    theConfig.replaceHostHeader = true;
+      theConfig.replaceHostHeader = true;
 
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Direct Root App',
-    });
-    await app.Save(dbManager);
+      const app = new Application({
+        AppName: 'Bat',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
 
-    {
       const version = new Version({
-        AppName: '[root]]',
-        SemVer: '1.2.1-beta.1',
+        AppName: 'Bat',
+        IntegrationID: 'abcd',
+        SemVer: '4.2.1-beta.1',
         Status: 'deployed',
         Type: 'lambda-url',
-        StartupType: 'direct',
         URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
       });
       await version.Save(dbManager);
-    }
 
-    {
-      const version = new Version({
+      const rules = new Rules({
+        AppName: 'Bat',
+        Version: 0,
+        RuleSet: { default: { SemVer: '4.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      const rootApp = new Application({
+        AppName: '[root]',
+        DisplayName: 'Root App',
+      });
+      await rootApp.Save(dbManager);
+
+      const versionRoot = new Version({
         AppName: '[root]',
-        SemVer: '1.2.2-beta.1',
+        IntegrationID: 'abcd',
+        SemVer: '1.2.3',
         Status: 'deployed',
         Type: 'lambda-url',
-        StartupType: 'direct',
-        URL: 'https://abc1234567.lambda-url.us-east-1.on.aws/',
+        URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
       });
-      await version.Save(dbManager);
-    }
-
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {
-                  host: [
-                    {
-                      key: 'Host',
-                      value: 'zyz.cloudfront.net',
-                    },
-                  ],
-                },
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/_next/data/1.2.2-beta.1/abc123.json',
-                origin: {
-                  custom: {
-                    customHeaders: {},
-                    domainName: 'zyz.cloudfront.net',
-                    keepaliveTimeout: 5,
-                    path: '',
-                    port: 443,
-                    protocol: 'https',
-                    readTimeout: 30,
-                    sslProtocols: ['TLSv1.2'],
-                  },
-                },
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const requestResponse = response as lambda.CloudFrontRequest;
-    expect(requestResponse).toBeDefined();
-    expect(requestResponse).not.toHaveProperty('status');
-    expect(requestResponse).not.toHaveProperty('body');
-    expect(requestResponse).toHaveProperty('headers');
-    expect(requestResponse.headers).toHaveProperty('host');
-    expect(requestResponse.headers.host).toHaveLength(1);
-    expect(requestResponse.headers.host[0].key).toBe('Host');
-    expect(requestResponse.headers.host[0].value).toBe('abc1234567.lambda-url.us-east-1.on.aws');
-    expect(requestResponse.uri).toBe('/_next/data/1.2.2-beta.1/abc123.json');
-    expect(requestResponse).toHaveProperty('origin');
-    expect(requestResponse.origin).toHaveProperty('custom');
-    expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
-    expect(requestResponse?.origin?.custom?.domainName).toBe(
-      'abc1234567.lambda-url.us-east-1.on.aws',
-    );
-  });
-
-  it('should route `iframe` _next/data request for [root] and version to origin', async () => {
-    theConfig.replaceHostHeader = true;
-
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Iframe Bat App',
-    });
-    await app.Save(dbManager);
-
-    const versionDefault = new Version({
-      AppName: '[root]',
-      SemVer: '1.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'iframe',
-      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
-    });
-    await versionDefault.Save(dbManager);
-    const versionNonDefault = new Version({
-      AppName: '[root]',
-      SemVer: '1.2.3',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'iframe',
-      URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
-    });
-    await versionNonDefault.Save(dbManager);
-
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {
-                  host: [
-                    {
-                      key: 'Host',
-                      value: 'zyz.cloudfront.net',
-                    },
-                  ],
-                },
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/1.2.3/_next/data/3n-GzDjz4nifw5OnywVdV/posts/ssg-ssr.json',
-                origin: {
-                  custom: {
-                    customHeaders: {},
-                    domainName: 'zyz.cloudfront.net',
-                    keepaliveTimeout: 5,
-                    path: '',
-                    port: 443,
-                    protocol: 'https',
-                    readTimeout: 30,
-                    sslProtocols: ['TLSv1.2'],
-                  },
-                },
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const requestResponse = response as lambda.CloudFrontRequest;
-    expect(requestResponse).toBeDefined();
-    expect(requestResponse).not.toHaveProperty('status');
-    expect(requestResponse).not.toHaveProperty('body');
-    expect(requestResponse).toHaveProperty('headers');
-    expect(requestResponse.headers).toHaveProperty('host');
-    expect(requestResponse.headers.host).toHaveLength(1);
-    expect(requestResponse.headers.host[0].key).toBe('Host');
-    expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
-    expect(requestResponse.uri).toBe('/1.2.3/_next/data/3n-GzDjz4nifw5OnywVdV/posts/ssg-ssr.json');
-    expect(requestResponse).toHaveProperty('origin');
-    expect(requestResponse.origin).toHaveProperty('custom');
-    expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
-    expect(requestResponse?.origin?.custom?.domainName).toBe(
-      'abc123456.lambda-url.us-east-1.on.aws',
-    );
-  });
-
-  it('should 404 `direct` request for [root] and ?appver=', async () => {
-    theConfig.replaceHostHeader = true;
-
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Direct Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: '[root]',
-      SemVer: '1.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'direct',
-      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
-    });
-    await version.Save(dbManager);
-
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {
-                  host: [
-                    {
-                      key: 'Host',
-                      value: 'zyz.cloudfront.net',
-                    },
-                  ],
-                },
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/?appver=1.2.2-beta.1',
-                origin: {
-                  custom: {
-                    customHeaders: {},
-                    domainName: 'zyz.cloudfront.net',
-                    keepaliveTimeout: 5,
-                    path: '',
-                    port: 443,
-                    protocol: 'https',
-                    readTimeout: 30,
-                    sslProtocols: ['TLSv1.2'],
-                  },
-                },
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toBeDefined();
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse?.status).toBe('404');
-    expect(responseResponse).not.toHaveProperty('body');
-  });
-
-  it('should route `iframe` app request for [root] by creating iframe response for ?appver=[version]', async () => {
-    theConfig.replaceHostHeader = true;
-
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Direct Bat App',
-    });
-    await app.Save(dbManager);
-
-    const versionDefault = new Version({
-      AppName: '[root]',
-      SemVer: '1.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'iframe',
-      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
-    });
-    await versionDefault.Save(dbManager);
-    const versionNonDefault = new Version({
-      AppName: '[root]',
-      SemVer: '1.2.3',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'iframe',
-      URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
-    });
-    await versionNonDefault.Save(dbManager);
+      await versionRoot.Save(dbManager);
 
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
+      const rulesRoot = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
+      });
+      await rulesRoot.Save(dbManager);
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {
-                  host: [
-                    {
-                      key: 'Host',
-                      value: 'zyz.cloudfront.net',
-                    },
-                  ],
-                },
-                method: 'GET',
-                querystring: 'appver=1.2.3',
-                clientIp: '1.1.1.1',
-                uri: '/',
-                origin: {
-                  custom: {
-                    customHeaders: {},
-                    domainName: 'zyz.cloudfront.net',
-                    keepaliveTimeout: 5,
-                    path: '',
-                    port: 443,
-                    protocol: 'https',
-                    readTimeout: 30,
-                    sslProtocols: ['TLSv1.2'],
-                  },
-                },
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toBeDefined();
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse?.status).toBe('200');
-    expect(responseResponse).toHaveProperty('body');
-    expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/1.2.3" seamless');
-
-    //
-    // Make sure request with version in path AND ?appver does not add to the path
-    //
-    {
       // Call the handler
       // @ts-expect-error no callback
       const response = await handler(
@@ -875,7 +224,7 @@ describe('edge-to-origin - routing - without prefix', () => {
                     ],
                   },
                   method: 'GET',
-                  querystring: 'appver=1.2.3',
+                  querystring: '',
                   clientIp: '1.1.1.1',
                   uri: '/1.2.3',
                   origin: {
@@ -900,19 +249,86 @@ describe('edge-to-origin - routing - without prefix', () => {
 
       const requestResponse = response as lambda.CloudFrontRequest;
       expect(requestResponse).toBeDefined();
-      expect(requestResponse.uri).toBe('/1.2.3');
-      expect(requestResponse.origin?.custom?.domainName).toBe(
-        'abc123456.lambda-url.us-east-1.on.aws',
-      );
       expect(requestResponse).not.toHaveProperty('status');
       expect(requestResponse).not.toHaveProperty('body');
-    }
+      expect(requestResponse).toHaveProperty('headers');
+      expect(requestResponse.headers).toHaveProperty('host');
+      expect(requestResponse.headers.host).toHaveLength(1);
+      expect(requestResponse.headers.host[0].key).toBe('Host');
+      expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
+      expect(requestResponse).toHaveProperty('origin');
+      expect(requestResponse.origin).toHaveProperty('custom');
+      expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+      expect(requestResponse?.origin?.custom?.domainName).toBe(
+        'abc123456.lambda-url.us-east-1.on.aws',
+      );
+    });
+  });
+
+  it('should route `iframe` /someMethod request for [root] to origin', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      theConfig.replaceHostHeader = true;
+
+      const app = new Application({
+        AppName: 'Bat',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: 'Bat',
+        IntegrationID: 'abcd',
+        SemVer: '4.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: 'Bat',
+        Version: 0,
+        RuleSet: { default: { SemVer: '4.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      const rootApp = new Application({
+        AppName: '[root]',
+        DisplayName: 'Root App',
+      });
+      await rootApp.Save(dbManager);
+
+      const versionRoot = new Version({
+        AppName: '[root]',
+        IntegrationID: 'abcd',
+        SemVer: '1.2.3',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
+      });
+      await versionRoot.Save(dbManager);
+
+      const rulesRoot = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
+      });
+      await rulesRoot.Save(dbManager);
 
-    //
-    // Request with non-matching version in path and conflicting
-    // ?appver should use version in path already
-    //
-    {
       // Call the handler
       // @ts-expect-error no callback
       const response = await handler(
@@ -936,9 +352,9 @@ describe('edge-to-origin - routing - without prefix', () => {
                     ],
                   },
                   method: 'GET',
-                  querystring: 'appver=1.2.3',
+                  querystring: '',
                   clientIp: '1.1.1.1',
-                  uri: '/1.2.1-beta.1',
+                  uri: '/1.2.3/someMethod',
                   origin: {
                     custom: {
                       customHeaders: {},
@@ -961,115 +377,87 @@ describe('edge-to-origin - routing - without prefix', () => {
 
       const requestResponse = response as lambda.CloudFrontRequest;
       expect(requestResponse).toBeDefined();
-      expect(requestResponse.uri).toBe('/1.2.1-beta.1');
-      expect(requestResponse.origin?.custom?.domainName).toBe('abc123.lambda-url.us-east-1.on.aws');
       expect(requestResponse).not.toHaveProperty('status');
       expect(requestResponse).not.toHaveProperty('body');
-    }
+      expect(requestResponse).toHaveProperty('headers');
+      expect(requestResponse.headers).toHaveProperty('host');
+      expect(requestResponse.headers.host).toHaveLength(1);
+      expect(requestResponse.headers.host[0].key).toBe('Host');
+      expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
+      expect(requestResponse).toHaveProperty('origin');
+      expect(requestResponse.origin).toHaveProperty('custom');
+      expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+      expect(requestResponse?.origin?.custom?.domainName).toBe(
+        'abc123456.lambda-url.us-east-1.on.aws',
+      );
+    });
   });
 
-  it('should route `direct` app request for [root] to origin for ?appver=[version]', async () => {
-    theConfig.replaceHostHeader = true;
+  it('should route `direct` app request with appName to origin', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
 
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Direct Bat App',
-    });
-    await app.Save(dbManager);
-
-    const versionDefault = new Version({
-      AppName: '[root]',
-      SemVer: '1.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'direct',
-      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
-    });
-    await versionDefault.Save(dbManager);
-    const versionNonDefault = new Version({
-      AppName: '[root]',
-      SemVer: '1.2.3',
-      Status: 'deployed',
-      Type: 'lambda-url',
-      StartupType: 'direct',
-      URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
-    });
-    await versionNonDefault.Save(dbManager);
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
 
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
+      theConfig.replaceHostHeader = true;
+
+      const app = new Application({
+        AppName: 'BatDirect',
+        DisplayName: 'Direct Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: 'BatDirect',
+        SemVer: '1.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'direct',
+        URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: 'BatDirect',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      const rootApp = new Application({
+        AppName: '[root]',
+        DisplayName: 'Root App',
+      });
+      await rootApp.Save(dbManager);
+
+      const versionRoot = new Version({
+        AppName: '[root]',
+        IntegrationID: 'abcd',
+        SemVer: '1.2.3',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'direct',
+        URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
+      });
+      await versionRoot.Save(dbManager);
+
+      const rulesRoot = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
+      });
+      await rulesRoot.Save(dbManager);
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {
-                  host: [
-                    {
-                      key: 'Host',
-                      value: 'zyz.cloudfront.net',
-                    },
-                  ],
-                },
-                method: 'GET',
-                querystring: 'appver=1.2.3',
-                clientIp: '1.1.1.1',
-                uri: '/',
-                origin: {
-                  custom: {
-                    customHeaders: {},
-                    domainName: 'zyz.cloudfront.net',
-                    keepaliveTimeout: 5,
-                    path: '',
-                    port: 443,
-                    protocol: 'https',
-                    readTimeout: 30,
-                    sslProtocols: ['TLSv1.2'],
-                  },
-                },
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const requestResponse = response as lambda.CloudFrontRequest;
-    expect(requestResponse).toBeDefined();
-    expect(requestResponse).not.toHaveProperty('status');
-    expect(requestResponse).not.toHaveProperty('body');
-    expect(requestResponse.querystring).toBe('');
-    expect(requestResponse).toHaveProperty('headers');
-    expect(requestResponse.headers).toHaveProperty('host');
-    expect(requestResponse.headers.host).toHaveLength(1);
-    expect(requestResponse.headers.host[0].key).toBe('Host');
-    expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
-    expect(requestResponse).toHaveProperty('origin');
-    expect(requestResponse.origin).toHaveProperty('custom');
-    expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
-    expect(requestResponse?.origin?.custom?.domainName).toBe(
-      'abc123456.lambda-url.us-east-1.on.aws',
-    );
-
-    //
-    // ?appver=[defaultversion] should select the default version correctly
-    //
-    {
       // Call the handler
       // @ts-expect-error no callback
       const response = await handler(
@@ -1093,9 +481,9 @@ describe('edge-to-origin - routing - without prefix', () => {
                     ],
                   },
                   method: 'GET',
-                  querystring: 'appver=1.2.1-beta.1',
+                  querystring: '',
                   clientIp: '1.1.1.1',
-                  uri: '/batdirectappver',
+                  uri: '/',
                   origin: {
                     custom: {
                       customHeaders: {},
@@ -1120,24 +508,75 @@ describe('edge-to-origin - routing - without prefix', () => {
       expect(requestResponse).toBeDefined();
       expect(requestResponse).not.toHaveProperty('status');
       expect(requestResponse).not.toHaveProperty('body');
-      expect(requestResponse.querystring).toBe('');
       expect(requestResponse).toHaveProperty('headers');
       expect(requestResponse.headers).toHaveProperty('host');
       expect(requestResponse.headers.host).toHaveLength(1);
       expect(requestResponse.headers.host[0].key).toBe('Host');
-      expect(requestResponse.headers.host[0].value).toBe('abc123.lambda-url.us-east-1.on.aws');
+      expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
       expect(requestResponse).toHaveProperty('origin');
       expect(requestResponse.origin).toHaveProperty('custom');
       expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
       expect(requestResponse?.origin?.custom?.domainName).toBe(
-        'abc123.lambda-url.us-east-1.on.aws',
+        'abc123456.lambda-url.us-east-1.on.aws',
       );
-    }
+    });
+  });
+
+  it('should route `direct` _next/data request for [root] and version to origin', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      theConfig.replaceHostHeader = true;
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Direct Root App',
+      });
+      await app.Save(dbManager);
+
+      {
+        const version = new Version({
+          AppName: '[root]]',
+          SemVer: '1.2.1-beta.1',
+          Status: 'deployed',
+          Type: 'lambda-url',
+          StartupType: 'direct',
+          URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+        });
+        await version.Save(dbManager);
+      }
+
+      {
+        const version = new Version({
+          AppName: '[root]',
+          SemVer: '1.2.2-beta.1',
+          Status: 'deployed',
+          Type: 'lambda-url',
+          StartupType: 'direct',
+          URL: 'https://abc1234567.lambda-url.us-east-1.on.aws/',
+        });
+        await version.Save(dbManager);
+      }
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
 
-    //
-    // ?appver=[defaultversion] should select the default version correctly
-    //
-    {
       // Call the handler
       // @ts-expect-error no callback
       const response = await handler(
@@ -1161,9 +600,9 @@ describe('edge-to-origin - routing - without prefix', () => {
                     ],
                   },
                   method: 'GET',
-                  querystring: 'appver=1.2.1-beta.1',
+                  querystring: '',
                   clientIp: '1.1.1.1',
-                  uri: '/batdirectappver',
+                  uri: '/_next/data/1.2.2-beta.1/abc123.json',
                   origin: {
                     custom: {
                       customHeaders: {},
@@ -1192,506 +631,1324 @@ describe('edge-to-origin - routing - without prefix', () => {
       expect(requestResponse.headers).toHaveProperty('host');
       expect(requestResponse.headers.host).toHaveLength(1);
       expect(requestResponse.headers.host[0].key).toBe('Host');
-      expect(requestResponse.headers.host[0].value).toBe('abc123.lambda-url.us-east-1.on.aws');
+      expect(requestResponse.headers.host[0].value).toBe('abc1234567.lambda-url.us-east-1.on.aws');
+      expect(requestResponse.uri).toBe('/_next/data/1.2.2-beta.1/abc123.json');
       expect(requestResponse).toHaveProperty('origin');
       expect(requestResponse.origin).toHaveProperty('custom');
       expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
       expect(requestResponse?.origin?.custom?.domainName).toBe(
-        'abc123.lambda-url.us-east-1.on.aws',
+        'abc1234567.lambda-url.us-east-1.on.aws',
       );
-    }
-  });
-
-  it('static app - request to [root]/x.y.z/ should not redirect if no defaultFile', async () => {
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Bat App',
     });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: '[root]',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'static',
-    });
-    await version.Save(dbManager);
-
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/3.2.1-beta.1',
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse.status).toBe('404');
-    expect(responseResponse).not.toHaveProperty('headers');
   });
 
-  it('static app - request to [root]/x.y.z should redirect to defaultFile', async () => {
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: '[root]',
-      DefaultFile: 'bat.html',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'static',
-    });
-    await version.Save(dbManager);
+  it('should route `iframe` _next/data request for [root] and version to origin', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
 
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/3.2.1-beta.1',
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse.status).toBe('302');
-    expect(responseResponse.headers).toHaveProperty('location');
-    expect(responseResponse.headers?.location[0].value).toContain('/3.2.1-beta.1/bat.html');
-  });
+      theConfig.replaceHostHeader = true;
 
-  it('static app - request to [root]/x.y.z/ should redirect to defaultFile', async () => {
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: '[root]',
-      DefaultFile: 'bat.html',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'static',
-    });
-    await version.Save(dbManager);
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Iframe Bat App',
+      });
+      await app.Save(dbManager);
 
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
+      const versionDefault = new Version({
+        AppName: '[root]',
+        SemVer: '1.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'iframe',
+        URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+      });
+      await versionDefault.Save(dbManager);
+      const versionNonDefault = new Version({
+        AppName: '[root]',
+        SemVer: '1.2.3',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'iframe',
+        URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
+      });
+      await versionNonDefault.Save(dbManager);
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/3.2.1-beta.1',
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {
+                    host: [
+                      {
+                        key: 'Host',
+                        value: 'zyz.cloudfront.net',
+                      },
+                    ],
+                  },
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/1.2.3/_next/data/3n-GzDjz4nifw5OnywVdV/posts/ssg-ssr.json',
+                  origin: {
+                    custom: {
+                      customHeaders: {},
+                      domainName: 'zyz.cloudfront.net',
+                      keepaliveTimeout: 5,
+                      path: '',
+                      port: 443,
+                      protocol: 'https',
+                      readTimeout: 30,
+                      sslProtocols: ['TLSv1.2'],
+                    },
+                  },
+                },
               },
             },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse.status).toBe('302');
-    expect(responseResponse.headers).toHaveProperty('location');
-    expect(responseResponse.headers?.location[0].value).toBe('/3.2.1-beta.1/bat.html');
-  });
-
-  it('static app - request to [root]/notVersion should load app frame with defaultFile', async () => {
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: '[root]',
-      DefaultFile: 'bat.html',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'static',
-    });
-    await version.Save(dbManager);
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
 
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      const requestResponse = response as lambda.CloudFrontRequest;
+      expect(requestResponse).toBeDefined();
+      expect(requestResponse).not.toHaveProperty('status');
+      expect(requestResponse).not.toHaveProperty('body');
+      expect(requestResponse).toHaveProperty('headers');
+      expect(requestResponse.headers).toHaveProperty('host');
+      expect(requestResponse.headers.host).toHaveLength(1);
+      expect(requestResponse.headers.host[0].key).toBe('Host');
+      expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
+      expect(requestResponse.uri).toBe(
+        '/1.2.3/_next/data/3n-GzDjz4nifw5OnywVdV/posts/ssg-ssr.json',
+      );
+      expect(requestResponse).toHaveProperty('origin');
+      expect(requestResponse.origin).toHaveProperty('custom');
+      expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+      expect(requestResponse?.origin?.custom?.domainName).toBe(
+        'abc123456.lambda-url.us-east-1.on.aws',
+      );
     });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/notVersion',
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse.status).toBe('200');
-    expect(responseResponse).toHaveProperty('body');
-    expect(responseResponse.headers).not.toHaveProperty('location');
-    expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/3.2.1-beta.1/bat.html" seamless');
   });
 
-  it('should serve appframe with no default file', async () => {
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: '[root]',
-      DefaultFile: '',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta1',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await version.Save(dbManager);
+  it('should 404 `direct` request for [root] and ?appver=', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
 
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta1', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/',
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse.status).toBe('200');
-    expect(responseResponse).toHaveProperty('body');
-    expect(responseResponse.headers).not.toHaveProperty('location');
-    expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/3.2.1-beta1" seamless');
-  });
+      theConfig.replaceHostHeader = true;
 
-  it('should serve appframe with sub-sub-route - beta2', async () => {
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: '[root]',
-      DefaultFile: '',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta2',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await version.Save(dbManager);
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Direct Bat App',
+      });
+      await app.Save(dbManager);
 
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta2', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
+      const version = new Version({
+        AppName: '[root]',
+        SemVer: '1.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'direct',
+        URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+      });
+      await version.Save(dbManager);
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/demo/grid',
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {
+                    host: [
+                      {
+                        key: 'Host',
+                        value: 'zyz.cloudfront.net',
+                      },
+                    ],
+                  },
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/?appver=1.2.2-beta.1',
+                  origin: {
+                    custom: {
+                      customHeaders: {},
+                      domainName: 'zyz.cloudfront.net',
+                      keepaliveTimeout: 5,
+                      path: '',
+                      port: 443,
+                      protocol: 'https',
+                      readTimeout: 30,
+                      sslProtocols: ['TLSv1.2'],
+                    },
+                  },
+                },
               },
             },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse.status).toBe('200');
-    expect(responseResponse).toHaveProperty('body');
-    expect(responseResponse.headers).not.toHaveProperty('location');
-    expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/3.2.1-beta2/demo/grid" seamless');
-  });
-
-  it('should serve appframe with sub-route - beta3', async () => {
-    const app = new Application({
-      AppName: '[root]',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: '[root]',
-      DefaultFile: 'someFile.html',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta3',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await version.Save(dbManager);
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
 
-    const rules = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toBeDefined();
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse?.status).toBe('404');
+      expect(responseResponse).not.toHaveProperty('body');
     });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
-              },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/demo',
-              },
-            },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse.status).toBe('200');
-    expect(responseResponse).toHaveProperty('body');
-    expect(responseResponse.headers).not.toHaveProperty('location');
-    expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/3.2.1-beta3/demo" seamless');
   });
 
-  // TODO: Unskip when we have S3 routing from the Origin Request handler
-  it.skip('should route through to S3 for /favicon.ico', async () => {
-    const app = new Application({
-      AppName: 'Bat',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: 'Bat',
-      DefaultFile: 'someFile.html',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta3',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await version.Save(dbManager);
+  it('should route `iframe` app request for [root] by creating iframe response for ?appver=[version]', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
 
-    const rules = new Rules({
-      AppName: 'Bat',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
 
-    const rootApp = new Application({
-      AppName: '[root]',
-      DisplayName: 'Root App',
-    });
-    await rootApp.Save(dbManager);
-
-    const versionRoot = new Version({
-      AppName: '[root]',
-      DefaultFile: 'someOtherFile.html',
-      IntegrationID: 'abcd',
-      SemVer: '1.2.3',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await versionRoot.Save(dbManager);
+      theConfig.replaceHostHeader = true;
 
-    const rulesRoot = new Rules({
-      AppName: '[root]',
-      Version: 0,
-      RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
-    });
-    await rulesRoot.Save(dbManager);
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Direct Bat App',
+      });
+      await app.Save(dbManager);
 
-    // Call the handler
-    // @ts-expect-error no callback
-    const response = await handler(
-      {
-        Records: [
-          {
-            cf: {
-              config: {
-                distributionDomainName: 'zyz.cloudfront.net',
-                distributionId: '123',
-                eventType: 'origin-request',
-                requestId: '123',
+      const versionDefault = new Version({
+        AppName: '[root]',
+        SemVer: '1.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'iframe',
+        URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+      });
+      await versionDefault.Save(dbManager);
+      const versionNonDefault = new Version({
+        AppName: '[root]',
+        SemVer: '1.2.3',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'iframe',
+        URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
+      });
+      await versionNonDefault.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {
+                    host: [
+                      {
+                        key: 'Host',
+                        value: 'zyz.cloudfront.net',
+                      },
+                    ],
+                  },
+                  method: 'GET',
+                  querystring: 'appver=1.2.3',
+                  clientIp: '1.1.1.1',
+                  uri: '/',
+                  origin: {
+                    custom: {
+                      customHeaders: {},
+                      domainName: 'zyz.cloudfront.net',
+                      keepaliveTimeout: 5,
+                      path: '',
+                      port: 443,
+                      protocol: 'https',
+                      readTimeout: 30,
+                      sslProtocols: ['TLSv1.2'],
+                    },
+                  },
+                },
+              },
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toBeDefined();
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse?.status).toBe('200');
+      expect(responseResponse).toHaveProperty('body');
+      expect(responseResponse.body?.length).toBeGreaterThan(80);
+      expect(responseResponse.body).toContain('<iframe src="/1.2.3" seamless');
+
+      //
+      // Make sure request with version in path AND ?appver does not add to the path
+      //
+      {
+        // Call the handler
+        // @ts-expect-error no callback
+        const response = await handler(
+          {
+            Records: [
+              {
+                cf: {
+                  config: {
+                    distributionDomainName: 'zyz.cloudfront.net',
+                    distributionId: '123',
+                    eventType: 'origin-request',
+                    requestId: '123',
+                  },
+                  request: {
+                    headers: {
+                      host: [
+                        {
+                          key: 'Host',
+                          value: 'zyz.cloudfront.net',
+                        },
+                      ],
+                    },
+                    method: 'GET',
+                    querystring: 'appver=1.2.3',
+                    clientIp: '1.1.1.1',
+                    uri: '/1.2.3',
+                    origin: {
+                      custom: {
+                        customHeaders: {},
+                        domainName: 'zyz.cloudfront.net',
+                        keepaliveTimeout: 5,
+                        path: '',
+                        port: 443,
+                        protocol: 'https',
+                        readTimeout: 30,
+                        sslProtocols: ['TLSv1.2'],
+                      },
+                    },
+                  },
+                },
+              },
+            ],
+          } as lambda.CloudFrontRequestEvent,
+          {} as lambda.Context,
+        );
+
+        const requestResponse = response as lambda.CloudFrontRequest;
+        expect(requestResponse).toBeDefined();
+        expect(requestResponse.uri).toBe('/1.2.3');
+        expect(requestResponse.origin?.custom?.domainName).toBe(
+          'abc123456.lambda-url.us-east-1.on.aws',
+        );
+        expect(requestResponse).not.toHaveProperty('status');
+        expect(requestResponse).not.toHaveProperty('body');
+      }
+
+      //
+      // Request with non-matching version in path and conflicting
+      // ?appver should use version in path already
+      //
+      {
+        // Call the handler
+        // @ts-expect-error no callback
+        const response = await handler(
+          {
+            Records: [
+              {
+                cf: {
+                  config: {
+                    distributionDomainName: 'zyz.cloudfront.net',
+                    distributionId: '123',
+                    eventType: 'origin-request',
+                    requestId: '123',
+                  },
+                  request: {
+                    headers: {
+                      host: [
+                        {
+                          key: 'Host',
+                          value: 'zyz.cloudfront.net',
+                        },
+                      ],
+                    },
+                    method: 'GET',
+                    querystring: 'appver=1.2.3',
+                    clientIp: '1.1.1.1',
+                    uri: '/1.2.1-beta.1',
+                    origin: {
+                      custom: {
+                        customHeaders: {},
+                        domainName: 'zyz.cloudfront.net',
+                        keepaliveTimeout: 5,
+                        path: '',
+                        port: 443,
+                        protocol: 'https',
+                        readTimeout: 30,
+                        sslProtocols: ['TLSv1.2'],
+                      },
+                    },
+                  },
+                },
+              },
+            ],
+          } as lambda.CloudFrontRequestEvent,
+          {} as lambda.Context,
+        );
+
+        const requestResponse = response as lambda.CloudFrontRequest;
+        expect(requestResponse).toBeDefined();
+        expect(requestResponse.uri).toBe('/1.2.1-beta.1');
+        expect(requestResponse.origin?.custom?.domainName).toBe(
+          'abc123.lambda-url.us-east-1.on.aws',
+        );
+        expect(requestResponse).not.toHaveProperty('status');
+        expect(requestResponse).not.toHaveProperty('body');
+      }
+    });
+  });
+
+  it('should route `direct` app request for [root] to origin for ?appver=[version]', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      theConfig.replaceHostHeader = true;
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Direct Bat App',
+      });
+      await app.Save(dbManager);
+
+      const versionDefault = new Version({
+        AppName: '[root]',
+        SemVer: '1.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'direct',
+        URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+      });
+      await versionDefault.Save(dbManager);
+      const versionNonDefault = new Version({
+        AppName: '[root]',
+        SemVer: '1.2.3',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'direct',
+        URL: 'https://abc123456.lambda-url.us-east-1.on.aws/',
+      });
+      await versionNonDefault.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {
+                    host: [
+                      {
+                        key: 'Host',
+                        value: 'zyz.cloudfront.net',
+                      },
+                    ],
+                  },
+                  method: 'GET',
+                  querystring: 'appver=1.2.3',
+                  clientIp: '1.1.1.1',
+                  uri: '/',
+                  origin: {
+                    custom: {
+                      customHeaders: {},
+                      domainName: 'zyz.cloudfront.net',
+                      keepaliveTimeout: 5,
+                      path: '',
+                      port: 443,
+                      protocol: 'https',
+                      readTimeout: 30,
+                      sslProtocols: ['TLSv1.2'],
+                    },
+                  },
+                },
+              },
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const requestResponse = response as lambda.CloudFrontRequest;
+      expect(requestResponse).toBeDefined();
+      expect(requestResponse).not.toHaveProperty('status');
+      expect(requestResponse).not.toHaveProperty('body');
+      expect(requestResponse.querystring).toBe('');
+      expect(requestResponse).toHaveProperty('headers');
+      expect(requestResponse.headers).toHaveProperty('host');
+      expect(requestResponse.headers.host).toHaveLength(1);
+      expect(requestResponse.headers.host[0].key).toBe('Host');
+      expect(requestResponse.headers.host[0].value).toBe('abc123456.lambda-url.us-east-1.on.aws');
+      expect(requestResponse).toHaveProperty('origin');
+      expect(requestResponse.origin).toHaveProperty('custom');
+      expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+      expect(requestResponse?.origin?.custom?.domainName).toBe(
+        'abc123456.lambda-url.us-east-1.on.aws',
+      );
+
+      //
+      // ?appver=[defaultversion] should select the default version correctly
+      //
+      {
+        // Call the handler
+        // @ts-expect-error no callback
+        const response = await handler(
+          {
+            Records: [
+              {
+                cf: {
+                  config: {
+                    distributionDomainName: 'zyz.cloudfront.net',
+                    distributionId: '123',
+                    eventType: 'origin-request',
+                    requestId: '123',
+                  },
+                  request: {
+                    headers: {
+                      host: [
+                        {
+                          key: 'Host',
+                          value: 'zyz.cloudfront.net',
+                        },
+                      ],
+                    },
+                    method: 'GET',
+                    querystring: 'appver=1.2.1-beta.1',
+                    clientIp: '1.1.1.1',
+                    uri: '/batdirectappver',
+                    origin: {
+                      custom: {
+                        customHeaders: {},
+                        domainName: 'zyz.cloudfront.net',
+                        keepaliveTimeout: 5,
+                        path: '',
+                        port: 443,
+                        protocol: 'https',
+                        readTimeout: 30,
+                        sslProtocols: ['TLSv1.2'],
+                      },
+                    },
+                  },
+                },
+              },
+            ],
+          } as lambda.CloudFrontRequestEvent,
+          {} as lambda.Context,
+        );
+
+        const requestResponse = response as lambda.CloudFrontRequest;
+        expect(requestResponse).toBeDefined();
+        expect(requestResponse).not.toHaveProperty('status');
+        expect(requestResponse).not.toHaveProperty('body');
+        expect(requestResponse.querystring).toBe('');
+        expect(requestResponse).toHaveProperty('headers');
+        expect(requestResponse.headers).toHaveProperty('host');
+        expect(requestResponse.headers.host).toHaveLength(1);
+        expect(requestResponse.headers.host[0].key).toBe('Host');
+        expect(requestResponse.headers.host[0].value).toBe('abc123.lambda-url.us-east-1.on.aws');
+        expect(requestResponse).toHaveProperty('origin');
+        expect(requestResponse.origin).toHaveProperty('custom');
+        expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+        expect(requestResponse?.origin?.custom?.domainName).toBe(
+          'abc123.lambda-url.us-east-1.on.aws',
+        );
+      }
+
+      //
+      // ?appver=[defaultversion] should select the default version correctly
+      //
+      {
+        // Call the handler
+        // @ts-expect-error no callback
+        const response = await handler(
+          {
+            Records: [
+              {
+                cf: {
+                  config: {
+                    distributionDomainName: 'zyz.cloudfront.net',
+                    distributionId: '123',
+                    eventType: 'origin-request',
+                    requestId: '123',
+                  },
+                  request: {
+                    headers: {
+                      host: [
+                        {
+                          key: 'Host',
+                          value: 'zyz.cloudfront.net',
+                        },
+                      ],
+                    },
+                    method: 'GET',
+                    querystring: 'appver=1.2.1-beta.1',
+                    clientIp: '1.1.1.1',
+                    uri: '/batdirectappver',
+                    origin: {
+                      custom: {
+                        customHeaders: {},
+                        domainName: 'zyz.cloudfront.net',
+                        keepaliveTimeout: 5,
+                        path: '',
+                        port: 443,
+                        protocol: 'https',
+                        readTimeout: 30,
+                        sslProtocols: ['TLSv1.2'],
+                      },
+                    },
+                  },
+                },
+              },
+            ],
+          } as lambda.CloudFrontRequestEvent,
+          {} as lambda.Context,
+        );
+
+        const requestResponse = response as lambda.CloudFrontRequest;
+        expect(requestResponse).toBeDefined();
+        expect(requestResponse).not.toHaveProperty('status');
+        expect(requestResponse).not.toHaveProperty('body');
+        expect(requestResponse).toHaveProperty('headers');
+        expect(requestResponse.headers).toHaveProperty('host');
+        expect(requestResponse.headers.host).toHaveLength(1);
+        expect(requestResponse.headers.host[0].key).toBe('Host');
+        expect(requestResponse.headers.host[0].value).toBe('abc123.lambda-url.us-east-1.on.aws');
+        expect(requestResponse).toHaveProperty('origin');
+        expect(requestResponse.origin).toHaveProperty('custom');
+        expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+        expect(requestResponse?.origin?.custom?.domainName).toBe(
+          'abc123.lambda-url.us-east-1.on.aws',
+        );
+      }
+    });
+  });
+
+  it('static app - request to [root]/x.y.z/ should not redirect if no defaultFile', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: '[root]',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'static',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/3.2.1-beta.1',
+                },
+              },
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse.status).toBe('404');
+      expect(responseResponse).not.toHaveProperty('headers');
+    });
+  });
+
+  it('static app - request to [root]/x.y.z should redirect to defaultFile', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: '[root]',
+        DefaultFile: 'bat.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta.3',
+        Status: 'deployed',
+        Type: 'static',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta.3', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/3.2.1-beta.3',
+                },
+              },
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse.status).toBe('302');
+      expect(responseResponse.headers).toHaveProperty('location');
+      expect(responseResponse.headers?.location[0].value).toContain('/3.2.1-beta.3/bat.html');
+    });
+  });
+
+  it('static app - request to [root]/x.y.z/ should redirect to defaultFile', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: '[root]',
+        DefaultFile: 'bat.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta.2',
+        Status: 'deployed',
+        Type: 'static',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta.2', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/3.2.1-beta.2',
+                },
+              },
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse.status).toBe('302');
+      expect(responseResponse.headers).toHaveProperty('location');
+      expect(responseResponse.headers?.location[0].value).toBe('/3.2.1-beta.2/bat.html');
+    });
+  });
+
+  it('static app - request to [root]/notVersion should load app frame with defaultFile', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: '[root]',
+        DefaultFile: 'bat.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'static',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/notVersion',
+                },
+              },
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse.status).toBe('200');
+      expect(responseResponse).toHaveProperty('body');
+      expect(responseResponse.headers).not.toHaveProperty('location');
+      expect(responseResponse.body?.length).toBeGreaterThan(80);
+      expect(responseResponse.body).toContain('<iframe src="/3.2.1-beta.1/bat.html" seamless');
+    });
+  });
+
+  it('should serve appframe with no default file', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: '[root]',
+        DefaultFile: '',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta1',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/',
+                },
+              },
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse.status).toBe('200');
+      expect(responseResponse).toHaveProperty('body');
+      expect(responseResponse.headers).not.toHaveProperty('location');
+      expect(responseResponse.body?.length).toBeGreaterThan(80);
+      expect(responseResponse.body).toContain('<iframe src="/3.2.1-beta1" seamless');
+    });
+  });
+
+  it('should serve appframe with sub-sub-route - beta2', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: '[root]',
+        DefaultFile: '',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta2',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta2', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/demo/grid',
+                },
+              },
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse.status).toBe('200');
+      expect(responseResponse).toHaveProperty('body');
+      expect(responseResponse.headers).not.toHaveProperty('location');
+      expect(responseResponse.body?.length).toBeGreaterThan(80);
+      expect(responseResponse.body).toContain('<iframe src="/3.2.1-beta2/demo/grid" seamless');
+    });
+  });
+
+  it('should serve appframe with sub-route - beta3', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      const app = new Application({
+        AppName: '[root]',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: '[root]',
+        DefaultFile: 'someFile.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta3',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/demo',
+                },
               },
-              request: {
-                headers: {},
-                method: 'GET',
-                querystring: '',
-                clientIp: '1.1.1.1',
-                uri: '/favicon.ico',
+            },
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse.status).toBe('200');
+      expect(responseResponse).toHaveProperty('body');
+      expect(responseResponse.headers).not.toHaveProperty('location');
+      expect(responseResponse.body?.length).toBeGreaterThan(80);
+      expect(responseResponse.body).toContain('<iframe src="/3.2.1-beta3/demo" seamless');
+    });
+  });
+
+  // TODO: Unskip when we have S3 routing from the Origin Request handler
+  it('should route through to S3 for /favicon.ico', async () => {
+    return jest.isolateModulesAsync(async () => {
+      const { Config } = await import('./config/config');
+
+      jest.mock('./config/config');
+      Object.defineProperty(Config, 'instance', {
+        configurable: false,
+        enumerable: false,
+        get: jest.fn((): IConfig => {
+          return theConfig;
+        }),
+      });
+
+      const { handler, overrideDBManager } = await import('./index');
+      overrideDBManager({ dbManager, dynamoClient });
+
+      const app = new Application({
+        AppName: 'Bat',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: 'Bat',
+        DefaultFile: 'someFile.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta3',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: 'Bat',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      const rootApp = new Application({
+        AppName: '[root]',
+        DisplayName: 'Root App',
+      });
+      await rootApp.Save(dbManager);
+
+      const versionRoot = new Version({
+        AppName: '[root]',
+        DefaultFile: 'someOtherFile.html',
+        IntegrationID: 'abcd',
+        SemVer: '1.2.3',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await versionRoot.Save(dbManager);
+
+      const rulesRoot = new Rules({
+        AppName: '[root]',
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.3', AttributeName: '', AttributeValue: '' } },
+      });
+      await rulesRoot.Save(dbManager);
+
+      // Call the handler
+      // @ts-expect-error no callback
+      const response = await handler(
+        {
+          Records: [
+            {
+              cf: {
+                config: {
+                  distributionDomainName: 'zyz.cloudfront.net',
+                  distributionId: '123',
+                  eventType: 'origin-request',
+                  requestId: '123',
+                },
+                request: {
+                  headers: {},
+                  method: 'GET',
+                  querystring: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/favicon.ico',
+                },
               },
             },
-          },
-        ],
-      } as lambda.CloudFrontRequestEvent,
-      {} as lambda.Context,
-    );
-
-    const responseResponse = response as lambda.CloudFrontResultResponse;
-    expect(responseResponse).toHaveProperty('status');
-    expect(responseResponse.status).toBe('404');
-    expect(responseResponse).not.toHaveProperty('body');
+          ],
+        } as lambda.CloudFrontRequestEvent,
+        {} as lambda.Context,
+      );
+
+      const responseResponse = response as lambda.CloudFrontResultResponse;
+      expect(responseResponse).toHaveProperty('status');
+      expect(responseResponse.status).toBe('200');
+      expect(responseResponse).toHaveProperty('body');
+    });
   });
 });
diff --git a/packages/microapps-edge-to-origin/src/index.route.spec.ts b/packages/microapps-edge-to-origin/src/index.route.spec.ts
index 8fa319a8..00a92c66 100644
--- a/packages/microapps-edge-to-origin/src/index.route.spec.ts
+++ b/packages/microapps-edge-to-origin/src/index.route.spec.ts
@@ -1100,6 +1100,7 @@ describe('edge-to-origin - routing - without prefix', () => {
   });
 
   it('static app - request to app/x.y.z/ should not redirect if no defaultFile', async () => {
+    const AppName = 'BatRedirectNoDefaultFile';
     const app = new Application({
       AppName: 'Bat',
       DisplayName: 'Bat App',
@@ -1107,7 +1108,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta.1',
       Status: 'deployed',
@@ -1116,7 +1117,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
     });
@@ -1140,7 +1141,7 @@ describe('edge-to-origin - routing - without prefix', () => {
                 method: 'GET',
                 querystring: '',
                 clientIp: '1.1.1.1',
-                uri: '/bat/3.2.1-beta.1',
+                uri: `/${AppName}/3.2.1-beta.1`,
               },
             },
           },
@@ -1166,7 +1167,7 @@ describe('edge-to-origin - routing - without prefix', () => {
       AppName: 'Bat',
       DefaultFile: 'bat.html',
       IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
+      SemVer: '3.2.1-beta.2',
       Status: 'deployed',
       Type: 'static',
     });
@@ -1175,7 +1176,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     const rules = new Rules({
       AppName: 'Bat',
       Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      RuleSet: { default: { SemVer: '3.2.1-beta.2', AttributeName: '', AttributeValue: '' } },
     });
     await rules.Save(dbManager);
 
@@ -1197,7 +1198,7 @@ describe('edge-to-origin - routing - without prefix', () => {
                 method: 'GET',
                 querystring: '',
                 clientIp: '1.1.1.1',
-                uri: '/bat/3.2.1-beta.1',
+                uri: '/bat/3.2.1-beta.2',
               },
             },
           },
@@ -1210,7 +1211,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     expect(responseResponse).toHaveProperty('status');
     expect(responseResponse.status).toBe('302');
     expect(responseResponse.headers).toHaveProperty('location');
-    expect(responseResponse.headers?.location[0].value).toContain('/bat/3.2.1-beta.1/bat.html');
+    expect(responseResponse.headers?.location[0].value).toContain('/bat/3.2.1-beta.2/bat.html');
   });
 
   it('static app - request to app/x.y.z/ should redirect to defaultFile', async () => {
@@ -1224,7 +1225,7 @@ describe('edge-to-origin - routing - without prefix', () => {
       AppName: 'Bat',
       DefaultFile: 'bat.html',
       IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
+      SemVer: '3.2.1-beta.3',
       Status: 'deployed',
       Type: 'static',
     });
@@ -1233,7 +1234,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     const rules = new Rules({
       AppName: 'Bat',
       Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      RuleSet: { default: { SemVer: '3.2.1-beta.3', AttributeName: '', AttributeValue: '' } },
     });
     await rules.Save(dbManager);
 
@@ -1255,7 +1256,7 @@ describe('edge-to-origin - routing - without prefix', () => {
                 method: 'GET',
                 querystring: '',
                 clientIp: '1.1.1.1',
-                uri: '/bat/3.2.1-beta.1',
+                uri: '/bat/3.2.1-beta.3',
               },
             },
           },
@@ -1268,18 +1269,19 @@ describe('edge-to-origin - routing - without prefix', () => {
     expect(responseResponse).toHaveProperty('status');
     expect(responseResponse.status).toBe('302');
     expect(responseResponse.headers).toHaveProperty('location');
-    expect(responseResponse.headers?.location[0].value).toBe('/bat/3.2.1-beta.1/bat.html');
+    expect(responseResponse.headers?.location[0].value).toBe('/bat/3.2.1-beta.3/bat.html');
   });
 
   it('static app - request to app/notVersion should load app frame with defaultFile', async () => {
+    const AppName = 'BatAppNotVersion';
     const app = new Application({
-      AppName: 'Bat',
+      AppName,
       DisplayName: 'Bat App',
     });
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       DefaultFile: 'bat.html',
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta.1',
@@ -1289,7 +1291,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
     });
@@ -1313,7 +1315,7 @@ describe('edge-to-origin - routing - without prefix', () => {
                 method: 'GET',
                 querystring: '',
                 clientIp: '1.1.1.1',
-                uri: '/bat/notVersion',
+                uri: `/${AppName}/notVersion`,
               },
             },
           },
@@ -1328,18 +1330,21 @@ describe('edge-to-origin - routing - without prefix', () => {
     expect(responseResponse).toHaveProperty('body');
     expect(responseResponse.headers).not.toHaveProperty('location');
     expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta.1/bat.html" seamless');
+    expect(responseResponse.body).toContain(
+      `<iframe src="/${AppName}/3.2.1-beta.1/bat.html" seamless`,
+    );
   });
 
   it('should serve appframe with no default file', async () => {
+    const AppName = 'BatNoDefaultFile';
     const app = new Application({
-      AppName: 'Bat',
+      AppName,
       DisplayName: 'Bat App',
     });
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       DefaultFile: '',
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta1',
@@ -1349,7 +1354,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta1', AttributeName: '', AttributeValue: '' } },
     });
@@ -1373,7 +1378,7 @@ describe('edge-to-origin - routing - without prefix', () => {
                 method: 'GET',
                 querystring: '',
                 clientIp: '1.1.1.1',
-                uri: '/bat/',
+                uri: `/${AppName}/`,
               },
             },
           },
@@ -1388,18 +1393,19 @@ describe('edge-to-origin - routing - without prefix', () => {
     expect(responseResponse).toHaveProperty('body');
     expect(responseResponse.headers).not.toHaveProperty('location');
     expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta1" seamless');
+    expect(responseResponse.body).toContain(`<iframe src="/${AppName}/3.2.1-beta1" seamless`);
   });
 
   it('should serve appframe with sub-sub-route - beta2', async () => {
+    const AppName = 'BatBeta2';
     const app = new Application({
-      AppName: 'Bat',
+      AppName,
       DisplayName: 'Bat App',
     });
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       DefaultFile: '',
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta2',
@@ -1409,7 +1415,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta2', AttributeName: '', AttributeValue: '' } },
     });
@@ -1433,7 +1439,7 @@ describe('edge-to-origin - routing - without prefix', () => {
                 method: 'GET',
                 querystring: '',
                 clientIp: '1.1.1.1',
-                uri: '/bat/demo/grid',
+                uri: `/${AppName}/demo/grid`,
               },
             },
           },
@@ -1448,18 +1454,21 @@ describe('edge-to-origin - routing - without prefix', () => {
     expect(responseResponse).toHaveProperty('body');
     expect(responseResponse.headers).not.toHaveProperty('location');
     expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta2/demo/grid" seamless');
+    expect(responseResponse.body).toContain(
+      `<iframe src="/${AppName}/3.2.1-beta2/demo/grid" seamless`,
+    );
   });
 
   it('should serve appframe with sub-route - beta3', async () => {
+    const AppName = 'BatSubRouteBeta3';
     const app = new Application({
-      AppName: 'Bat',
+      AppName,
       DisplayName: 'Bat App',
     });
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       DefaultFile: 'someFile.html',
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta3',
@@ -1469,7 +1478,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
     });
@@ -1493,7 +1502,7 @@ describe('edge-to-origin - routing - without prefix', () => {
                 method: 'GET',
                 querystring: '',
                 clientIp: '1.1.1.1',
-                uri: '/bat/demo',
+                uri: `/${AppName}/demo`,
               },
             },
           },
@@ -1508,7 +1517,7 @@ describe('edge-to-origin - routing - without prefix', () => {
     expect(responseResponse).toHaveProperty('body');
     expect(responseResponse.headers).not.toHaveProperty('location');
     expect(responseResponse.body?.length).toBeGreaterThan(80);
-    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta3/demo" seamless');
+    expect(responseResponse.body).toContain(`<iframe src="/${AppName}/3.2.1-beta3/demo" seamless`);
   });
 
   it('should return 404 for /favicon.ico', async () => {
diff --git a/packages/microapps-publish/README.md b/packages/microapps-publish/README.md
index 9447a8e6..717e3f76 100644
--- a/packages/microapps-publish/README.md
+++ b/packages/microapps-publish/README.md
@@ -229,17 +229,11 @@ USAGE
   $ microapps-publish delete
 
 OPTIONS
-  -a, --appName=appName                        Name of the MicroApp
-
-  -d, --deployerLambdaName=deployerLambdaName  (required) Name of the deployer
-                                               lambda function
-
-  -n, --newVersion=newVersion                  (required) New semantic version
-                                               to apply
-
-  -v, --version                                show CLI version
-
-  --help                                       show CLI help
+  -a, --app-name=app-name                          MicroApps app name (this becomes the path the app is rooted at)
+  -d, --deployer-lambda-name=deployer-lambda-name  Name of the deployer lambda function
+  -n, --new-version=new-version                    New semantic version to apply
+  -v, --version                                    show CLI version
+  --help                                           show CLI help
 
 EXAMPLE
   $ microapps-publish delete -d microapps-deployer-dev -a release -n 0.0.13
diff --git a/packages/microapps-router-lib/src/app-cache.spec.ts b/packages/microapps-router-lib/src/app-cache.spec.ts
new file mode 100644
index 00000000..9ced2aae
--- /dev/null
+++ b/packages/microapps-router-lib/src/app-cache.spec.ts
@@ -0,0 +1,175 @@
+/// <reference types="jest" />
+import 'jest-dynalite/withDb';
+import * as dynamodb from '@aws-sdk/client-dynamodb';
+import { Application, DBManager, Version, Rules } from '@pwrdrvr/microapps-datalib';
+import { AppVersionCache } from './app-cache';
+
+let dynamoClient: dynamodb.DynamoDBClient;
+let dbManager: DBManager;
+
+const TEST_TABLE_NAME = 'microapps';
+
+describe('app-cache', () => {
+  beforeAll(() => {
+    jest.setTimeout(30000);
+
+    dynamoClient = new dynamodb.DynamoDBClient({
+      endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
+      tls: false,
+      region: 'local',
+    });
+
+    // Init the DB manager to point it at the right table
+    dbManager = new DBManager({ dynamoClient, tableName: TEST_TABLE_NAME });
+  });
+
+  describe('GetRules', () => {
+    it('Returns cached record after deleted from DB', async () => {
+      const app = new Application({
+        AppName: 'Cached',
+        DisplayName: 'Cached App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: 'Cached',
+        LambdaARN: 'arn:aws:lambda:us-east-1:123456789012:function:cached',
+        URL: 'https://some-lambda-function-url-id.lambda-url.us-east-2.on.aws/',
+        SemVer: '3.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: 'Cached',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      const appVersionCache = new AppVersionCache({ dbManager });
+
+      // Seed the cache while record exists
+      const versionWhenDBExists = await appVersionCache.GetRules({
+        key: { AppName: 'Cached' },
+      });
+
+      expect(versionWhenDBExists).toHaveProperty('AppName', 'cached');
+      expect(versionWhenDBExists).toHaveProperty('RuleSet', {
+        default: { AttributeName: '', AttributeValue: '', SemVer: '3.2.1-beta.1' },
+      });
+
+      // Clobber the Rules record
+      await Application.UpdateDefaultRule({
+        dbManager,
+        key: { AppName: 'Cached', SemVer: '9.9.9' },
+      });
+
+      // Get record from cache after DB record is deleted
+      const rulesWhenCleared = await appVersionCache.GetRules({
+        key: { AppName: 'Cached' },
+      });
+
+      expect(rulesWhenCleared).toEqual(versionWhenDBExists);
+    });
+  });
+
+  describe('GetVersionInfo', () => {
+    it('Returns cached record after deleted from DB', async () => {
+      const app = new Application({
+        AppName: 'Cached',
+        DisplayName: 'Cached App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: 'Cached',
+        LambdaARN: 'arn:aws:lambda:us-east-1:123456789012:function:cached',
+        URL: 'https://some-lambda-function-url-id.lambda-url.us-east-2.on.aws/',
+        SemVer: '3.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: 'Cached',
+        Version: 0,
+        RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      const appVersionCache = new AppVersionCache({ dbManager });
+
+      // Seed the cache while record exists
+      const versionWhenDBExists = await appVersionCache.GetVersionInfo({
+        key: { AppName: 'Cached', SemVer: '3.2.1-beta.1' },
+      });
+
+      expect(versionWhenDBExists).toHaveProperty('AppName', 'cached');
+      expect(versionWhenDBExists).toHaveProperty(
+        'URL',
+        'https://some-lambda-function-url-id.lambda-url.us-east-2.on.aws/',
+      );
+      expect(versionWhenDBExists).toHaveProperty('SemVer', '3.2.1-beta.1');
+      expect(versionWhenDBExists).toHaveProperty('Status', 'deployed');
+      expect(versionWhenDBExists).toHaveProperty('Type', 'lambda');
+
+      // Delete the record
+      await Version.DeleteVersion({
+        dbManager,
+        key: { AppName: 'Cached', SemVer: '3.2.1-beta.1' },
+      });
+
+      // Get record from cache after DB record is deleted
+      const versionWhenDBCleared = await appVersionCache.GetVersionInfo({
+        key: { AppName: 'Cached', SemVer: '3.2.1-beta.1' },
+      });
+
+      expect(versionWhenDBCleared).toEqual(versionWhenDBExists);
+    });
+
+    it('Negative Cache', async () => {
+      const appVersionCache = new AppVersionCache({ dbManager });
+
+      // Seed the negative cache
+      const versionWhenDBExists = await appVersionCache.GetVersionInfo({
+        key: { AppName: 'Missing', SemVer: '7.6.8' },
+      });
+
+      // Confirm record is not in the DB
+      expect(versionWhenDBExists).toBeUndefined();
+
+      const app = new Application({
+        AppName: 'Missing',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: 'Missing',
+        SemVer: '7.6.8',
+        LambdaARN: 'arn:aws:lambda:us-east-1:123456789012:function:bat',
+        URL: 'https://some-lambda-function-url-id.lambda-url.us-east-2.on.aws/',
+        Status: 'deployed',
+        Type: 'lambda',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: 'Missing',
+        Version: 0,
+        RuleSet: { default: { SemVer: '7.6.8', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      // Get record from cache after DB record is created - should be hidden by negative cache
+      const versionWhenDBCreated = await appVersionCache.GetVersionInfo({
+        key: { AppName: 'Missing', SemVer: '7.6.8' },
+      });
+
+      expect(versionWhenDBCreated).toBeUndefined();
+    });
+  });
+});
diff --git a/packages/microapps-router-lib/src/app-cache.ts b/packages/microapps-router-lib/src/app-cache.ts
new file mode 100644
index 00000000..29023ba1
--- /dev/null
+++ b/packages/microapps-router-lib/src/app-cache.ts
@@ -0,0 +1,193 @@
+import { Application, DBManager, Rules, Version } from '@pwrdrvr/microapps-datalib';
+
+interface ICacheEntry<T> {
+  timestamp: number;
+  data: T;
+}
+
+export class AppVersionCache {
+  private negativeAppNameCache: Map<string, ICacheEntry<undefined>>;
+  private appRulesCache: Map<string, ICacheEntry<Rules>>;
+  private appVersionsCache: Map<string, Map<string, ICacheEntry<Version>>>;
+  private dbManager: DBManager;
+
+  private static instance: AppVersionCache;
+
+  /**
+   * Get the AppVersionCache instance or create it
+   */
+  public static GetInstance({ dbManager }: { dbManager: DBManager }): AppVersionCache {
+    if (!AppVersionCache.instance) {
+      AppVersionCache.instance = new AppVersionCache({ dbManager });
+    }
+
+    return AppVersionCache.instance;
+  }
+
+  constructor({ dbManager }: { dbManager: DBManager }) {
+    this.negativeAppNameCache = new Map();
+    this.appRulesCache = new Map();
+    this.appVersionsCache = new Map();
+    this.dbManager = dbManager;
+  }
+
+  private get CacheIsEmpty(): boolean {
+    return this.appRulesCache.size === 0 && this.appVersionsCache.size === 0;
+  }
+
+  /**
+   * Used to populate the cache when the cache is completely empty
+   * @param appName
+   */
+  private async PopulateEmptyCache({
+    key: { AppName },
+  }: {
+    key: { AppName: string };
+  }): Promise<void> {
+    const versionsAndRules = await Application.GetVersionsAndRules({
+      dbManager: this.dbManager,
+      key: { AppName },
+    });
+
+    if (!versionsAndRules || (versionsAndRules.Versions.length === 0 && !versionsAndRules.Rules)) {
+      this.negativeAppNameCache.set(AppName.toLowerCase(), {
+        timestamp: Date.now(),
+        data: undefined,
+      });
+      return;
+    }
+
+    // Remove negative cache entry if it exists
+    this.negativeAppNameCache.delete(AppName.toLowerCase());
+
+    this.appRulesCache.set(AppName.toLowerCase(), {
+      timestamp: Date.now(),
+      data: versionsAndRules.Rules,
+    });
+
+    const versionsMap = new Map<string, ICacheEntry<Version>>();
+    for (const version of versionsAndRules.Versions) {
+      versionsMap.set(version.SemVer, {
+        timestamp: Date.now(),
+        data: version,
+      });
+    }
+    this.appVersionsCache.set(AppName.toLowerCase(), versionsMap);
+  }
+
+  public ClearCache(): void {
+    this.appRulesCache.clear();
+    this.appVersionsCache.clear();
+  }
+
+  /**
+   * Get Rules for an app
+   *
+   * Populates the cache if empty
+   * Freshens a cache entry if it is stale
+   * Feturns the cached value if it is fresh
+   * Fetches a single item if the cache is not empty but the item is not in the cache
+   *
+   * @param appName
+   * @returns
+   */
+  public async GetRules({
+    key: { AppName },
+  }: {
+    key: { AppName: string };
+  }): Promise<Rules | undefined> {
+    const now = Date.now();
+
+    // Check the negative cache first
+    const negativeCacheEntry = this.negativeAppNameCache.get(AppName.toLowerCase());
+    if (negativeCacheEntry && now - negativeCacheEntry.timestamp < 60000) {
+      return undefined;
+    } else if (negativeCacheEntry) {
+      this.negativeAppNameCache.delete(AppName.toLowerCase());
+    }
+
+    const ruleCacheEntry = this.appRulesCache.get(AppName.toLowerCase());
+    if (ruleCacheEntry && now - ruleCacheEntry.timestamp < 60000) {
+      return ruleCacheEntry.data;
+    }
+
+    if (this.CacheIsEmpty || !ruleCacheEntry) {
+      await this.PopulateEmptyCache({ key: { AppName } });
+
+      return this.appRulesCache.get(AppName.toLowerCase())?.data;
+    }
+
+    const versionsAndRules = await Application.GetVersionsAndRules({
+      dbManager: this.dbManager,
+      key: { AppName: AppName },
+    });
+    this.appRulesCache.set(AppName.toLowerCase(), { timestamp: now, data: versionsAndRules.Rules });
+
+    return versionsAndRules.Rules;
+  }
+
+  /**
+   * Get Version info for an app and semVer
+   *
+   * Populates the cache if empty
+   * Freshens a cache entry if it is stale
+   * Feturns the cached value if it is fresh
+   * Fetches a single item if the cache is not empty but the item is not in the cache
+   *
+   * @param appName
+   * @returns
+   */
+  public async GetVersionInfo({
+    key: { AppName, SemVer },
+  }: {
+    key: { AppName: string; SemVer: string };
+  }): Promise<Version | undefined> {
+    const now = Date.now();
+
+    // Check the negative cache first
+    const negativeCacheEntry = this.negativeAppNameCache.get(AppName.toLowerCase());
+    if (negativeCacheEntry && now - negativeCacheEntry.timestamp < 60000) {
+      return undefined;
+    } else if (negativeCacheEntry) {
+      this.negativeAppNameCache.delete(AppName.toLowerCase());
+    }
+
+    // Check if we have the item cached and if it is fresh enough
+    const versionCacheEntry = this.appVersionsCache.get(AppName.toLowerCase());
+    if (versionCacheEntry) {
+      const versionInfo = versionCacheEntry.get(SemVer);
+      if (versionInfo && now - versionInfo.timestamp < 900000) {
+        return versionInfo.data;
+      }
+    }
+
+    if (this.CacheIsEmpty || !versionCacheEntry) {
+      await this.PopulateEmptyCache({ key: { AppName } });
+      if (!this.appVersionsCache) {
+        return undefined;
+      }
+
+      return this.appVersionsCache.get(AppName.toLowerCase())?.get(SemVer)?.data;
+    } else {
+      // We have some data in the cache, but not for this version
+      // So we need to get the data for this version
+      const versionInfo = await Version.LoadVersion({
+        dbManager: this.dbManager,
+        key: { AppName, SemVer: SemVer },
+      });
+      if (!versionInfo) {
+        return undefined;
+      }
+
+      const versionsMap = this.appVersionsCache.get(AppName.toLowerCase());
+      if (!versionsMap) {
+        return undefined;
+      }
+
+      // Save the version info in the cache
+      versionsMap.set(SemVer, { timestamp: now, data: versionInfo });
+
+      return versionInfo;
+    }
+  }
+}
diff --git a/packages/microapps-router-lib/src/index.spec.ts b/packages/microapps-router-lib/src/index.spec.ts
index 4bd2455a..287cfda6 100644
--- a/packages/microapps-router-lib/src/index.spec.ts
+++ b/packages/microapps-router-lib/src/index.spec.ts
@@ -97,7 +97,7 @@ describe('router - without prefix', () => {
     const version = new Version({
       AppName: 'Bat',
       IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
+      SemVer: '3.2.1-beta.2',
       Status: 'deployed',
       Type: 'static',
     });
@@ -106,17 +106,17 @@ describe('router - without prefix', () => {
     const rules = new Rules({
       AppName: 'Bat',
       Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      RuleSet: { default: { SemVer: '3.2.1-beta.2', AttributeName: '', AttributeValue: '' } },
     });
     await rules.Save(dbManager);
 
     // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1/' });
+    const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.2/' });
 
     expect(response).toHaveProperty('appName');
     expect(response.appName).toBe('bat');
     expect(response).toHaveProperty('semVer');
-    expect(response.semVer).toBe('3.2.1-beta.1');
+    expect(response.semVer).toBe('3.2.1-beta.2');
   });
 
   it('static app - request to app/x.y.z/ should redirect to defaultFile', async () => {
@@ -153,14 +153,15 @@ describe('router - without prefix', () => {
   });
 
   it('static app - request to app/notVersion should load app frame with defaultFile', async () => {
+    const AppName = 'Bat123';
     const app = new Application({
-      AppName: 'Bat',
+      AppName,
       DisplayName: 'Bat App',
     });
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       DefaultFile: 'bat.html',
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta.1',
@@ -170,30 +171,31 @@ describe('router - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
     });
     await rules.Save(dbManager);
 
     // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/bat/notVersion' });
+    const response = await GetRoute({ dbManager, rawPath: `/${AppName}/notVersion` });
 
     expect(response).toHaveProperty('statusCode');
     expect(response.statusCode).toBe(200);
     expect(response).toBeDefined();
-    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta.1/bat.html');
+    expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta.1/bat.html`);
   });
 
   it('should serve appframe with no default file', async () => {
+    const AppName = 'Bat124';
     const app = new Application({
-      AppName: 'Bat',
+      AppName,
       DisplayName: 'Bat App',
     });
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       DefaultFile: '',
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta1',
@@ -203,30 +205,31 @@ describe('router - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta1', AttributeName: '', AttributeValue: '' } },
     });
     await rules.Save(dbManager);
 
     // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/bat/' });
+    const response = await GetRoute({ dbManager, rawPath: `/${AppName}/` });
 
     expect(response).toBeDefined();
     expect(response).toHaveProperty('statusCode');
     expect(response.statusCode).toBe(200);
-    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta1');
+    expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta1`);
   });
 
   it('should serve appframe with sub-route', async () => {
+    const AppName = 'Bat125';
     const app = new Application({
-      AppName: 'Bat',
+      AppName,
       DisplayName: 'Bat App',
     });
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       DefaultFile: '',
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta2',
@@ -236,30 +239,31 @@ describe('router - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta2', AttributeName: '', AttributeValue: '' } },
     });
     await rules.Save(dbManager);
 
     // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/bat/demo/grid' });
+    const response = await GetRoute({ dbManager, rawPath: `/${AppName}/demo/grid` });
 
     expect(response).toBeDefined();
     expect(response).toHaveProperty('statusCode');
     expect(response.statusCode).toBe(200);
-    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta2/demo/grid');
+    expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta2/demo/grid`);
   });
 
   it('should serve appframe with sub-route', async () => {
+    const AppName = 'Bat126';
     const app = new Application({
-      AppName: 'Bat',
+      AppName,
       DisplayName: 'Bat App',
     });
     await app.Save(dbManager);
 
     const version = new Version({
-      AppName: 'Bat',
+      AppName,
       DefaultFile: 'someFile.html',
       IntegrationID: 'abcd',
       SemVer: '3.2.1-beta3',
@@ -269,19 +273,19 @@ describe('router - without prefix', () => {
     await version.Save(dbManager);
 
     const rules = new Rules({
-      AppName: 'Bat',
+      AppName,
       Version: 0,
       RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
     });
     await rules.Save(dbManager);
 
     // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/bat/demo' });
+    const response = await GetRoute({ dbManager, rawPath: `/${AppName}/demo` });
 
     expect(response).toBeDefined();
     expect(response).toHaveProperty('statusCode');
     expect(response.statusCode).toBe(200);
-    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta3/demo');
+    expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta3/demo`);
   });
 
   it('should return 404 for /favicon.ico', async () => {
diff --git a/packages/microapps-router-lib/src/index.ts b/packages/microapps-router-lib/src/index.ts
index c8f82177..eb44de40 100644
--- a/packages/microapps-router-lib/src/index.ts
+++ b/packages/microapps-router-lib/src/index.ts
@@ -1,9 +1,9 @@
-// Used by ts-convict
 import 'source-map-support/register';
 import path from 'path';
 import { pathExistsSync, readFileSync } from 'fs-extra';
-import { Application, DBManager, IVersionsAndRules, Version } from '@pwrdrvr/microapps-datalib';
+import { DBManager, Rules, Version } from '@pwrdrvr/microapps-datalib';
 import Log from './lib/log';
+import { AppVersionCache } from './app-cache';
 
 const log = Log.Instance;
 
@@ -159,10 +159,13 @@ export async function GetRoute(event: IGetRouteEvent): Promise<IGetRouteResult>
     // /someapp/somepath/somefile.foo will split into length 4 with ["", "someapp", "somepath", "somefile.foo", ""] as results
     const partsAfterPrefix = pathAfterPrefix.split('/');
 
-    const { versionsAndRules, appName } = await GetAppInfo({
+    const appName = await GetAppInfo({
       dbManager,
       appName: partsAfterPrefix.length >= 2 ? partsAfterPrefix[1] : '[root]',
     });
+    if (!appName) {
+      return { statusCode: 404, errorMessage: 'App not found' };
+    }
 
     const isRootApp = appName === '[root]';
     const appNameOrRootTrailingSlash = isRootApp ? '' : `${appName}/`;
@@ -215,7 +218,7 @@ export async function GetRoute(event: IGetRouteEvent): Promise<IGetRouteResult>
     // ^  ^^^^^^^^^^^^
     // 0             1
     // Got at least an application name, try to route it
-    const response = RouteApp({
+    const response = await RouteApp({
       dbManager,
       normalizedPathPrefix,
       event,
@@ -224,7 +227,6 @@ export async function GetRoute(event: IGetRouteEvent): Promise<IGetRouteResult>
       possibleSemVerPathAfterApp,
       possibleSemVerQuery: queryStringParameters?.get('appver') || '',
       additionalParts,
-      versionsAndRules,
       appNameOrRootTrailingSlash,
     });
     if (response) {
@@ -244,44 +246,32 @@ export async function GetRoute(event: IGetRouteEvent): Promise<IGetRouteResult>
   }
 }
 
-export interface IAppInfo {
-  appName: string;
-  versionsAndRules: IVersionsAndRules;
-}
-
 /**
- * Get info about an app or the root app
+ * Determine if we have an appname or a catch all app
  */
 export async function GetAppInfo(opts: {
   dbManager: DBManager;
   appName: string;
-}): Promise<IAppInfo> {
+}): Promise<string | undefined> {
   const { dbManager, appName } = opts;
 
-  let versionsAndRules: IVersionsAndRules;
+  let rules: Rules | undefined;
 
-  versionsAndRules = await Application.GetVersionsAndRules({
-    dbManager,
-    key: { AppName: appName },
-  });
+  const appVersionCache = AppVersionCache.GetInstance({ dbManager });
 
-  if (versionsAndRules.Versions.length > 0) {
-    return {
-      appName,
-      versionsAndRules,
-    };
+  // Check if we got a matching app name
+  rules = await appVersionCache.GetRules({ key: { AppName: appName } });
+  if (rules && rules.AppName === appName.toLowerCase()) {
+    return appName;
   }
 
   // Check if we have a `[root]` app that is a catch all
-  versionsAndRules = await Application.GetVersionsAndRules({
-    dbManager,
-    key: { AppName: '[root]' },
-  });
+  rules = await appVersionCache.GetRules({ key: { AppName: '[root]' } });
+  if (rules && rules.AppName === '[root]') {
+    return '[root]';
+  }
 
-  return {
-    appName: '[root]',
-    versionsAndRules,
-  };
+  return undefined;
 }
 
 /**
@@ -293,7 +283,7 @@ export async function GetAppInfo(opts: {
  * @param log
  * @returns
  */
-function RouteApp(opts: {
+async function RouteApp(opts: {
   dbManager: DBManager;
   event: IGetRouteEvent;
   appName: string;
@@ -302,10 +292,10 @@ function RouteApp(opts: {
   possibleSemVerQuery?: string;
   additionalParts: string;
   normalizedPathPrefix?: string;
-  versionsAndRules: IVersionsAndRules;
   appNameOrRootTrailingSlash: string;
-}): IGetRouteResult {
+}): Promise<IGetRouteResult> {
   const {
+    dbManager,
     event,
     normalizedPathPrefix = '',
     appName,
@@ -313,21 +303,28 @@ function RouteApp(opts: {
     possibleSemVerPathAfterApp,
     possibleSemVerQuery,
     additionalParts,
-    versionsAndRules,
     appNameOrRootTrailingSlash,
   } = opts;
 
   let versionInfoToUse: Version | undefined;
 
+  const appVersionCache = AppVersionCache.GetInstance({ dbManager });
+
   // Check if the semver placeholder is actually a defined version
   const possibleSemVerPathAfterAppVersionInfo = possibleSemVerPathAfterApp
-    ? versionsAndRules.Versions.find((item) => item.SemVer === possibleSemVerPathAfterApp)
+    ? await appVersionCache.GetVersionInfo({
+        key: { AppName: appName, SemVer: possibleSemVerPathAfterApp },
+      })
     : undefined;
   const possibleSemVerPathNextDataVersionInfo = possibleSemVerPathNextData
-    ? versionsAndRules.Versions.find((item) => item.SemVer === possibleSemVerPathNextData)
+    ? await appVersionCache.GetVersionInfo({
+        key: { AppName: appName, SemVer: possibleSemVerPathNextData },
+      })
     : undefined;
   const possibleSemVerQueryVersionInfo = possibleSemVerQuery
-    ? versionsAndRules.Versions.find((item) => item.SemVer === possibleSemVerQuery)
+    ? await appVersionCache.GetVersionInfo({
+        key: { AppName: appName, SemVer: possibleSemVerQuery },
+      })
     : undefined;
 
   // If there is a version in the path, use it
@@ -377,7 +374,8 @@ function RouteApp(opts: {
     //     80% to 1.1.0, 20% to default (1.0.3)
     //
 
-    const defaultVersion = versionsAndRules.Rules?.RuleSet.default?.SemVer;
+    const rules = await appVersionCache.GetRules({ key: { AppName: appName } });
+    const defaultVersion = rules?.RuleSet.default?.SemVer;
 
     if (defaultVersion == null) {
       log.error(`could not find app ${appName}, for path ${event.rawPath} - returning 404`, {
@@ -390,10 +388,9 @@ function RouteApp(opts: {
       };
     }
 
-    // TODO: Yeah, this is lame - We should save these in a dictionary keyed by SemVer
-    const defaultVersionInfo = versionsAndRules.Versions.find(
-      (item) => item.SemVer === defaultVersion,
-    );
+    const defaultVersionInfo = await appVersionCache.GetVersionInfo({
+      key: { AppName: appName, SemVer: defaultVersion },
+    });
 
     versionInfoToUse = defaultVersionInfo;
   }
@@ -487,11 +484,13 @@ async function RedirectToDefaultFile(opts: {
     appNameOrRootTrailingSlash,
     semVer,
   } = opts;
-  let versionInfo: Version;
+  let versionInfo: Version | undefined;
 
   try {
-    versionInfo = await Version.LoadVersion({
-      dbManager,
+    // Get the cache
+    const appVersionCache = AppVersionCache.GetInstance({ dbManager });
+
+    versionInfo = await appVersionCache.GetVersionInfo({
       key: { AppName: appName, SemVer: semVer },
     });
   } catch (error) {
diff --git a/yarn.lock b/yarn.lock
index 775e57b3..ce2aa132 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2875,61 +2875,61 @@
   resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz"
   integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
 
-"@jest/console@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.4.3.tgz#1f25a99f7f860e4c46423b5b1038262466fadde1"
-  integrity sha512-W/o/34+wQuXlgqlPYTansOSiBnuxrTv61dEVkA6HNmpcgHLUjfaUbdqt6oVvOzaawwo9IdW9QOtMgQ1ScSZC4A==
+"@jest/console@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.5.0.tgz#593a6c5c0d3f75689835f1b3b4688c4f8544cb57"
+  integrity sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==
   dependencies:
-    "@jest/types" "^29.4.3"
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
     chalk "^4.0.0"
-    jest-message-util "^29.4.3"
-    jest-util "^29.4.3"
+    jest-message-util "^29.5.0"
+    jest-util "^29.5.0"
     slash "^3.0.0"
 
-"@jest/core@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.4.3.tgz#829dd65bffdb490de5b0f69e97de8e3b5eadd94b"
-  integrity sha512-56QvBq60fS4SPZCuM7T+7scNrkGIe7Mr6PVIXUpu48ouvRaWOFqRPV91eifvFM0ay2HmfswXiGf97NGUN5KofQ==
+"@jest/core@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.5.0.tgz#76674b96904484e8214614d17261cc491e5f1f03"
+  integrity sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==
   dependencies:
-    "@jest/console" "^29.4.3"
-    "@jest/reporters" "^29.4.3"
-    "@jest/test-result" "^29.4.3"
-    "@jest/transform" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/console" "^29.5.0"
+    "@jest/reporters" "^29.5.0"
+    "@jest/test-result" "^29.5.0"
+    "@jest/transform" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
     ansi-escapes "^4.2.1"
     chalk "^4.0.0"
     ci-info "^3.2.0"
     exit "^0.1.2"
     graceful-fs "^4.2.9"
-    jest-changed-files "^29.4.3"
-    jest-config "^29.4.3"
-    jest-haste-map "^29.4.3"
-    jest-message-util "^29.4.3"
+    jest-changed-files "^29.5.0"
+    jest-config "^29.5.0"
+    jest-haste-map "^29.5.0"
+    jest-message-util "^29.5.0"
     jest-regex-util "^29.4.3"
-    jest-resolve "^29.4.3"
-    jest-resolve-dependencies "^29.4.3"
-    jest-runner "^29.4.3"
-    jest-runtime "^29.4.3"
-    jest-snapshot "^29.4.3"
-    jest-util "^29.4.3"
-    jest-validate "^29.4.3"
-    jest-watcher "^29.4.3"
+    jest-resolve "^29.5.0"
+    jest-resolve-dependencies "^29.5.0"
+    jest-runner "^29.5.0"
+    jest-runtime "^29.5.0"
+    jest-snapshot "^29.5.0"
+    jest-util "^29.5.0"
+    jest-validate "^29.5.0"
+    jest-watcher "^29.5.0"
     micromatch "^4.0.4"
-    pretty-format "^29.4.3"
+    pretty-format "^29.5.0"
     slash "^3.0.0"
     strip-ansi "^6.0.0"
 
-"@jest/environment@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.4.3.tgz#9fe2f3169c3b33815dc4bd3960a064a83eba6548"
-  integrity sha512-dq5S6408IxIa+lr54zeqce+QgI+CJT4nmmA+1yzFgtcsGK8c/EyiUb9XQOgz3BMKrRDfKseeOaxj2eO8LlD3lA==
+"@jest/environment@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.5.0.tgz#9152d56317c1fdb1af389c46640ba74ef0bb4c65"
+  integrity sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==
   dependencies:
-    "@jest/fake-timers" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/fake-timers" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
-    jest-mock "^29.4.3"
+    jest-mock "^29.5.0"
 
 "@jest/expect-utils@^29.4.3":
   version "29.4.3"
@@ -2938,46 +2938,53 @@
   dependencies:
     jest-get-type "^29.4.3"
 
-"@jest/expect@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.4.3.tgz#d31a28492e45a6bcd0f204a81f783fe717045c6e"
-  integrity sha512-iktRU/YsxEtumI9zsPctYUk7ptpC+AVLLk1Ax3AsA4g1C+8OOnKDkIQBDHtD5hA/+VtgMd5AWI5gNlcAlt2vxQ==
+"@jest/expect-utils@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036"
+  integrity sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==
   dependencies:
-    expect "^29.4.3"
-    jest-snapshot "^29.4.3"
+    jest-get-type "^29.4.3"
 
-"@jest/fake-timers@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.4.3.tgz#31e982638c60fa657d310d4b9d24e023064027b0"
-  integrity sha512-4Hote2MGcCTWSD2gwl0dwbCpBRHhE6olYEuTj8FMowdg3oQWNKr2YuxenPQYZ7+PfqPY1k98wKDU4Z+Hvd4Tiw==
+"@jest/expect@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.5.0.tgz#80952f5316b23c483fbca4363ce822af79c38fba"
+  integrity sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==
   dependencies:
-    "@jest/types" "^29.4.3"
+    expect "^29.5.0"
+    jest-snapshot "^29.5.0"
+
+"@jest/fake-timers@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.5.0.tgz#d4d09ec3286b3d90c60bdcd66ed28d35f1b4dc2c"
+  integrity sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==
+  dependencies:
+    "@jest/types" "^29.5.0"
     "@sinonjs/fake-timers" "^10.0.2"
     "@types/node" "*"
-    jest-message-util "^29.4.3"
-    jest-mock "^29.4.3"
-    jest-util "^29.4.3"
+    jest-message-util "^29.5.0"
+    jest-mock "^29.5.0"
+    jest-util "^29.5.0"
 
-"@jest/globals@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.4.3.tgz#63a2c4200d11bc6d46f12bbe25b07f771fce9279"
-  integrity sha512-8BQ/5EzfOLG7AaMcDh7yFCbfRLtsc+09E1RQmRBI4D6QQk4m6NSK/MXo+3bJrBN0yU8A2/VIcqhvsOLFmziioA==
+"@jest/globals@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.5.0.tgz#6166c0bfc374c58268677539d0c181f9c1833298"
+  integrity sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==
   dependencies:
-    "@jest/environment" "^29.4.3"
-    "@jest/expect" "^29.4.3"
-    "@jest/types" "^29.4.3"
-    jest-mock "^29.4.3"
+    "@jest/environment" "^29.5.0"
+    "@jest/expect" "^29.5.0"
+    "@jest/types" "^29.5.0"
+    jest-mock "^29.5.0"
 
-"@jest/reporters@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.4.3.tgz#0a68a0c0f20554760cc2e5443177a0018969e353"
-  integrity sha512-sr2I7BmOjJhyqj9ANC6CTLsL4emMoka7HkQpcoMRlhCbQJjz2zsRzw0BDPiPyEFDXAbxKgGFYuQZiSJ1Y6YoTg==
+"@jest/reporters@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.5.0.tgz#985dfd91290cd78ddae4914ba7921bcbabe8ac9b"
+  integrity sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==
   dependencies:
     "@bcoe/v8-coverage" "^0.2.3"
-    "@jest/console" "^29.4.3"
-    "@jest/test-result" "^29.4.3"
-    "@jest/transform" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/console" "^29.5.0"
+    "@jest/test-result" "^29.5.0"
+    "@jest/transform" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@jridgewell/trace-mapping" "^0.3.15"
     "@types/node" "*"
     chalk "^4.0.0"
@@ -2990,9 +2997,9 @@
     istanbul-lib-report "^3.0.0"
     istanbul-lib-source-maps "^4.0.0"
     istanbul-reports "^3.1.3"
-    jest-message-util "^29.4.3"
-    jest-util "^29.4.3"
-    jest-worker "^29.4.3"
+    jest-message-util "^29.5.0"
+    jest-util "^29.5.0"
+    jest-worker "^29.5.0"
     slash "^3.0.0"
     string-length "^4.0.1"
     strip-ansi "^6.0.0"
@@ -3014,42 +3021,42 @@
     callsites "^3.0.0"
     graceful-fs "^4.2.9"
 
-"@jest/test-result@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.4.3.tgz#e13d973d16c8c7cc0c597082d5f3b9e7f796ccb8"
-  integrity sha512-Oi4u9NfBolMq9MASPwuWTlC5WvmNRwI4S8YrQg5R5Gi47DYlBe3sh7ILTqi/LGrK1XUE4XY9KZcQJTH1WJCLLA==
+"@jest/test-result@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.5.0.tgz#7c856a6ca84f45cc36926a4e9c6b57f1973f1408"
+  integrity sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==
   dependencies:
-    "@jest/console" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/console" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/istanbul-lib-coverage" "^2.0.0"
     collect-v8-coverage "^1.0.0"
 
-"@jest/test-sequencer@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.4.3.tgz#0862e876a22993385a0f3e7ea1cc126f208a2898"
-  integrity sha512-yi/t2nES4GB4G0mjLc0RInCq/cNr9dNwJxcGg8sslajua5Kb4kmozAc+qPLzplhBgfw1vLItbjyHzUN92UXicw==
+"@jest/test-sequencer@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz#34d7d82d3081abd523dbddc038a3ddcb9f6d3cc4"
+  integrity sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==
   dependencies:
-    "@jest/test-result" "^29.4.3"
+    "@jest/test-result" "^29.5.0"
     graceful-fs "^4.2.9"
-    jest-haste-map "^29.4.3"
+    jest-haste-map "^29.5.0"
     slash "^3.0.0"
 
-"@jest/transform@^29.4.3":
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.4.3.tgz#f7d17eac9cb5bb2e1222ea199c7c7e0835e0c037"
-  integrity sha512-8u0+fBGWolDshsFgPQJESkDa72da/EVwvL+II0trN2DR66wMwiQ9/CihaGfHdlLGFzbBZwMykFtxuwFdZqlKwg==
+"@jest/transform@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.5.0.tgz#cf9c872d0965f0cbd32f1458aa44a2b1988b00f9"
+  integrity sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==
   dependencies:
     "@babel/core" "^7.11.6"
-    "@jest/types" "^29.4.3"
+    "@jest/types" "^29.5.0"
     "@jridgewell/trace-mapping" "^0.3.15"
     babel-plugin-istanbul "^6.1.1"
     chalk "^4.0.0"
     convert-source-map "^2.0.0"
     fast-json-stable-stringify "^2.1.0"
     graceful-fs "^4.2.9"
-    jest-haste-map "^29.4.3"
+    jest-haste-map "^29.5.0"
     jest-regex-util "^29.4.3"
-    jest-util "^29.4.3"
+    jest-util "^29.5.0"
     micromatch "^4.0.4"
     pirates "^4.0.4"
     slash "^3.0.0"
@@ -3078,6 +3085,18 @@
     "@types/yargs" "^17.0.8"
     chalk "^4.0.0"
 
+"@jest/types@^29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593"
+  integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==
+  dependencies:
+    "@jest/schemas" "^29.4.3"
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^3.0.0"
+    "@types/node" "*"
+    "@types/yargs" "^17.0.8"
+    chalk "^4.0.0"
+
 "@jridgewell/gen-mapping@^0.1.0":
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz"
@@ -3489,7 +3508,7 @@
   dependencies:
     type-detect "4.0.8"
 
-"@sinonjs/fake-timers@>=5":
+"@sinonjs/fake-timers@>=5", "@sinonjs/fake-timers@^9.1.2":
   version "9.1.2"
   resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz"
   integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==
@@ -3503,35 +3522,28 @@
   dependencies:
     "@sinonjs/commons" "^2.0.0"
 
-"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz"
-  integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
-  dependencies:
-    "@sinonjs/commons" "^1.7.0"
-
-"@sinonjs/fake-timers@^7.0.4", "@sinonjs/fake-timers@^7.1.0", "@sinonjs/fake-timers@^7.1.2":
+"@sinonjs/fake-timers@^7.0.4":
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz"
   integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
-"@sinonjs/samsam@^5.3.1":
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.3.1.tgz"
-  integrity sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==
+"@sinonjs/samsam@^6.0.1":
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.1.tgz"
+  integrity sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==
   dependencies:
     "@sinonjs/commons" "^1.6.0"
     lodash.get "^4.4.2"
     type-detect "^4.0.8"
 
-"@sinonjs/samsam@^6.0.1", "@sinonjs/samsam@^6.0.2":
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.1.tgz"
-  integrity sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==
+"@sinonjs/samsam@^7.0.1":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-7.0.1.tgz#5b5fa31c554636f78308439d220986b9523fc51f"
+  integrity sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==
   dependencies:
-    "@sinonjs/commons" "^1.6.0"
+    "@sinonjs/commons" "^2.0.0"
     lodash.get "^4.4.2"
     type-detect "^4.0.8"
 
@@ -3657,10 +3669,10 @@
   dependencies:
     "@types/istanbul-lib-report" "*"
 
-"@types/jest@29.4.0":
-  version "29.4.0"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.0.tgz#a8444ad1704493e84dbf07bb05990b275b3b9206"
-  integrity sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==
+"@types/jest@29.5.0":
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.0.tgz#337b90bbcfe42158f39c2fb5619ad044bbb518ac"
+  integrity sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==
   dependencies:
     expect "^29.0.0"
     pretty-format "^29.0.0"
@@ -3743,13 +3755,6 @@
   resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
   integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
 
-"@types/sinon@10.0.10":
-  version "10.0.10"
-  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.10.tgz"
-  integrity sha512-US5E539UfeL2DiWALzCyk0c4zKh6sCv86V/0lpda/afMJJ0oEm2SrKgedH5optvFWstnJ8e1MNYhLmPhAy4rvQ==
-  dependencies:
-    "@sinonjs/fake-timers" "^7.1.0"
-
 "@types/sinon@^10.0.0":
   version "10.0.11"
   resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.11.tgz"
@@ -3757,6 +3762,13 @@
   dependencies:
     "@types/sinonjs__fake-timers" "*"
 
+"@types/sinon@^10.0.10":
+  version "10.0.13"
+  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.13.tgz#60a7a87a70d9372d0b7b38cc03e825f46981fb83"
+  integrity sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==
+  dependencies:
+    "@types/sinonjs__fake-timers" "*"
+
 "@types/sinonjs__fake-timers@*":
   version "8.1.2"
   resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz"
@@ -4290,21 +4302,23 @@ aws-cdk@^2.24.1:
   optionalDependencies:
     fsevents "2.3.2"
 
-aws-sdk-client-mock@^0.3.0:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/aws-sdk-client-mock/-/aws-sdk-client-mock-0.3.1.tgz"
-  integrity sha512-k0YIHv3aczTzwHzgkuPKnsZ73BGzsystWV8fmVkXNT9/0AdniO4SaOeGe1ZG93qhmPHb4ezWkosYp70TMijE7A==
+aws-sdk-client-mock@2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/aws-sdk-client-mock/-/aws-sdk-client-mock-2.1.0.tgz#58247c0fb651cddedbd093c0c06d6bc0caf82e89"
+  integrity sha512-JkrPrcEvQ4JwylVdQ0njYQMFVwVaZzoVc557rsCorIMjEtmrFvlVYEfIKWoYw8psU4cLjOMyqpxg65NcAn9fvQ==
   dependencies:
-    sinon "^9.2.4"
+    "@types/sinon" "^10.0.10"
+    sinon "^14.0.2"
+    tslib "^2.1.0"
 
-aws-sdk-client-mock@^0.6.2:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/aws-sdk-client-mock/-/aws-sdk-client-mock-0.6.2.tgz"
-  integrity sha512-0kH1cyfqGogp69vJitzVducx9q691vGOwg7wEG+DUegGlepjaeskp6YG4SJI0y5Vwh3vi+fP27YQTcWllv4GlQ==
+axios@1.3.4:
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024"
+  integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==
   dependencies:
-    "@types/sinon" "10.0.10"
-    sinon "^11.1.1"
-    tslib "^2.1.0"
+    follow-redirects "^1.15.0"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
 
 axios@^0.21.1:
   version "0.21.4"
@@ -4313,15 +4327,15 @@ axios@^0.21.1:
   dependencies:
     follow-redirects "^1.14.0"
 
-babel-jest@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.4.3.tgz#478b84d430972b277ad67dd631be94abea676792"
-  integrity sha512-o45Wyn32svZE+LnMVWv/Z4x0SwtLbh4FyGcYtR20kIWd+rdrDZ9Fzq8Ml3MYLD+mZvEdzCjZsCnYZ2jpJyQ+Nw==
+babel-jest@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.5.0.tgz#3fe3ddb109198e78b1c88f9ebdecd5e4fc2f50a5"
+  integrity sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==
   dependencies:
-    "@jest/transform" "^29.4.3"
+    "@jest/transform" "^29.5.0"
     "@types/babel__core" "^7.1.14"
     babel-plugin-istanbul "^6.1.1"
-    babel-preset-jest "^29.4.3"
+    babel-preset-jest "^29.5.0"
     chalk "^4.0.0"
     graceful-fs "^4.2.9"
     slash "^3.0.0"
@@ -4337,10 +4351,10 @@ babel-plugin-istanbul@^6.1.1:
     istanbul-lib-instrument "^5.0.4"
     test-exclude "^6.0.0"
 
-babel-plugin-jest-hoist@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.3.tgz#ad1dfb5d31940957e00410ef7d9b2aa94b216101"
-  integrity sha512-mB6q2q3oahKphy5V7CpnNqZOCkxxZ9aokf1eh82Dy3jQmg4xvM1tGrh5y6BQUJh4a3Pj9+eLfwvAZ7VNKg7H8Q==
+babel-plugin-jest-hoist@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a"
+  integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==
   dependencies:
     "@babel/template" "^7.3.3"
     "@babel/types" "^7.3.3"
@@ -4365,12 +4379,12 @@ babel-preset-current-node-syntax@^1.0.0:
     "@babel/plugin-syntax-optional-chaining" "^7.8.3"
     "@babel/plugin-syntax-top-level-await" "^7.8.3"
 
-babel-preset-jest@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.4.3.tgz#bb926b66ae253b69c6e3ef87511b8bb5c53c5b52"
-  integrity sha512-gWx6COtSuma6n9bw+8/F+2PCXrIgxV/D1TJFnp6OyBK2cxPWg0K9p/sriNYeifKjpUkMViWQ09DSWtzJQRETsw==
+babel-preset-jest@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2"
+  integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==
   dependencies:
-    babel-plugin-jest-hoist "^29.4.3"
+    babel-plugin-jest-hoist "^29.5.0"
     babel-preset-current-node-syntax "^1.0.0"
 
 balanced-match@^1.0.0:
@@ -6082,7 +6096,7 @@ exit@^0.1.2:
   resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz"
   integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
 
-expect@^29.0.0, expect@^29.4.3:
+expect@^29.0.0:
   version "29.4.3"
   resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.3.tgz#5e47757316df744fe3b8926c3ae8a3ebdafff7fe"
   integrity sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg==
@@ -6093,6 +6107,17 @@ expect@^29.0.0, expect@^29.4.3:
     jest-message-util "^29.4.3"
     jest-util "^29.4.3"
 
+expect@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/expect/-/expect-29.5.0.tgz#68c0509156cb2a0adb8865d413b137eeaae682f7"
+  integrity sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==
+  dependencies:
+    "@jest/expect-utils" "^29.5.0"
+    jest-get-type "^29.4.3"
+    jest-matcher-utils "^29.5.0"
+    jest-message-util "^29.5.0"
+    jest-util "^29.5.0"
+
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
@@ -6248,6 +6273,11 @@ follow-redirects@^1.14.0:
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz"
   integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==
 
+follow-redirects@^1.15.0:
+  version "1.15.2"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
 for-each@^0.3.3:
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz"
@@ -6264,6 +6294,15 @@ form-data@^3.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 fp-and-or@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/fp-and-or/-/fp-and-or-0.1.3.tgz"
@@ -7220,82 +7259,83 @@ jake@^10.8.5:
     filelist "^1.0.1"
     minimatch "^3.0.4"
 
-jest-changed-files@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.3.tgz#7961fe32536b9b6d5c28dfa0abcfab31abcf50a7"
-  integrity sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ==
+jest-changed-files@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e"
+  integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==
   dependencies:
     execa "^5.0.0"
     p-limit "^3.1.0"
 
-jest-circus@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.4.3.tgz#fff7be1cf5f06224dd36a857d52a9efeb005ba04"
-  integrity sha512-Vw/bVvcexmdJ7MLmgdT3ZjkJ3LKu8IlpefYokxiqoZy6OCQ2VAm6Vk3t/qHiAGUXbdbJKJWnc8gH3ypTbB/OBw==
+jest-circus@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.5.0.tgz#b5926989449e75bff0d59944bae083c9d7fb7317"
+  integrity sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==
   dependencies:
-    "@jest/environment" "^29.4.3"
-    "@jest/expect" "^29.4.3"
-    "@jest/test-result" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/environment" "^29.5.0"
+    "@jest/expect" "^29.5.0"
+    "@jest/test-result" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
     chalk "^4.0.0"
     co "^4.6.0"
     dedent "^0.7.0"
     is-generator-fn "^2.0.0"
-    jest-each "^29.4.3"
-    jest-matcher-utils "^29.4.3"
-    jest-message-util "^29.4.3"
-    jest-runtime "^29.4.3"
-    jest-snapshot "^29.4.3"
-    jest-util "^29.4.3"
+    jest-each "^29.5.0"
+    jest-matcher-utils "^29.5.0"
+    jest-message-util "^29.5.0"
+    jest-runtime "^29.5.0"
+    jest-snapshot "^29.5.0"
+    jest-util "^29.5.0"
     p-limit "^3.1.0"
-    pretty-format "^29.4.3"
+    pretty-format "^29.5.0"
+    pure-rand "^6.0.0"
     slash "^3.0.0"
     stack-utils "^2.0.3"
 
-jest-cli@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.4.3.tgz#fe31fdd0c90c765f392b8b7c97e4845071cd2163"
-  integrity sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg==
+jest-cli@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.5.0.tgz#b34c20a6d35968f3ee47a7437ff8e53e086b4a67"
+  integrity sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==
   dependencies:
-    "@jest/core" "^29.4.3"
-    "@jest/test-result" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/core" "^29.5.0"
+    "@jest/test-result" "^29.5.0"
+    "@jest/types" "^29.5.0"
     chalk "^4.0.0"
     exit "^0.1.2"
     graceful-fs "^4.2.9"
     import-local "^3.0.2"
-    jest-config "^29.4.3"
-    jest-util "^29.4.3"
-    jest-validate "^29.4.3"
+    jest-config "^29.5.0"
+    jest-util "^29.5.0"
+    jest-validate "^29.5.0"
     prompts "^2.0.1"
     yargs "^17.3.1"
 
-jest-config@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.4.3.tgz#fca9cdfe6298ae6d04beef1624064d455347c978"
-  integrity sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ==
+jest-config@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.5.0.tgz#3cc972faec8c8aaea9ae158c694541b79f3748da"
+  integrity sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==
   dependencies:
     "@babel/core" "^7.11.6"
-    "@jest/test-sequencer" "^29.4.3"
-    "@jest/types" "^29.4.3"
-    babel-jest "^29.4.3"
+    "@jest/test-sequencer" "^29.5.0"
+    "@jest/types" "^29.5.0"
+    babel-jest "^29.5.0"
     chalk "^4.0.0"
     ci-info "^3.2.0"
     deepmerge "^4.2.2"
     glob "^7.1.3"
     graceful-fs "^4.2.9"
-    jest-circus "^29.4.3"
-    jest-environment-node "^29.4.3"
+    jest-circus "^29.5.0"
+    jest-environment-node "^29.5.0"
     jest-get-type "^29.4.3"
     jest-regex-util "^29.4.3"
-    jest-resolve "^29.4.3"
-    jest-runner "^29.4.3"
-    jest-util "^29.4.3"
-    jest-validate "^29.4.3"
+    jest-resolve "^29.5.0"
+    jest-runner "^29.5.0"
+    jest-util "^29.5.0"
+    jest-validate "^29.5.0"
     micromatch "^4.0.4"
     parse-json "^5.2.0"
-    pretty-format "^29.4.3"
+    pretty-format "^29.5.0"
     slash "^3.0.0"
     strip-json-comments "^3.1.1"
 
@@ -7319,6 +7359,16 @@ jest-diff@^29.4.3:
     jest-get-type "^29.4.3"
     pretty-format "^29.4.3"
 
+jest-diff@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.5.0.tgz#e0d83a58eb5451dcc1fa61b1c3ee4e8f5a290d63"
+  integrity sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==
+  dependencies:
+    chalk "^4.0.0"
+    diff-sequences "^29.4.3"
+    jest-get-type "^29.4.3"
+    pretty-format "^29.5.0"
+
 jest-docblock@^29.4.3:
   version "29.4.3"
   resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8"
@@ -7335,28 +7385,28 @@ jest-dynalite@3.6.1:
     dynalite "^3.2.1"
     setimmediate "^1.0.5"
 
-jest-each@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.4.3.tgz#a434c199a2f6151c5e3dc80b2d54586bdaa72819"
-  integrity sha512-1ElHNAnKcbJb/b+L+7j0/w7bDvljw4gTv1wL9fYOczeJrbTbkMGQ5iQPFJ3eFQH19VPTx1IyfePdqSpePKss7Q==
+jest-each@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.5.0.tgz#fc6e7014f83eac68e22b7195598de8554c2e5c06"
+  integrity sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==
   dependencies:
-    "@jest/types" "^29.4.3"
+    "@jest/types" "^29.5.0"
     chalk "^4.0.0"
     jest-get-type "^29.4.3"
-    jest-util "^29.4.3"
-    pretty-format "^29.4.3"
+    jest-util "^29.5.0"
+    pretty-format "^29.5.0"
 
-jest-environment-node@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.4.3.tgz#579c4132af478befc1889ddc43c2413a9cdbe014"
-  integrity sha512-gAiEnSKF104fsGDXNkwk49jD/0N0Bqu2K9+aMQXA6avzsA9H3Fiv1PW2D+gzbOSR705bWd2wJZRFEFpV0tXISg==
+jest-environment-node@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.5.0.tgz#f17219d0f0cc0e68e0727c58b792c040e332c967"
+  integrity sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==
   dependencies:
-    "@jest/environment" "^29.4.3"
-    "@jest/fake-timers" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/environment" "^29.5.0"
+    "@jest/fake-timers" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
-    jest-mock "^29.4.3"
-    jest-util "^29.4.3"
+    jest-mock "^29.5.0"
+    jest-util "^29.5.0"
 
 jest-get-type@^26.3.0:
   version "26.3.0"
@@ -7368,32 +7418,32 @@ jest-get-type@^29.4.3:
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5"
   integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==
 
-jest-haste-map@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.4.3.tgz#085a44283269e7ace0645c63a57af0d2af6942e2"
-  integrity sha512-eZIgAS8tvm5IZMtKlR8Y+feEOMfo2pSQkmNbufdbMzMSn9nitgGxF1waM/+LbryO3OkMcKS98SUb+j/cQxp/vQ==
+jest-haste-map@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.5.0.tgz#69bd67dc9012d6e2723f20a945099e972b2e94de"
+  integrity sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==
   dependencies:
-    "@jest/types" "^29.4.3"
+    "@jest/types" "^29.5.0"
     "@types/graceful-fs" "^4.1.3"
     "@types/node" "*"
     anymatch "^3.0.3"
     fb-watchman "^2.0.0"
     graceful-fs "^4.2.9"
     jest-regex-util "^29.4.3"
-    jest-util "^29.4.3"
-    jest-worker "^29.4.3"
+    jest-util "^29.5.0"
+    jest-worker "^29.5.0"
     micromatch "^4.0.4"
     walker "^1.0.8"
   optionalDependencies:
     fsevents "^2.3.2"
 
-jest-leak-detector@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.4.3.tgz#2b35191d6b35aa0256e63a9b79b0f949249cf23a"
-  integrity sha512-9yw4VC1v2NspMMeV3daQ1yXPNxMgCzwq9BocCwYrRgXe4uaEJPAN0ZK37nFBhcy3cUwEVstFecFLaTHpF7NiGA==
+jest-leak-detector@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz#cf4bdea9615c72bac4a3a7ba7e7930f9c0610c8c"
+  integrity sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==
   dependencies:
     jest-get-type "^29.4.3"
-    pretty-format "^29.4.3"
+    pretty-format "^29.5.0"
 
 jest-matcher-utils@^29.4.3:
   version "29.4.3"
@@ -7405,6 +7455,16 @@ jest-matcher-utils@^29.4.3:
     jest-get-type "^29.4.3"
     pretty-format "^29.4.3"
 
+jest-matcher-utils@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz#d957af7f8c0692c5453666705621ad4abc2c59c5"
+  integrity sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==
+  dependencies:
+    chalk "^4.0.0"
+    jest-diff "^29.5.0"
+    jest-get-type "^29.4.3"
+    pretty-format "^29.5.0"
+
 jest-message-util@^29.4.3:
   version "29.4.3"
   resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.3.tgz#65b5280c0fdc9419503b49d4f48d4999d481cb5b"
@@ -7420,14 +7480,29 @@ jest-message-util@^29.4.3:
     slash "^3.0.0"
     stack-utils "^2.0.3"
 
-jest-mock@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.4.3.tgz#23d84a20a74cdfff0510fdbeefb841ed57b0fe7e"
-  integrity sha512-LjFgMg+xed9BdkPMyIJh+r3KeHt1klXPJYBULXVVAkbTaaKjPX1o1uVCAZADMEp/kOxGTwy/Ot8XbvgItOrHEg==
+jest-message-util@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.5.0.tgz#1f776cac3aca332ab8dd2e3b41625435085c900e"
+  integrity sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==
   dependencies:
-    "@jest/types" "^29.4.3"
+    "@babel/code-frame" "^7.12.13"
+    "@jest/types" "^29.5.0"
+    "@types/stack-utils" "^2.0.0"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.9"
+    micromatch "^4.0.4"
+    pretty-format "^29.5.0"
+    slash "^3.0.0"
+    stack-utils "^2.0.3"
+
+jest-mock@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed"
+  integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==
+  dependencies:
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
-    jest-util "^29.4.3"
+    jest-util "^29.5.0"
 
 jest-pnp-resolver@^1.2.2:
   version "1.2.2"
@@ -7439,88 +7514,88 @@ jest-regex-util@^29.4.3:
   resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8"
   integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==
 
-jest-resolve-dependencies@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.3.tgz#9ad7f23839a6d88cef91416bda9393a6e9fd1da5"
-  integrity sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw==
+jest-resolve-dependencies@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz#f0ea29955996f49788bf70996052aa98e7befee4"
+  integrity sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==
   dependencies:
     jest-regex-util "^29.4.3"
-    jest-snapshot "^29.4.3"
+    jest-snapshot "^29.5.0"
 
-jest-resolve@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.4.3.tgz#3c5b5c984fa8a763edf9b3639700e1c7900538e2"
-  integrity sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw==
+jest-resolve@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.5.0.tgz#b053cc95ad1d5f6327f0ac8aae9f98795475ecdc"
+  integrity sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==
   dependencies:
     chalk "^4.0.0"
     graceful-fs "^4.2.9"
-    jest-haste-map "^29.4.3"
+    jest-haste-map "^29.5.0"
     jest-pnp-resolver "^1.2.2"
-    jest-util "^29.4.3"
-    jest-validate "^29.4.3"
+    jest-util "^29.5.0"
+    jest-validate "^29.5.0"
     resolve "^1.20.0"
     resolve.exports "^2.0.0"
     slash "^3.0.0"
 
-jest-runner@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.4.3.tgz#68dc82c68645eda12bea42b5beece6527d7c1e5e"
-  integrity sha512-GWPTEiGmtHZv1KKeWlTX9SIFuK19uLXlRQU43ceOQ2hIfA5yPEJC7AMkvFKpdCHx6pNEdOD+2+8zbniEi3v3gA==
+jest-runner@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.5.0.tgz#6a57c282eb0ef749778d444c1d758c6a7693b6f8"
+  integrity sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==
   dependencies:
-    "@jest/console" "^29.4.3"
-    "@jest/environment" "^29.4.3"
-    "@jest/test-result" "^29.4.3"
-    "@jest/transform" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/console" "^29.5.0"
+    "@jest/environment" "^29.5.0"
+    "@jest/test-result" "^29.5.0"
+    "@jest/transform" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
     chalk "^4.0.0"
     emittery "^0.13.1"
     graceful-fs "^4.2.9"
     jest-docblock "^29.4.3"
-    jest-environment-node "^29.4.3"
-    jest-haste-map "^29.4.3"
-    jest-leak-detector "^29.4.3"
-    jest-message-util "^29.4.3"
-    jest-resolve "^29.4.3"
-    jest-runtime "^29.4.3"
-    jest-util "^29.4.3"
-    jest-watcher "^29.4.3"
-    jest-worker "^29.4.3"
+    jest-environment-node "^29.5.0"
+    jest-haste-map "^29.5.0"
+    jest-leak-detector "^29.5.0"
+    jest-message-util "^29.5.0"
+    jest-resolve "^29.5.0"
+    jest-runtime "^29.5.0"
+    jest-util "^29.5.0"
+    jest-watcher "^29.5.0"
+    jest-worker "^29.5.0"
     p-limit "^3.1.0"
     source-map-support "0.5.13"
 
-jest-runtime@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.4.3.tgz#f25db9874dcf35a3ab27fdaabca426666cc745bf"
-  integrity sha512-F5bHvxSH+LvLV24vVB3L8K467dt3y3dio6V3W89dUz9nzvTpqd/HcT9zfYKL2aZPvD63vQFgLvaUX/UpUhrP6Q==
+jest-runtime@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.5.0.tgz#c83f943ee0c1da7eb91fa181b0811ebd59b03420"
+  integrity sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==
   dependencies:
-    "@jest/environment" "^29.4.3"
-    "@jest/fake-timers" "^29.4.3"
-    "@jest/globals" "^29.4.3"
+    "@jest/environment" "^29.5.0"
+    "@jest/fake-timers" "^29.5.0"
+    "@jest/globals" "^29.5.0"
     "@jest/source-map" "^29.4.3"
-    "@jest/test-result" "^29.4.3"
-    "@jest/transform" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/test-result" "^29.5.0"
+    "@jest/transform" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
     chalk "^4.0.0"
     cjs-module-lexer "^1.0.0"
     collect-v8-coverage "^1.0.0"
     glob "^7.1.3"
     graceful-fs "^4.2.9"
-    jest-haste-map "^29.4.3"
-    jest-message-util "^29.4.3"
-    jest-mock "^29.4.3"
+    jest-haste-map "^29.5.0"
+    jest-message-util "^29.5.0"
+    jest-mock "^29.5.0"
     jest-regex-util "^29.4.3"
-    jest-resolve "^29.4.3"
-    jest-snapshot "^29.4.3"
-    jest-util "^29.4.3"
+    jest-resolve "^29.5.0"
+    jest-snapshot "^29.5.0"
+    jest-util "^29.5.0"
     slash "^3.0.0"
     strip-bom "^4.0.0"
 
-jest-snapshot@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.4.3.tgz#183d309371450d9c4a3de7567ed2151eb0e91145"
-  integrity sha512-NGlsqL0jLPDW91dz304QTM/SNO99lpcSYYAjNiX0Ou+sSGgkanKBcSjCfp/pqmiiO1nQaOyLp6XQddAzRcx3Xw==
+jest-snapshot@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.5.0.tgz#c9c1ce0331e5b63cd444e2f95a55a73b84b1e8ce"
+  integrity sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==
   dependencies:
     "@babel/core" "^7.11.6"
     "@babel/generator" "^7.7.2"
@@ -7528,23 +7603,22 @@ jest-snapshot@^29.4.3:
     "@babel/plugin-syntax-typescript" "^7.7.2"
     "@babel/traverse" "^7.7.2"
     "@babel/types" "^7.3.3"
-    "@jest/expect-utils" "^29.4.3"
-    "@jest/transform" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/expect-utils" "^29.5.0"
+    "@jest/transform" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/babel__traverse" "^7.0.6"
     "@types/prettier" "^2.1.5"
     babel-preset-current-node-syntax "^1.0.0"
     chalk "^4.0.0"
-    expect "^29.4.3"
+    expect "^29.5.0"
     graceful-fs "^4.2.9"
-    jest-diff "^29.4.3"
+    jest-diff "^29.5.0"
     jest-get-type "^29.4.3"
-    jest-haste-map "^29.4.3"
-    jest-matcher-utils "^29.4.3"
-    jest-message-util "^29.4.3"
-    jest-util "^29.4.3"
+    jest-matcher-utils "^29.5.0"
+    jest-message-util "^29.5.0"
+    jest-util "^29.5.0"
     natural-compare "^1.4.0"
-    pretty-format "^29.4.3"
+    pretty-format "^29.5.0"
     semver "^7.3.5"
 
 jest-util@^29.0.0, jest-util@^29.4.3:
@@ -7559,51 +7633,63 @@ jest-util@^29.0.0, jest-util@^29.4.3:
     graceful-fs "^4.2.9"
     picomatch "^2.2.3"
 
-jest-validate@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.4.3.tgz#a13849dec4f9e95446a7080ad5758f58fa88642f"
-  integrity sha512-J3u5v7aPQoXPzaar6GndAVhdQcZr/3osWSgTeKg5v574I9ybX/dTyH0AJFb5XgXIB7faVhf+rS7t4p3lL9qFaw==
+jest-util@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f"
+  integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==
   dependencies:
-    "@jest/types" "^29.4.3"
+    "@jest/types" "^29.5.0"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    ci-info "^3.2.0"
+    graceful-fs "^4.2.9"
+    picomatch "^2.2.3"
+
+jest-validate@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.5.0.tgz#8e5a8f36178d40e47138dc00866a5f3bd9916ffc"
+  integrity sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==
+  dependencies:
+    "@jest/types" "^29.5.0"
     camelcase "^6.2.0"
     chalk "^4.0.0"
     jest-get-type "^29.4.3"
     leven "^3.1.0"
-    pretty-format "^29.4.3"
+    pretty-format "^29.5.0"
 
-jest-watcher@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.4.3.tgz#e503baa774f0c2f8f3c8db98a22ebf885f19c384"
-  integrity sha512-zwlXH3DN3iksoIZNk73etl1HzKyi5FuQdYLnkQKm5BW4n8HpoG59xSwpVdFrnh60iRRaRBGw0gcymIxjJENPcA==
+jest-watcher@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.5.0.tgz#cf7f0f949828ba65ddbbb45c743a382a4d911363"
+  integrity sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==
   dependencies:
-    "@jest/test-result" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/test-result" "^29.5.0"
+    "@jest/types" "^29.5.0"
     "@types/node" "*"
     ansi-escapes "^4.2.1"
     chalk "^4.0.0"
     emittery "^0.13.1"
-    jest-util "^29.4.3"
+    jest-util "^29.5.0"
     string-length "^4.0.1"
 
-jest-worker@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.3.tgz#9a4023e1ea1d306034237c7133d7da4240e8934e"
-  integrity sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==
+jest-worker@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.5.0.tgz#bdaefb06811bd3384d93f009755014d8acb4615d"
+  integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==
   dependencies:
     "@types/node" "*"
-    jest-util "^29.4.3"
+    jest-util "^29.5.0"
     merge-stream "^2.0.0"
     supports-color "^8.0.0"
 
-jest@29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest/-/jest-29.4.3.tgz#1b8be541666c6feb99990fd98adac4737e6e6386"
-  integrity sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA==
+jest@29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/jest/-/jest-29.5.0.tgz#f75157622f5ce7ad53028f2f8888ab53e1f1f24e"
+  integrity sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==
   dependencies:
-    "@jest/core" "^29.4.3"
-    "@jest/types" "^29.4.3"
+    "@jest/core" "^29.5.0"
+    "@jest/types" "^29.5.0"
     import-local "^3.0.2"
-    jest-cli "^29.4.3"
+    jest-cli "^29.5.0"
 
 jju@^1.1.0:
   version "1.4.0"
@@ -8458,18 +8544,7 @@ nice-try@^1.0.4:
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
-nise@^4.0.4:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/nise/-/nise-4.1.0.tgz"
-  integrity sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==
-  dependencies:
-    "@sinonjs/commons" "^1.7.0"
-    "@sinonjs/fake-timers" "^6.0.0"
-    "@sinonjs/text-encoding" "^0.7.1"
-    just-extend "^4.0.2"
-    path-to-regexp "^1.7.0"
-
-nise@^5.0.1, nise@^5.1.0:
+nise@^5.0.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.1.tgz"
   integrity sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==
@@ -8480,6 +8555,17 @@ nise@^5.0.1, nise@^5.1.0:
     just-extend "^4.0.2"
     path-to-regexp "^1.7.0"
 
+nise@^5.1.2:
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0"
+  integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==
+  dependencies:
+    "@sinonjs/commons" "^2.0.0"
+    "@sinonjs/fake-timers" "^10.0.2"
+    "@sinonjs/text-encoding" "^0.7.1"
+    just-extend "^4.0.2"
+    path-to-regexp "^1.7.0"
+
 node-fetch@^2.6.1:
   version "2.6.7"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz"
@@ -9110,6 +9196,15 @@ pretty-format@^29.0.0, pretty-format@^29.4.3:
     ansi-styles "^5.0.0"
     react-is "^18.0.0"
 
+pretty-format@^29.5.0:
+  version "29.5.0"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a"
+  integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==
+  dependencies:
+    "@jest/schemas" "^29.4.3"
+    ansi-styles "^5.0.0"
+    react-is "^18.0.0"
+
 proc-log@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz"
@@ -9164,6 +9259,11 @@ prompts@^2.0.1, prompts@^2.4.2:
     kleur "^3.0.3"
     sisteransi "^1.0.5"
 
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
 prr@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz"
@@ -9196,6 +9296,11 @@ pupa@^2.1.1:
   dependencies:
     escape-goat "^2.0.0"
 
+pure-rand@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.1.tgz#31207dddd15d43f299fdcdb2f572df65030c19af"
+  integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==
+
 q@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz"
@@ -9652,30 +9757,18 @@ sinon@^10.0.0:
     nise "^5.0.1"
     supports-color "^7.1.0"
 
-sinon@^11.1.1:
-  version "11.1.2"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-11.1.2.tgz"
-  integrity sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==
+sinon@^14.0.2:
+  version "14.0.2"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.2.tgz#585a81a3c7b22cf950762ac4e7c28eb8b151c46f"
+  integrity sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==
   dependencies:
-    "@sinonjs/commons" "^1.8.3"
-    "@sinonjs/fake-timers" "^7.1.2"
-    "@sinonjs/samsam" "^6.0.2"
+    "@sinonjs/commons" "^2.0.0"
+    "@sinonjs/fake-timers" "^9.1.2"
+    "@sinonjs/samsam" "^7.0.1"
     diff "^5.0.0"
-    nise "^5.1.0"
+    nise "^5.1.2"
     supports-color "^7.2.0"
 
-sinon@^9.2.4:
-  version "9.2.4"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.4.tgz"
-  integrity sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==
-  dependencies:
-    "@sinonjs/commons" "^1.8.1"
-    "@sinonjs/fake-timers" "^6.0.1"
-    "@sinonjs/samsam" "^5.3.1"
-    diff "^4.0.2"
-    nise "^4.0.4"
-    supports-color "^7.1.0"
-
 sisteransi@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz"


Test patch:
diff --git a/tests/integration/demo-app.spec.ts b/tests/integration/demo-app.spec.ts
index e69de29b..c2b7bf87 100644
--- a/tests/integration/demo-app.spec.ts
+++ b/tests/integration/demo-app.spec.ts
@@ -0,0 +1,152 @@
+/// <reference types="jest" />
+import axios from 'axios';
+
+jest.setTimeout(10000);
+jest.retryTimes(2);
+
+const DEMO_AND_ROOT_APP_VER = process.env.DEMO_AND_ROOT_APP_VER || '0.0.0';
+
+describe('demo-app', () => {
+  const baseUrl = new URL(process.env.BASE_URL || 'https://apps.ghpublic.pwrdrvr.com/');
+  const demoAppUrl = new URL('demo-app', baseUrl);
+
+  describe('single account - non-root app', () => {
+    it('should return a status of 200', async () => {
+      const response = await axios.get(demoAppUrl.toString(), {
+        headers: { Accept: 'text/html' },
+      });
+      expect(response.status).toEqual(200);
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-semver', '0.0.0');
+    });
+
+    it('should return HTML with an expected string', async () => {
+      const response = await axios.get(demoAppUrl.toString(), {
+        headers: { Accept: 'text/html' },
+      });
+      const data = response.data;
+
+      expect(data).toContain('<div>This is a simple demo app</div>');
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-semver', '0.0.0');
+    });
+
+    it('should return JSON with an expected value', async () => {
+      const response = await axios.get(
+        new URL('/demo-app/serverIncrement?currValue=1', baseUrl).toString(),
+        {
+          headers: { Accept: 'application/json' },
+        },
+      );
+      const data = response.data;
+
+      expect(data).toMatchObject({
+        newValue: 2,
+        source: 'demo-app',
+      });
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-semver', '0.0.0');
+    });
+  });
+
+  describe('single account - root app', () => {
+    it('should return a status of 200', async () => {
+      const response = await axios.get(
+        new URL(`/?appver=${DEMO_AND_ROOT_APP_VER}`, baseUrl).toString(),
+        {
+          headers: { Accept: 'text/html' },
+        },
+      );
+      expect(response.status).toEqual(200);
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', '[root]');
+      expect(response.headers).toHaveProperty('x-microapps-semver', DEMO_AND_ROOT_APP_VER);
+    });
+
+    it('should return HTML with an expected string', async () => {
+      const response = await axios.get(
+        new URL(`/?appver=${DEMO_AND_ROOT_APP_VER}`, baseUrl).toString(),
+        {
+          headers: { Accept: 'text/html' },
+        },
+      );
+      const data = response.data;
+
+      expect(data).toContain('<div>This is a simple demo app</div>');
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', '[root]');
+      expect(response.headers).toHaveProperty('x-microapps-semver', DEMO_AND_ROOT_APP_VER);
+    });
+
+    it('should return JSON with an expected value', async () => {
+      const response = await axios.get(
+        new URL(`/serverIncrement?currValue=1&appver=${DEMO_AND_ROOT_APP_VER}`, baseUrl).toString(),
+        {
+          headers: { Accept: 'application/json' },
+        },
+      );
+      const data = response.data;
+
+      expect(data).toMatchObject({
+        newValue: 2,
+        source: 'demo-app',
+      });
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', '[root]');
+      expect(response.headers).toHaveProperty('x-microapps-semver', DEMO_AND_ROOT_APP_VER);
+    });
+  });
+
+  describe('single account - child account version of app', () => {
+    const childVer = '0.0.0-child.1';
+
+    it('should return a status of 200', async () => {
+      const response = await axios.get(
+        new URL(`/demo-app?appver=${childVer}`, baseUrl).toString(),
+        {
+          headers: { Accept: 'text/html' },
+        },
+      );
+      expect(response.status).toEqual(200);
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-semver', '0.0.0-child.1');
+    });
+
+    it('should return HTML with an expected string', async () => {
+      const response = await axios.get(
+        new URL(`/demo-app?appver=${childVer}`, baseUrl).toString(),
+        {
+          headers: { Accept: 'text/html' },
+        },
+      );
+      const data = response.data;
+
+      expect(data).toContain('<div>This is a simple demo app</div>');
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-semver', '0.0.0-child.1');
+    });
+
+    it('should return JSON with an expected value', async () => {
+      const response = await axios.get(
+        new URL(`/demo-app/serverIncrement?currValue=1&appver=${childVer}`, baseUrl).toString(),
+        {
+          headers: { Accept: 'application/json' },
+        },
+      );
+      const data = response.data;
+
+      expect(data).toMatchObject({
+        newValue: 2,
+        source: 'demo-app',
+      });
+      expect(response.headers).toHaveProperty('powered-by', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-appname', 'demo-app');
+      expect(response.headers).toHaveProperty('x-microapps-semver', '0.0.0-child.1');
+    });
+  });
+});
diff --git a/tests/integration/nextjs-demo.spec.ts b/tests/integration/nextjs-demo.spec.ts
new file mode 100644
index 00000000..e9dd7b53
--- /dev/null
+++ b/tests/integration/nextjs-demo.spec.ts
@@ -0,0 +1,83 @@
+/// <reference types="jest" />
+import axios from 'axios';
+
+jest.setTimeout(10000);
+jest.retryTimes(2);
+
+const NEXTJS_DEMO_APP_VER = process.env.NEXTJS_DEMO_APP_VER || '0.0.0';
+
+describe('nextjs-demo', () => {
+  const baseUrl = new URL(process.env.BASE_URL || 'https://apps.ghpublic.pwrdrvr.com/');
+  const demoAppUrl = new URL('nextjs-demo', baseUrl);
+
+  it('should return a status of 200', async () => {
+    const response = await axios.get(demoAppUrl.toString(), {
+      headers: { Accept: 'text/html' },
+    });
+    expect(response.status).toEqual(200);
+    expect(response.headers).toHaveProperty('x-powered-by', 'Next.js');
+  });
+
+  it('should return HTML with an expected string', async () => {
+    const response = await axios.get(demoAppUrl.toString(), {
+      headers: { Accept: 'text/html' },
+    });
+    const data = response.data;
+
+    expect(data).toContain('<title>PwrDrvr Website - 2</title>');
+    expect(response.headers).toHaveProperty('x-powered-by', 'Next.js');
+  });
+
+  it('should sub page HTML with an expected string - ssg-ssr', async () => {
+    const response = await axios.get(new URL('/nextjs-demo/posts/ssg-ssr', baseUrl).toString(), {
+      headers: { Accept: 'text/html' },
+    });
+    const data = response.data;
+
+    expect(data).toContain(
+      '<title>When to Use Static Generation v.s. Server-side Rendering</title>',
+    );
+    expect(response.headers).toHaveProperty('x-powered-by', 'Next.js');
+  });
+
+  it('should sub page HTML with an expected string - pre-rendering', async () => {
+    const response = await axios.get(
+      new URL('/nextjs-demo/posts/pre-rendering', baseUrl).toString(),
+      {
+        headers: { Accept: 'text/html' },
+      },
+    );
+    const data = response.data;
+
+    expect(data).toContain('<title>Two Forms of Pre-rendering</title>');
+    expect(response.headers).toHaveProperty('x-powered-by', 'Next.js');
+  });
+
+  it('should return JSON for home page _next/data route', async () => {
+    const response = await axios.get(
+      new URL(`/nextjs-demo/_next/data/${NEXTJS_DEMO_APP_VER}/index.json`, baseUrl).toString(),
+      {
+        headers: { Accept: 'application/json' },
+      },
+    );
+    const data = response.data;
+
+    expect(data).toMatchObject({
+      __N_SSG: true,
+      pageProps: {
+        allPostsData: [
+          {
+            date: '2020-01-02',
+            id: 'ssg-ssr',
+            title: 'When to Use Static Generation v.s. Server-side Rendering',
+          },
+          {
+            date: '2020-01-01',
+            id: 'pre-rendering',
+            title: 'Two Forms of Pre-rendering',
+          },
+        ],
+      },
+    });
+  });
+});