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

Issue:
microapps-router-lib - Route `_next/data` requests for apps without basePath
## Motivations
- Next.js apps with `basePath` set are complicated to get working correclty
- Leaving `basePath` unset, but setting `assetPrefix` should allow multi-page apps to be hosted on the same domain

## Example Client-Side Nav Request

`https://apps.ghpublic.pwrdrvr.com/_next/data/0.0.0-pr.37/nextjs-demo/posts/ssg-ssr.json?id=ssg-ssr`

## Acceptance Criteria
- [x] Parse the app name after `_next/data` if it comes at the root of the deployment
- [x] Route requests using that app name
- [x] Remove the `basePath` setting from the Next.js Demo app to test


Discussion / hints:
nextjs-demo without basePath: https://github.com/pwrdrvr/microapps-app-nextjs-demo/pull/37

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 54670267..db6103eb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -232,7 +232,7 @@ jobs:
         run: yarn esbuild:edge-to-origin
     
       - name: Configure AWS Credentials
-        uses: aws-actions/configure-aws-credentials@v1
+        uses: aws-actions/configure-aws-credentials@v2
         with:
           role-session-name: microapps-ghpublic-build
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/builder-writeRole
@@ -497,7 +497,7 @@ jobs:
       # Change to Child Account Creds
       - name: Change to Child Account
         if: ${{ matrix.deployName == 'microapps-core' }}
-        uses: aws-actions/configure-aws-credentials@v1
+        uses: aws-actions/configure-aws-credentials@v2
         with:
           role-session-name: microapps-ghpublic-build
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID_CHILD }}:role/builder-writeRole
@@ -527,7 +527,7 @@ jobs:
 
       - name: Change Back to Parent AWS Account
         if: ${{ matrix.deployName == 'microapps-core' }}
-        uses: aws-actions/configure-aws-credentials@v1
+        uses: aws-actions/configure-aws-credentials@v2
         with:
           role-session-name: microapps-ghpublic-build
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/builder-writeRole
@@ -544,7 +544,7 @@ jobs:
 
       - name: Change Back to Child Account
         if: ${{ matrix.deployName == 'microapps-core' }}
-        uses: aws-actions/configure-aws-credentials@v1
+        uses: aws-actions/configure-aws-credentials@v2
         with:
           role-session-name: microapps-ghpublic-build
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID_CHILD }}:role/builder-writeRole
@@ -601,7 +601,7 @@ jobs:
       - name: Run Integration Tests
         env:
           BASE_URL: https://${{ env.EDGE_DOMAIN }}${{ env.PREFIX }}/
-          DEMO_AND_ROOT_APP_VER: '${{ env.PACKAGE_VERSION }}'
+          DEMO_AND_ROOT_APP_VER: '0.0.0' # '${{ env.PACKAGE_VERSION }}'
           NEXTJS_DEMO_APP_VER: '${{ needs.build.outputs.nextjsDemoAppPackageVersion }}'
           DEPLOY_NAME: ${{ matrix.deployName }}
         run: |
diff --git a/.github/workflows/pr-closed.yml b/.github/workflows/pr-closed.yml
index fc569849..d742f88b 100644
--- a/.github/workflows/pr-closed.yml
+++ b/.github/workflows/pr-closed.yml
@@ -42,7 +42,7 @@ jobs:
       #
       - name: Change to Parent Account
         if: ${{ matrix.deployName == 'microapps-core' }}
-        uses: aws-actions/configure-aws-credentials@v1
+        uses: aws-actions/configure-aws-credentials@v2
         with:
           role-session-name: microapps-ghpublic-build
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/builder-writeRole
@@ -59,7 +59,7 @@ jobs:
       #
       - name: Change to Child Account
         if: ${{ matrix.deployName == 'microapps-core' }}
-        uses: aws-actions/configure-aws-credentials@v1
+        uses: aws-actions/configure-aws-credentials@v2
         with:
           role-session-name: microapps-ghpublic-build
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID_CHILD }}:role/builder-writeRole
@@ -76,7 +76,7 @@ jobs:
       # to invoke the Parent Deployer, when applicable
       #
       - name: Change to Parent Account
