From d98dbda416a4e5d4b3e2b6e709ead7873fc24711 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 30 Jan 2026 16:56:20 -0600 Subject: [PATCH 1/7] Add preliminary return override for host mode --- packages/realm-server/server.ts | 61 ++++++++++++++++--- .../server-endpoints/index-responses-test.ts | 48 ++++++++++++++- 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index 715a92ef56..89a73b22a8 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -278,7 +278,34 @@ export class RealmServer { } private serveIndex = async (ctxt: Koa.Context, next: Koa.Next) => { - if (ctxt.header.accept?.includes('text/html')) { + let acceptHeader = ctxt.header.accept ?? ''; + let lowerAcceptHeader = acceptHeader.toLowerCase(); + let includesVndMimeType = lowerAcceptHeader.includes('application/vnd.'); + let includesHtmlMimeType = lowerAcceptHeader.includes('text/html'); + + let requestURL = new URL( + `${ctxt.protocol}://${ctxt.host}${ctxt.originalUrl}`, + ); + + if (includesHtmlMimeType) { + if (includesVndMimeType) { + let isPublishedRealmRequest = + await this.isPublishedRealmRequest(requestURL); + if (isPublishedRealmRequest) { + return next(); + } + } + } else { + if (includesVndMimeType) { + return next(); + } + let isPublishedRealmRequest = + await this.isPublishedRealmRequest(requestURL); + if (!isPublishedRealmRequest) { + return next(); + } + } + // If this is a /connect iframe request, is the origin a valid published realm? let connectMatch = ctxt.request.path.match(/\/connect\/(.+)$/); @@ -320,9 +347,6 @@ export class RealmServer { ctxt.type = 'html'; - let requestURL = new URL( - `${ctxt.protocol}://${ctxt.host}${ctxt.originalUrl}`, - ); let cardURL = requestURL; let isIndexRequest = requestURL.pathname.endsWith('/'); if (isIndexRequest) { @@ -408,16 +432,33 @@ export class RealmServer { ctxt.body = responseHTML; return; - } - return next(); + 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 isPublishedRealmRequest(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 95a9529162..96ee7f2480 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'; @@ -330,4 +331,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', + ); + }); + }); }); From a092c0f28ed546b82a85814a7ce6f5a147cfe098 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 30 Jan 2026 17:11:10 -0600 Subject: [PATCH 2/7] Change function name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More descriptive…? --- packages/realm-server/server.ts | 218 ++++++++++++++++---------------- 1 file changed, 109 insertions(+), 109 deletions(-) diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index 89a73b22a8..e5cedf92ce 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -289,9 +289,9 @@ export class RealmServer { if (includesHtmlMimeType) { if (includesVndMimeType) { - let isPublishedRealmRequest = - await this.isPublishedRealmRequest(requestURL); - if (isPublishedRealmRequest) { + let isHostModeRequest = await this.isHostModeRequest(requestURL); + + if (isHostModeRequest) { return next(); } } @@ -299,139 +299,139 @@ export class RealmServer { if (includesVndMimeType) { return next(); } - let isPublishedRealmRequest = - await this.isPublishedRealmRequest(requestURL); - if (!isPublishedRealmRequest) { + + let isHostModeRequest = await this.isHostModeRequest(requestURL); + + if (!isHostModeRequest) { return next(); } } - // If this is a /connect iframe request, is the origin a valid published realm? - - let connectMatch = ctxt.request.path.match(/\/connect\/(.+)$/); + // If this is a /connect iframe request, is the origin a valid published realm? - 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}%`), - ]); + let connectMatch = ctxt.request.path.match(/\/connect\/(.+)$/); - if (publishedRealms.length === 0) { - ctxt.status = 404; - ctxt.body = `Not Found: No published realm found for origin ${originParameter}`; + if (connectMatch) { + try { + let originParameter = new URL(decodeURIComponent(connectMatch[1])).href; - this.log.debug( - `Ignoring /connect request for origin ${originParameter}: no matching published realm`, - ); + let publishedRealms = await query(this.dbAdapter, [ + `SELECT published_realm_url FROM published_realms WHERE published_realm_url LIKE `, + param(`${originParameter}%`), + ]); - return; - } + if (publishedRealms.length === 0) { + ctxt.status = 404; + ctxt.body = `Not Found: No published realm found for origin ${originParameter}`; - ctxt.set( - 'Content-Security-Policy', - `frame-ancestors ${originParameter}`, + this.log.debug( + `Ignoring /connect request for origin ${originParameter}: no matching published realm`, ); - } catch (error) { - ctxt.status = 400; - ctxt.body = 'Bad Request'; - - this.log.info(`Error processing /connect request: ${error}`); return; } - } - ctxt.type = 'html'; - - 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); + } + + 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; }; @@ -443,7 +443,7 @@ export class RealmServer { }); } - private async isPublishedRealmRequest(requestURL: URL): Promise { + private async isHostModeRequest(requestURL: URL): Promise { let realm = this.findRealmForRequestURL(requestURL); if (!realm) { return false; From 8175550e03b39dbcde86d91feddbde03fb7c802a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 2 Feb 2026 15:56:07 -0600 Subject: [PATCH 3/7] Add some lint fixes --- packages/realm-server/server.ts | 1 - .../tests/server-endpoints/index-responses-test.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index 439f8e143a..51d0548530 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -432,7 +432,6 @@ export class RealmServer { ctxt.body = responseHTML; return; - return; }; private findRealmForRequestURL(requestURL: URL): Realm | undefined { 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 0d117f1ef2..7450beb6a9 100644 --- a/packages/realm-server/tests/server-endpoints/index-responses-test.ts +++ b/packages/realm-server/tests/server-endpoints/index-responses-test.ts @@ -358,9 +358,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let realmURL = new URL('http://127.0.0.1:4444/'); let request: SuperTest; - function onRealmSetup(args: { - request: SuperTest; - }) { + function onRealmSetup(args: { request: SuperTest }) { request = args.request; } From f9a44d55499fdfd59abcf2bf1c37fefb55f75b58 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 2 Feb 2026 17:12:47 -0600 Subject: [PATCH 4/7] Add exception for assets --- packages/realm-server/server.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index 51d0548530..496f1cabc3 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -21,6 +21,7 @@ import { fetchSessionRoom, REALM_SERVER_REALM, userInitiatedPriority, + hasExtension, } from '@cardstack/runtime-common'; import { ensureDirSync, @@ -300,6 +301,10 @@ export class RealmServer { return next(); } + if (hasExtension(requestURL.pathname)) { + return next(); + } + let isHostModeRequest = await this.isHostModeRequest(requestURL); if (!isHostModeRequest) { From aa0cf4fac9024ec5aad4c8ef3b37b2c3d2f48d12 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 2 Feb 2026 17:12:54 -0600 Subject: [PATCH 5/7] Add await --- packages/realm-server/tests/helpers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/realm-server/tests/helpers/index.ts b/packages/realm-server/tests/helpers/index.ts index a5efc5dba8..16b8903826 100644 --- a/packages/realm-server/tests/helpers/index.ts +++ b/packages/realm-server/tests/helpers/index.ts @@ -1192,7 +1192,7 @@ export function setupPermissionedRealm( publishedRealmId, ); - dbAdapter.execute( + await dbAdapter.execute( `INSERT INTO published_realms (id, owner_username, source_realm_url, published_realm_url) From d7e03ba3593ea3ee3fb391246c97a45d6de4a0b8 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 2 Feb 2026 18:29:19 -0600 Subject: [PATCH 6/7] Fix module loading for published realms by checking index for card instances For published realms with generic Accept headers (like */*), distinguish card URLs from module URLs. Module imports (e.g., "./person") resolve to URLs without extensions and would incorrectly get HTML served instead of module content, causing FilterRefersToNonexistentTypeError. Now only serve HTML if: 1. This is a directory index request (path ends with /), OR 2. The URL corresponds to an indexed card instance Co-Authored-By: Claude Opus 4.5 --- packages/realm-server/server.ts | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index 496f1cabc3..f453b5bd06 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -310,6 +310,21 @@ export class RealmServer { if (!isHostModeRequest) { return next(); } + + // For published realms with generic Accept headers (like */*), we need to + // distinguish card URLs from module URLs. Module imports (e.g., "./person") + // resolve to URLs without extensions and would incorrectly get HTML served. + // Only serve HTML if: + // 1. This is a directory index request (path ends with /), OR + // 2. The URL corresponds to an indexed card instance + let isIndexRequest = requestURL.pathname.endsWith('/'); + if (!isIndexRequest) { + let cardURL = requestURL; + let isCardInstance = await this.isIndexedCardInstance(cardURL); + if (!isCardInstance) { + return next(); + } + } } // If this is a /connect iframe request, is the origin a valid published realm? @@ -461,6 +476,32 @@ export class RealmServer { return rows.length > 0; } + // Check if the URL corresponds to an indexed card instance. + // This is used to distinguish card URLs from module URLs when deciding + // whether to serve HTML for published realms. + private async isIndexedCardInstance(cardURL: URL): Promise { + let candidates = this.indexURLCandidates(cardURL); + if (candidates.length === 0) { + return false; + } + + let rows = await query(this.dbAdapter, [ + ` + SELECT 1 + FROM boxel_index + WHERE type = 'instance' + AND is_deleted IS NOT TRUE + AND + `, + ...this.indexCandidateExpressions(candidates), + ` + LIMIT 1 + `, + ]); + + return rows.length > 0; + } + private async hasPublicPermissions(cardURL: URL): Promise { let realm = this.findRealmForRequestURL(cardURL); From 406a9f8928693225ae815c26a60382ae4ec9a10c Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 3 Feb 2026 11:40:28 -0600 Subject: [PATCH 7/7] Add more fallbacks --- packages/realm-server/server.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index f453b5bd06..78088f1f1e 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -479,12 +479,41 @@ export class RealmServer { // Check if the URL corresponds to an indexed card instance. // This is used to distinguish card URLs from module URLs when deciding // whether to serve HTML for published realms. + // + // IMPORTANT: Card instances have their file_alias set to the URL without + // the .json extension. This means an instance at /foo/bar.json has + // file_alias /foo/bar. When a module request comes in for /foo/bar (no + // extension), we must check if it's actually a module before assuming it's + // an instance. Modules take precedence over instance aliases. private async isIndexedCardInstance(cardURL: URL): Promise { let candidates = this.indexURLCandidates(cardURL); if (candidates.length === 0) { return false; } + // First check if there's a module at this URL - modules take precedence + // over instance aliases. This handles the case where: + // - Module: /foo/bar.gts (file_alias: /foo/bar) + // - Instance: /foo/bar.json (file_alias: /foo/bar) + // A request for /foo/bar should serve the module, not HTML for the instance. + let moduleRows = await query(this.dbAdapter, [ + ` + SELECT 1 + FROM boxel_index + WHERE type = 'module' + AND is_deleted IS NOT TRUE + AND + `, + ...this.indexCandidateExpressions(candidates), + ` + LIMIT 1 + `, + ]); + + if (moduleRows.length > 0) { + return false; + } + let rows = await query(this.dbAdapter, [ ` SELECT 1