diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts
index e6df1c2204..439f8e143a 100644
--- a/packages/realm-server/server.ts
+++ b/packages/realm-server/server.ts
@@ -278,146 +278,187 @@ export class RealmServer {
}
private serveIndex = async (ctxt: Koa.Context, next: Koa.Next) => {
- if (ctxt.header.accept?.includes('text/html')) {
- // If this is a /connect iframe request, is the origin a valid published realm?
+ let acceptHeader = ctxt.header.accept ?? '';
+ let lowerAcceptHeader = acceptHeader.toLowerCase();
+ let includesVndMimeType = lowerAcceptHeader.includes('application/vnd.');
+ let includesHtmlMimeType = lowerAcceptHeader.includes('text/html');
- let connectMatch = ctxt.request.path.match(/\/connect\/(.+)$/);
+ let requestURL = new URL(
+ `${ctxt.protocol}://${ctxt.host}${ctxt.originalUrl}`,
+ );
- if (connectMatch) {
- try {
- let originParameter = new URL(decodeURIComponent(connectMatch[1]))
- .href;
+ if (includesHtmlMimeType) {
+ if (includesVndMimeType) {
+ let isHostModeRequest = await this.isHostModeRequest(requestURL);
- let publishedRealms = await query(this.dbAdapter, [
- `SELECT published_realm_url FROM published_realms WHERE published_realm_url LIKE `,
- param(`${originParameter}%`),
- ]);
+ if (isHostModeRequest) {
+ return next();
+ }
+ }
+ } else {
+ if (includesVndMimeType) {
+ return next();
+ }
- if (publishedRealms.length === 0) {
- ctxt.status = 404;
- ctxt.body = `Not Found: No published realm found for origin ${originParameter}`;
+ let isHostModeRequest = await this.isHostModeRequest(requestURL);
- this.log.debug(
- `Ignoring /connect request for origin ${originParameter}: no matching published realm`,
- );
+ if (!isHostModeRequest) {
+ return next();
+ }
+ }
- return;
- }
+ // If this is a /connect iframe request, is the origin a valid published realm?
- ctxt.set(
- 'Content-Security-Policy',
- `frame-ancestors ${originParameter}`,
- );
- } catch (error) {
- ctxt.status = 400;
- ctxt.body = 'Bad Request';
+ let connectMatch = ctxt.request.path.match(/\/connect\/(.+)$/);
+
+ if (connectMatch) {
+ try {
+ let originParameter = new URL(decodeURIComponent(connectMatch[1])).href;
+
+ let publishedRealms = await query(this.dbAdapter, [
+ `SELECT published_realm_url FROM published_realms WHERE published_realm_url LIKE `,
+ param(`${originParameter}%`),
+ ]);
- this.log.info(`Error processing /connect request: ${error}`);
+ if (publishedRealms.length === 0) {
+ ctxt.status = 404;
+ ctxt.body = `Not Found: No published realm found for origin ${originParameter}`;
+
+ this.log.debug(
+ `Ignoring /connect request for origin ${originParameter}: no matching published realm`,
+ );
return;
}
- }
-
- ctxt.type = 'html';
- let requestURL = new URL(
- `${ctxt.protocol}://${ctxt.host}${ctxt.originalUrl}`,
- );
- let cardURL = requestURL;
- let isIndexRequest = requestURL.pathname.endsWith('/');
- if (isIndexRequest) {
- cardURL = new URL('index', requestURL);
- }
+ ctxt.set(
+ 'Content-Security-Policy',
+ `frame-ancestors ${originParameter}`,
+ );
+ } catch (error) {
+ ctxt.status = 400;
+ ctxt.body = 'Bad Request';
- let indexHTML = await this.retrieveIndexHTML();
- let hasPublicPermissions = await this.hasPublicPermissions(cardURL);
+ this.log.info(`Error processing /connect request: ${error}`);
- if (!hasPublicPermissions) {
- ctxt.body = indexHTML;
return;
}
+ }
- this.headLog.debug(`Fetching head HTML for ${cardURL.href}`);
- this.isolatedLog.debug(`Fetching isolated HTML for ${cardURL.href}`);
- this.scopedCSSLog.debug(`Fetching scoped CSS for ${cardURL.href}`);
+ ctxt.type = 'html';
- let [headHTML, isolatedHTML, scopedCSS] = await Promise.all([
- this.retrieveHeadHTML(cardURL),
- this.retrieveIsolatedHTML(cardURL),
- retrieveScopedCSS({
- cardURL,
- dbAdapter: this.dbAdapter,
- indexURLCandidates: (url) => this.indexURLCandidates(url),
- indexCandidateExpressions: (candidates) =>
- this.indexCandidateExpressions(candidates),
- log: this.scopedCSSLog,
- }),
- ]);
+ let cardURL = requestURL;
+ let isIndexRequest = requestURL.pathname.endsWith('/');
+ if (isIndexRequest) {
+ cardURL = new URL('index', requestURL);
+ }
- if (headHTML != null) {
- this.headLog.debug(
- `Injecting head HTML for ${cardURL.href} (length ${headHTML.length})\n${this.truncateLogLines(
- headHTML,
- )}`,
- );
- } else {
- this.headLog.debug(
- `No head HTML found for ${cardURL.href}, serving base index.html`,
- );
- }
+ let indexHTML = await this.retrieveIndexHTML();
+ let hasPublicPermissions = await this.hasPublicPermissions(cardURL);
- if (scopedCSS != null) {
- this.scopedCSSLog.debug(
- `Using scoped CSS for ${cardURL.href} (length ${scopedCSS.length})`,
- );
- } else {
- this.scopedCSSLog.debug(
- `No scoped CSS returned from database for ${cardURL.href}`,
- );
- }
+ if (!hasPublicPermissions) {
+ ctxt.body = indexHTML;
+ return;
+ }
- let responseHTML = indexHTML;
- let headFragments: string[] = [];
+ this.headLog.debug(`Fetching head HTML for ${cardURL.href}`);
+ this.isolatedLog.debug(`Fetching isolated HTML for ${cardURL.href}`);
+ this.scopedCSSLog.debug(`Fetching scoped CSS for ${cardURL.href}`);
- if (headHTML != null) {
- headFragments.push(headHTML);
- }
+ let [headHTML, isolatedHTML, scopedCSS] = await Promise.all([
+ this.retrieveHeadHTML(cardURL),
+ this.retrieveIsolatedHTML(cardURL),
+ retrieveScopedCSS({
+ cardURL,
+ dbAdapter: this.dbAdapter,
+ indexURLCandidates: (url) => this.indexURLCandidates(url),
+ indexCandidateExpressions: (candidates) =>
+ this.indexCandidateExpressions(candidates),
+ log: this.scopedCSSLog,
+ }),
+ ]);
- if (scopedCSS != null) {
- this.scopedCSSLog.debug(`Injecting scoped CSS for ${cardURL.href}`);
- headFragments.push(
- ``,
- );
- }
+ if (headHTML != null) {
+ this.headLog.debug(
+ `Injecting head HTML for ${cardURL.href} (length ${headHTML.length})\n${this.truncateLogLines(
+ headHTML,
+ )}`,
+ );
+ } else {
+ this.headLog.debug(
+ `No head HTML found for ${cardURL.href}, serving base index.html`,
+ );
+ }
- if (headFragments.length > 0) {
- responseHTML = this.injectHeadHTML(
- responseHTML,
- headFragments.join('\n'),
- );
- }
+ if (scopedCSS != null) {
+ this.scopedCSSLog.debug(
+ `Using scoped CSS for ${cardURL.href} (length ${scopedCSS.length})`,
+ );
+ } else {
+ this.scopedCSSLog.debug(
+ `No scoped CSS returned from database for ${cardURL.href}`,
+ );
+ }
- if (isolatedHTML != null) {
- this.isolatedLog.debug(
- `Injecting isolated HTML for ${cardURL.href} (length ${isolatedHTML.length})\n${this.truncateLogLines(
- isolatedHTML,
- )}`,
- );
- responseHTML = this.injectIsolatedHTML(responseHTML, isolatedHTML);
- }
+ let responseHTML = indexHTML;
+ let headFragments: string[] = [];
- ctxt.body = responseHTML;
- return;
+ if (headHTML != null) {
+ headFragments.push(headHTML);
}
- return next();
+
+ if (scopedCSS != null) {
+ this.scopedCSSLog.debug(`Injecting scoped CSS for ${cardURL.href}`);
+ headFragments.push(
+ ``,
+ );
+ }
+
+ if (headFragments.length > 0) {
+ responseHTML = this.injectHeadHTML(
+ responseHTML,
+ headFragments.join('\n'),
+ );
+ }
+
+ if (isolatedHTML != null) {
+ this.isolatedLog.debug(
+ `Injecting isolated HTML for ${cardURL.href} (length ${isolatedHTML.length})\n${this.truncateLogLines(
+ isolatedHTML,
+ )}`,
+ );
+ responseHTML = this.injectIsolatedHTML(responseHTML, isolatedHTML);
+ }
+
+ ctxt.body = responseHTML;
+ return;
+ return;
};
- private async hasPublicPermissions(cardURL: URL): Promise {
- let realm = this.realms.find((candidate) => {
+ private findRealmForRequestURL(requestURL: URL): Realm | undefined {
+ return this.realms.find((candidate) => {
let realmURL = new URL(candidate.url);
- realmURL.protocol = cardURL.protocol;
- return new RealmPaths(realmURL).inRealm(cardURL);
+ realmURL.protocol = requestURL.protocol;
+ return new RealmPaths(realmURL).inRealm(requestURL);
});
+ }
+
+ private async isHostModeRequest(requestURL: URL): Promise {
+ let realm = this.findRealmForRequestURL(requestURL);
+ if (!realm) {
+ return false;
+ }
+
+ let rows = await query(this.dbAdapter, [
+ `SELECT published_realm_url FROM published_realms WHERE published_realm_url =`,
+ param(realm.url),
+ ]);
+
+ return rows.length > 0;
+ }
+
+ private async hasPublicPermissions(cardURL: URL): Promise {
+ let realm = this.findRealmForRequestURL(cardURL);
if (!realm) {
return false;
diff --git a/packages/realm-server/tests/server-endpoints/index-responses-test.ts b/packages/realm-server/tests/server-endpoints/index-responses-test.ts
index eb951353ae..0d117f1ef2 100644
--- a/packages/realm-server/tests/server-endpoints/index-responses-test.ts
+++ b/packages/realm-server/tests/server-endpoints/index-responses-test.ts
@@ -1,8 +1,9 @@
import { module, test } from 'qunit';
import { join, basename } from 'path';
+import type { Test, SuperTest } from 'supertest';
import { systemInitiatedPriority } from '@cardstack/runtime-common';
import { setupServerEndpointsTest, testRealm2URL } from './helpers';
-import { waitUntil } from '../helpers';
+import { setupPermissionedRealmAtURL, waitUntil } from '../helpers';
import { ensureDirSync, writeFileSync, writeJSONSync } from 'fs-extra';
import '@cardstack/runtime-common/helpers/code-equality-assertion';
@@ -352,4 +353,49 @@ module(`server-endpoints/${basename(__filename)}`, function () {
});
},
);
+
+ module('Published realm index responses', function (hooks) {
+ let realmURL = new URL('http://127.0.0.1:4444/');
+ let request: SuperTest;
+
+ function onRealmSetup(args: {
+ request: SuperTest;
+ }) {
+ request = args.request;
+ }
+
+ setupPermissionedRealmAtURL(hooks, realmURL, {
+ permissions: {
+ '*': ['read'],
+ },
+ published: true,
+ onRealmSetup,
+ });
+
+ test('serves index HTML by default for published realm', async function (assert) {
+ let response = await request.get('/').set('Accept', 'application/json');
+
+ assert.strictEqual(response.status, 200, 'serves HTML response');
+ assert.ok(
+ response.headers['content-type']?.includes('text/html'),
+ 'content type is text/html',
+ );
+ assert.ok(
+ response.text.includes('data-test-home-card'),
+ 'index HTML is served',
+ );
+ });
+
+ test('skips index HTML when vendor mime type is requested', async function (assert) {
+ let response = await request
+ .get('/person-1')
+ .set('Accept', 'application/vnd.card+json');
+
+ assert.strictEqual(response.status, 200, 'serves JSON response');
+ assert.ok(
+ response.headers['content-type']?.includes('application/vnd.card+json'),
+ 'content type is vendor JSON',
+ );
+ });
+ });
});