-        uses: aws-actions/configure-aws-credentials@v1
+        uses: aws-actions/configure-aws-credentials@v2
         with:
           role-session-name: microapps-ghpublic-build
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/builder-writeRole
diff --git a/packages/microapps-cdk/src/MicroAppsS3.ts b/packages/microapps-cdk/src/MicroAppsS3.ts
index a63dec26..1ebcc9fd 100644
--- a/packages/microapps-cdk/src/MicroAppsS3.ts
+++ b/packages/microapps-cdk/src/MicroAppsS3.ts
@@ -161,6 +161,8 @@ export class MicroAppsS3 extends Construct implements IMicroAppsS3 {
     this._bucketLogs = new s3.Bucket(this, 'logs', {
       bucketName: props.bucketLogsName,
       autoDeleteObjects: s3AutoDeleteItems,
+      accessControl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL,
+      objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
       removalPolicy,
     });
 
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 d327ba1d..b6b3ee53 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
@@ -1855,6 +1855,143 @@ describe('edge-to-origin - routing - without prefix', () => {
     });
   });
 
+  it('should route `direct` /_next/data/[semver]/[appname] request to root when [appname] does not exist', 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 AppName = 'BatDirectNoBaseNextDataToRoot';
+
+      const app = new Application({
+        AppName,
+        DisplayName: 'Direct Bat App',
+      });
+      await app.Save(dbManager);
+      {
+        const version = new Version({
+          AppName,
+          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,
+        Version: 0,
+        RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+      });
+      await rules.Save(dbManager);
+
+      //
+      // Setup Root App
+      //
+      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://abc123.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: '',
+                  clientIp: '1.1.1.1',
+                  uri: '/_next/data/1.2.3/someNonAppPagePath/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('abc123.lambda-url.us-east-1.on.aws');
+      expect(requestResponse.headers).toHaveProperty('x-microapps-appname');
+      expect(requestResponse.headers['x-microapps-appname'][0].key).toBe('X-MicroApps-AppName');
+      expect(requestResponse.headers['x-microapps-appname'][0].value).toBe('[root]');
+      expect(requestResponse.headers).toHaveProperty('x-microapps-semver');
+      expect(requestResponse.headers['x-microapps-semver'][0].key).toBe('X-MicroApps-SemVer');
+      expect(requestResponse.headers['x-microapps-semver'][0].value).toBe('1.2.3');
+      expect(requestResponse.uri).toBe('/_next/data/1.2.3/someNonAppPagePath/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',
+      );
+    });
+  });
+
   // 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 () => {
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 00a92c66..a2e489cd 100644
--- a/packages/microapps-edge-to-origin/src/index.route.spec.ts
+++ b/packages/microapps-edge-to-origin/src/index.route.spec.ts
@@ -291,7 +291,139 @@ describe('edge-to-origin - routing - without prefix', () => {
     expect(requestResponse?.origin?.custom?.domainName).toBe('abc123.lambda-url.us-east-1.on.aws');
   });
 
-  it('should route `direct` _next/data request with appName and version to origin', async () => {
+  describe('/_next/data/ requests with no basePath', () => {
+    const testCases = [
+      [
+        {
+          AppName: 'BatDirectNoBaseNextData',
+          LambdaURL1: 'https://abc123.lambda-url.us-east-1.on.aws/',
+          SemVer1: '1.2.1-beta.1',
+          LambdaURL2: 'https://abc1234567.lambda-url.us-east-1.on.aws/',
+          SemVer2: '1.2.1-beta.2',
+          SuffixPath: 'batdirectnobasenextdata/route.json',
+        },
+      ],
+      [
+        {
+          AppName: 'BatDirectNoBaseNextDataRootRoute',
+          LambdaURL1: 'https://abc124.lambda-url.us-east-1.on.aws/',
+          SemVer1: '1.2.1-beta.3',
+          LambdaURL2: 'https://abc1234568.lambda-url.us-east-1.on.aws/',
+          SemVer2: '1.2.1-beta.4',
+          SuffixPath: 'batdirectnobasenextdatarootroute.json',
+        },
+      ],
+    ];
+
+    it.each(testCases)(
+      'should route `direct` /_next/data/[${SemVer2}]/[${AppName}] request to [${AppName}] when it exists but ${SemVer1} is the default',
+      async ({ AppName, LambdaURL1, SemVer1, LambdaURL2, SemVer2, SuffixPath }) => {
+        theConfig.replaceHostHeader = true;
+
+        const app = new Application({
+          AppName,
+          DisplayName: 'Direct Bat App',
+        });
+        await app.Save(dbManager);
+        {
+          const version = new Version({
+            AppName,
+            SemVer: SemVer1,
+            Status: 'deployed',
+            Type: 'lambda-url',
+            StartupType: 'direct',
+            URL: LambdaURL1,
+          });
+          await version.Save(dbManager);
+        }
+        {
+          const version = new Version({
+            AppName,
+            SemVer: SemVer2,
+            Status: 'deployed',
+            Type: 'lambda-url',
+            StartupType: 'direct',
+            URL: LambdaURL2,
+          });
+          await version.Save(dbManager);
+        }
+        const rules = new Rules({
+          AppName,
+          Version: 0,
+          RuleSet: { default: { SemVer: SemVer1, 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/${SemVer2}/${SuffixPath}`,
+                    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(new URL(LambdaURL2).hostname);
+        expect(requestResponse.headers['x-microapps-appname'][0].key).toBe('X-MicroApps-AppName');
+        expect(requestResponse.headers['x-microapps-appname'][0].value).toBe(AppName.toLowerCase());
+        expect(requestResponse.headers).toHaveProperty('x-microapps-semver');
+        expect(requestResponse.headers['x-microapps-semver'][0].key).toBe('X-MicroApps-SemVer');
+        expect(requestResponse.headers['x-microapps-semver'][0].value).toBe(SemVer2);
+        expect(requestResponse.uri).toBe(`/_next/data/${SemVer2}/${SuffixPath}`);
+        expect(requestResponse).toHaveProperty('origin');
+        expect(requestResponse.origin).toHaveProperty('custom');
+        expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+        expect(requestResponse?.origin?.custom?.domainName).toBe(new URL(LambdaURL2).hostname);
+      },
+    );
+  });
+
+  it('should route `direct` /[appname]/_next/data request with appName and version to origin', async () => {
     theConfig.replaceHostHeader = true;
 
     const app = new Application({
@@ -395,6 +527,115 @@ describe('edge-to-origin - routing - without prefix', () => {
     );
   });
 
+  it('should route `direct` /_next/data/[semver]/[appname] request to [appname] when it exists', async () => {
+    theConfig.replaceHostHeader = true;
+    const AppName = 'BatDirectNoBaseNextData';
+
+    const app = new Application({
+      AppName,
+      DisplayName: 'Direct Bat App',
+    });
+    await app.Save(dbManager);
+    {
+      const version = new Version({
+        AppName,
+        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,
+        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,
+      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/${AppName.toLowerCase()}/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.headers['x-microapps-appname'][0].key).toBe('X-MicroApps-AppName');
+    expect(requestResponse.headers['x-microapps-appname'][0].value).toBe(AppName.toLowerCase());
+    expect(requestResponse.headers).toHaveProperty('x-microapps-semver');
+    expect(requestResponse.headers['x-microapps-semver'][0].key).toBe('X-MicroApps-SemVer');
+    expect(requestResponse.headers['x-microapps-semver'][0].value).toBe('1.2.2-beta.1');
+    expect(requestResponse.uri).toBe(
+      `/_next/data/1.2.2-beta.1/${AppName.toLowerCase()}/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 with appName and version to origin', async () => {
     theConfig.replaceHostHeader = true;
 
diff --git a/packages/microapps-router-lib/src/index.spec.ts b/packages/microapps-router-lib/src/index.spec.ts
index 287cfda6..7cdfe27d 100644
--- a/packages/microapps-router-lib/src/index.spec.ts
+++ b/packages/microapps-router-lib/src/index.spec.ts
@@ -21,302 +21,457 @@ describe('router - without prefix', () => {
     dbManager = new DBManager({ dynamoClient, tableName: TEST_TABLE_NAME });
   });
 
-  it('should serve appframe with version and default file substitued', async () => {
-    const app = new Application({
-      AppName: 'Bat',
-      DisplayName: 'Bat App',
+  describe('StartupType: iframe', () => {
+    it('should serve appframe 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);
+
+      // Call the handler
+      const response = await GetRoute({ dbManager, rawPath: '/bat/' });
+
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(200);
+      expect(response).toBeDefined();
+      expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta.1/bat.html');
     });
-    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: '' } },
+    it('static app - request to app/x.y.z should redirect to defaultFile', 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: 'static',
+        StartupType: 'iframe',
+      });
+      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);
+
+      // Call the handler
+      const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1' });
+
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(302);
+      expect(response.redirectLocation).toBeDefined();
+      expect(response.redirectLocation).toBe('/bat/3.2.1-beta.1/bat.html');
+    }, 60000);
+
+    it('static app - request to app/x.y.z/ should not redirect if no defaultFile', async () => {
+      const app = new Application({
+        AppName: 'Bat',
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName: 'Bat',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta.2',
+        Status: 'deployed',
+        Type: 'static',
+        StartupType: 'iframe',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        AppName: 'Bat',
+        Version: 0,
+        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.2/' });
+
+      expect(response).toHaveProperty('appName');
+      expect(response.appName).toBe('bat');
+      expect(response).toHaveProperty('semVer');
+      expect(response.semVer).toBe('3.2.1-beta.2');
     });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/bat/' });
-
-    expect(response).toHaveProperty('statusCode');
-    expect(response.statusCode).toBe(200);
-    expect(response).toBeDefined();
-    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta.1/bat.html');
-  });
 
-  it('static app - request to app/x.y.z should redirect to defaultFile', 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: 'static',
+    it('static app - request to app/x.y.z/ should redirect to defaultFile', 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: 'static',
+        StartupType: 'iframe',
+      });
+      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);
+
+      // Call the handler
+      const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1/' });
+
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(302);
+      expect(response.redirectLocation).toBeDefined();
+      expect(response.redirectLocation).toBe('/bat/3.2.1-beta.1/bat.html');
     });
-    await version.Save(dbManager);
 
-    const rules = new Rules({
-      AppName: 'Bat',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+    it('static app - request to app/notVersion should load app frame with defaultFile', async () => {
+      const AppName = 'Bat123';
+      const app = new Application({
+        AppName,
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName,
+        DefaultFile: 'bat.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta.1',
+        Status: 'deployed',
+        Type: 'static',
+        StartupType: 'iframe',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        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: `/${AppName}/notVersion` });
+
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(200);
+      expect(response).toBeDefined();
+      expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta.1/bat.html`);
     });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1' });
 
-    expect(response).toHaveProperty('statusCode');
-    expect(response.statusCode).toBe(302);
-    expect(response.redirectLocation).toBeDefined();
-    expect(response.redirectLocation).toBe('/bat/3.2.1-beta.1/bat.html');
-  }, 60000);
-
-  it('static app - request to app/x.y.z/ should not redirect if no defaultFile', async () => {
-    const app = new Application({
-      AppName: 'Bat',
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName: 'Bat',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.2',
-      Status: 'deployed',
-      Type: 'static',
+    it('should serve appframe with no default file', async () => {
+      const AppName = 'Bat124';
+      const app = new Application({
+        AppName,
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName,
+        DefaultFile: '',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta1',
+        Status: 'deployed',
+        Type: 'lambda',
+        StartupType: 'iframe',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        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: `/${AppName}/` });
+
+      expect(response).toBeDefined();
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(200);
+      expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta1`);
     });
-    await version.Save(dbManager);
 
-    const rules = new Rules({
-      AppName: 'Bat',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.2', AttributeName: '', AttributeValue: '' } },
+    it('should serve appframe with sub-route', async () => {
+      const AppName = 'Bat125';
+      const app = new Application({
+        AppName,
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName,
+        DefaultFile: '',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta2',
+        Status: 'deployed',
+        Type: 'lambda',
+        StartupType: 'iframe',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        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: `/${AppName}/demo/grid` });
+
+      expect(response).toBeDefined();
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(200);
+      expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta2/demo/grid`);
     });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    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.2');
-  });
-
-  it('static app - request to app/x.y.z/ should redirect to defaultFile', 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: 'static',
+    it('should serve appframe with sub-route', async () => {
+      const AppName = 'Bat126';
+      const app = new Application({
+        AppName,
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName,
+        DefaultFile: 'someFile.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta3',
+        Status: 'deployed',
+        Type: 'lambda',
+        StartupType: 'iframe',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        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: `/${AppName}/demo` });
+
+      expect(response).toBeDefined();
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(200);
+      expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta3/demo`);
     });
-    await version.Save(dbManager);
 
-    const rules = new Rules({
-      AppName: 'Bat',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+    it('should return 404 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',
+        StartupType: 'iframe',
+      });
+      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);
+
+      // Call the handler
+      const response = await GetRoute({ dbManager, rawPath: '/favicon.ico' });
+
+      expect(response).toBeDefined();
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(404);
     });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1/' });
-
-    expect(response).toHaveProperty('statusCode');
-    expect(response.statusCode).toBe(302);
-    expect(response.redirectLocation).toBeDefined();
-    expect(response.redirectLocation).toBe('/bat/3.2.1-beta.1/bat.html');
   });
 
-  it('static app - request to app/notVersion should load app frame with defaultFile', async () => {
-    const AppName = 'Bat123';
-    const app = new Application({
-      AppName,
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName,
-      DefaultFile: 'bat.html',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta.1',
-      Status: 'deployed',
-      Type: 'static',
-    });
-    await version.Save(dbManager);
-
-    const rules = new Rules({
-      AppName,
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+  describe('StartupType: direct', () => {
+    it('should serve direct app with sub-route', async () => {
+      const AppName = 'DirectBat126';
+      const app = new Application({
+        AppName,
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName,
+        DefaultFile: 'someFile.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta3',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'direct',
+        URL: 'https://abc123.lambda-url.us-east-1.on.aws',
+        LambdaARN: 'arn:aws:lambda:us-east-1:123456789012:function:my-function',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        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: `/${AppName}/demo` });
+
+      expect(response).toBeDefined();
+      expect(response).toHaveProperty('statusCode');
+      expect(response.statusCode).toBe(200);
+      expect(response.appName).toBe(AppName);
+      expect(response.semVer).toBe('3.2.1-beta3');
+      expect(response.url).toBe('https://abc123.lambda-url.us-east-1.on.aws');
     });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: `/${AppName}/notVersion` });
 
-    expect(response).toHaveProperty('statusCode');
-    expect(response.statusCode).toBe(200);
-    expect(response).toBeDefined();
-    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,
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName,
-      DefaultFile: '',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta1',
-      Status: 'deployed',
-      Type: 'lambda',
+    describe('/_next/data/ no-basePath', () => {
+      const testCases = [
+        [
+          'DirectBatNextData',
+          '3.2.1-beta3',
+          'directbatnextdata/demo.json',
+          'https://abc123.lambda-url.us-east-1.on.aws',
+        ],
+        [
+          'AnotherAppName',
+          '1.0.0',
+          'anotherappname.json',
+          'https://abc124.lambda-url.us-east-1.on.aws',
+        ],
+        // Add more test cases as needed
+      ];
+
+      it.each(testCases)(
+        'should serve direct app with /_next/data/%s/%s/%s route',
+        async (AppName, SemVer, RoutePathSuffix, LambdaFunctionURL) => {
+          const app = new Application({
+            AppName,
+            DisplayName: 'Bat App',
+          });
+          await app.Save(dbManager);
+
+          const version = new Version({
+            AppName,
+            DefaultFile: 'someFile.html',
+            IntegrationID: 'abcd',
+            SemVer,
+            Status: 'deployed',
+            Type: 'lambda-url',
+            StartupType: 'direct',
+            URL: LambdaFunctionURL,
+            LambdaARN: 'arn:aws:lambda:us-east-1:123456789012:function:my-function',
+          });
+          await version.Save(dbManager);
+
+          const rules = new Rules({
+            AppName,
+            Version: 0,
+            RuleSet: { default: { SemVer, AttributeName: '', AttributeValue: '' } },
+          });
+          await rules.Save(dbManager);
+
+          // Call the handler
+          const response = await GetRoute({
+            dbManager,
+            rawPath: `/_next/data/${SemVer}/${RoutePathSuffix}`,
+          });
+
+          expect(response).toBeDefined();
+          expect(response).toHaveProperty('statusCode');
+          expect(response.appName).toBe(AppName.toLowerCase());
+          expect(response.semVer).toBe(SemVer);
+          expect(response.statusCode).toBe(200);
+          expect(response.url).toBe(LambdaFunctionURL);
+        },
+      );
     });
-    await version.Save(dbManager);
 
-    const rules = new Rules({
-      AppName,
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta1', AttributeName: '', AttributeValue: '' } },
+    it('should serve direct app with /_next/data/[semver]/[appname]/route route', async () => {
+      const AppName = 'DirectBatNextData';
+      const app = new Application({
+        AppName,
+        DisplayName: 'Bat App',
+      });
+      await app.Save(dbManager);
+
+      const version = new Version({
+        AppName,
+        DefaultFile: 'someFile.html',
+        IntegrationID: 'abcd',
+        SemVer: '3.2.1-beta3',
+        Status: 'deployed',
+        Type: 'lambda-url',
+        StartupType: 'direct',
+        URL: 'https://abc123.lambda-url.us-east-1.on.aws',
+        LambdaARN: 'arn:aws:lambda:us-east-1:123456789012:function:my-function',
+      });
+      await version.Save(dbManager);
+
+      const rules = new Rules({
+        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: `/_next/data/3.2.1-beta3/${AppName}/demo`,
+      });
+
+      expect(response).toBeDefined();
+      expect(response).toHaveProperty('statusCode');
+      expect(response.appName).toBe(AppName);
+      expect(response.semVer).toBe('3.2.1-beta3');
+      expect(response.statusCode).toBe(200);
+      expect(response.url).toBe('https://abc123.lambda-url.us-east-1.on.aws');
     });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: `/${AppName}/` });
-
-    expect(response).toBeDefined();
-    expect(response).toHaveProperty('statusCode');
-    expect(response.statusCode).toBe(200);
-    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,
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName,
-      DefaultFile: '',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta2',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await version.Save(dbManager);
-
-    const rules = new Rules({
-      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: `/${AppName}/demo/grid` });
-
-    expect(response).toBeDefined();
-    expect(response).toHaveProperty('statusCode');
-    expect(response.statusCode).toBe(200);
-    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,
-      DisplayName: 'Bat App',
-    });
-    await app.Save(dbManager);
-
-    const version = new Version({
-      AppName,
-      DefaultFile: 'someFile.html',
-      IntegrationID: 'abcd',
-      SemVer: '3.2.1-beta3',
-      Status: 'deployed',
-      Type: 'lambda',
-    });
-    await version.Save(dbManager);
-
-    const rules = new Rules({
-      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: `/${AppName}/demo` });
-
-    expect(response).toBeDefined();
-    expect(response).toHaveProperty('statusCode');
-    expect(response.statusCode).toBe(200);
-    expect(response.iFrameAppVersionPath).toBe(`/${AppName}/3.2.1-beta3/demo`);
-  });
-
-  it('should return 404 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);
-
-    const rules = new Rules({
-      AppName: 'Bat',
-      Version: 0,
-      RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
-    });
-    await rules.Save(dbManager);
-
-    // Call the handler
-    const response = await GetRoute({ dbManager, rawPath: '/favicon.ico' });
-
-    expect(response).toBeDefined();
-    expect(response).toHaveProperty('statusCode');
-    expect(response.statusCode).toBe(404);
   });
 });
diff --git a/packages/microapps-router-lib/src/index.ts b/packages/microapps-router-lib/src/index.ts
index d5df5719..0d013552 100644
--- a/packages/microapps-router-lib/src/index.ts
+++ b/packages/microapps-router-lib/src/index.ts
@@ -161,10 +161,31 @@ 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 appName = await GetAppInfo({
-      dbManager,
-      appName: partsAfterPrefix.length >= 2 ? partsAfterPrefix[1] : '[root]',
-    });
+    // Handle ${prefix}/_next/data/${semver}/${appname}/route
+    let appName: string | undefined;
+    if (
+      partsAfterPrefix.length >= 4 &&
+      partsAfterPrefix[1] === '_next' &&
+      partsAfterPrefix[2] === 'data'
+    ) {
+      // if partsAfterPrefix[4] has .json suffix, strip it
+      const possibleAppName = partsAfterPrefix[4].endsWith('.json')
+        ? partsAfterPrefix[4].slice(0, partsAfterPrefix[4].length - 5)
+        : partsAfterPrefix[4];
+
+      appName = await GetAppInfo({
+        dbManager,
+        appName: possibleAppName,
+      });
+    }
+
+    if (!appName) {
+      appName = await GetAppInfo({
+        dbManager,
+        appName: partsAfterPrefix.length >= 2 ? partsAfterPrefix[1] : '[root]',
+      });
+    }
+
     if (!appName) {
       return { statusCode: 404, errorMessage: 'App not found' };
     }
@@ -213,7 +234,16 @@ export async function GetRoute(event: IGetRouteEvent): Promise<IGetRouteResult>
     // Examples
     //  / semVer / somepath
     //  / _next / data / semVer / somepath
-    const possibleSemVerPathNextData = partsAfterAppName.length >= 4 ? partsAfterAppName[3] : '';
+    // Get the version afer `/_next/data/` from partsAfterPrefix
+    const possibleSemVerPathNextDataBasePath =
+      partsAfterAppName.length >= 4 ? partsAfterAppName[3] : '';
+    const possibleSemVerPathNextData =
+      partsAfterPrefix.length >= 4 &&
+      partsAfterPrefix[1] === '_next' &&
+      partsAfterPrefix[2] == 'data'
+        ? partsAfterPrefix[3]
+        : possibleSemVerPathNextDataBasePath;
+
     const possibleSemVerPathAfterApp = partsAfterAppName.length >= 2 ? partsAfterAppName[1] : '';
 
     //  (/ something)?
@@ -336,6 +366,7 @@ async function RouteApp(opts: {
     // This is a version, and it's in the path already, route the request to it
     // without creating iframe
     return {
+      statusCode: 200,
       appName,
       semVer: possibleSemVerPathVersionInfo.SemVer,
       ...(possibleSemVerPathVersionInfo?.URL ? { url: possibleSemVerPathVersionInfo?.URL } : {}),
@@ -455,6 +486,7 @@ async function RouteApp(opts: {
     }
 
     return {
+      statusCode: 200,
       appName,
       semVer: versionInfoToUse.SemVer,
       startupType: 'direct',


Test patch:
diff --git a/packages/microapps-cdk/test/__snapshots__/MicroAppsS3.spec.ts.snap b/packages/microapps-cdk/test/__snapshots__/MicroAppsS3.spec.ts.snap
index 553fdffb..e862377a 100644
--- a/packages/microapps-cdk/test/__snapshots__/MicroAppsS3.spec.ts.snap
+++ b/packages/microapps-cdk/test/__snapshots__/MicroAppsS3.spec.ts.snap
@@ -17,6 +17,16 @@ exports[`MicroAppsS3 works with no params 1`] = `
     },
     "constructlogsBC77618C": {
       "DeletionPolicy": "Retain",
+      "Properties": {
+        "AccessControl": "BucketOwnerFullControl",
+        "OwnershipControls": {
+          "Rules": [
+            {
+              "ObjectOwnership": "BucketOwnerPreferred",
+            },
+          ],
+        },
+      },
       "Type": "AWS::S3::Bucket",
       "UpdateReplacePolicy": "Retain",
     },
diff --git a/tests/integration/nextjs-demo.spec.ts b/tests/integration/nextjs-demo.spec.ts
index 31741186..28618e89 100644
--- a/tests/integration/nextjs-demo.spec.ts
+++ b/tests/integration/nextjs-demo.spec.ts
@@ -27,7 +27,7 @@ describe('nextjs-demo', () => {
     });
     const data = response.data;
 
-    expect(data).toContain('<title>PwrDrvr Website - 2</title>');
+    expect(data).toContain('<title>PwrDrvr - Next.js Demo</title>');
     expect(response.headers).toHaveProperty('x-powered-by', 'Next.js');
   });