From d0133546cfcdb3346fdc09b2a2c801e33220137f Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Mon, 2 Feb 2026 18:18:03 -0500 Subject: [PATCH 01/19] Add ImageDef/PngDef with format components and PNG metadata extraction Introduce ImageDef (extends FileDef) with isolated, embedded, atom, and fitted format components, and PngDef (extends ImageDef) with PNG dimension extraction. The atom format shows a small thumbnail with the file name, embedded renders the image at container width with natural height, and fitted uses a CSS background-image to cover available space. Includes a PNG metadata extractor and acceptance tests. Co-Authored-By: Claude Opus 4.5 --- packages/base/image-file-def.gts | 207 +++++++++++ packages/base/png-image-def.gts | 30 ++ packages/base/png-meta-extractor.ts | 42 +++ .../tests/acceptance/png-image-def-test.gts | 323 ++++++++++++++++++ packages/host/tests/helpers/adapter.ts | 6 +- packages/host/tests/helpers/index.gts | 3 +- packages/runtime-common/index-runner.ts | 4 + 7 files changed, 612 insertions(+), 3 deletions(-) create mode 100644 packages/base/image-file-def.gts create mode 100644 packages/base/png-image-def.gts create mode 100644 packages/base/png-meta-extractor.ts create mode 100644 packages/host/tests/acceptance/png-image-def-test.gts diff --git a/packages/base/image-file-def.gts b/packages/base/image-file-def.gts new file mode 100644 index 0000000000..e061ad7747 --- /dev/null +++ b/packages/base/image-file-def.gts @@ -0,0 +1,207 @@ +import NumberField from './number'; +import { BaseDefComponent, Component, contains, field } from './card-api'; +import { FileDef } from './file-api'; + +class Isolated extends Component { + +} + +class Atom extends Component { + +} + +class Embedded extends Component { + +} + +class Fitted extends Component { + get backgroundImageStyle() { + if (this.args.model.url) { + return `background-image: url(${this.args.model.url});`; + } + return undefined; + } + + +} + +export class ImageDef extends FileDef { + static displayName = 'Image'; + + @field width = contains(NumberField); + @field height = contains(NumberField); + + static isolated: BaseDefComponent = Isolated; + static embedded: BaseDefComponent = Embedded; + static atom: BaseDefComponent = Atom; + static fitted: BaseDefComponent = Fitted; +} diff --git a/packages/base/png-image-def.gts b/packages/base/png-image-def.gts new file mode 100644 index 0000000000..4801be96c8 --- /dev/null +++ b/packages/base/png-image-def.gts @@ -0,0 +1,30 @@ +import { byteStreamToUint8Array } from '@cardstack/runtime-common'; +import { ImageDef } from './image-file-def'; +import { type ByteStream, type SerializedFile } from './file-api'; +import { extractPngDimensions } from './png-meta-extractor'; + +export class PngDef extends ImageDef { + static displayName = 'PNG Image'; + + static async extractAttributes( + url: string, + getStream: () => Promise, + options: { contentHash?: string } = {}, + ): Promise> { + let bytesPromise: Promise | undefined; + let memoizedStream = async () => { + bytesPromise ??= byteStreamToUint8Array(await getStream()); + return bytesPromise; + }; + + let base = await super.extractAttributes(url, memoizedStream, options); + let bytes = await memoizedStream(); + let { width, height } = extractPngDimensions(bytes); + + return { + ...base, + width, + height, + }; + } +} diff --git a/packages/base/png-meta-extractor.ts b/packages/base/png-meta-extractor.ts new file mode 100644 index 0000000000..08d476eac3 --- /dev/null +++ b/packages/base/png-meta-extractor.ts @@ -0,0 +1,42 @@ +import { FileContentMismatchError } from './file-api'; + +// PNG 8-byte magic signature +const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); + +// Minimum bytes needed: 8 (signature) + 8 (IHDR chunk header) + 8 (width + height) +const MIN_BYTES = 24; + +function validatePngSignature(bytes: Uint8Array): void { + if (bytes.length < PNG_SIGNATURE.length) { + throw new FileContentMismatchError( + 'File is too small to be a valid PNG image', + ); + } + for (let i = 0; i < PNG_SIGNATURE.length; i++) { + if (bytes[i] !== PNG_SIGNATURE[i]) { + throw new FileContentMismatchError( + 'File does not have a valid PNG signature', + ); + } + } +} + +export function extractPngDimensions(bytes: Uint8Array): { + width: number; + height: number; +} { + validatePngSignature(bytes); + + if (bytes.length < MIN_BYTES) { + throw new FileContentMismatchError( + 'PNG file is too small to contain IHDR chunk', + ); + } + + // Width is at bytes 16-19, height at 20-23 (big-endian uint32) + let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + let width = view.getUint32(16); + let height = view.getUint32(20); + + return { width, height }; +} diff --git a/packages/host/tests/acceptance/png-image-def-test.gts b/packages/host/tests/acceptance/png-image-def-test.gts new file mode 100644 index 0000000000..ed78782805 --- /dev/null +++ b/packages/host/tests/acceptance/png-image-def-test.gts @@ -0,0 +1,323 @@ +import { visit, waitUntil } from '@ember/test-helpers'; + +import { getService } from '@universal-ember/test-support'; + +import { module, test } from 'qunit'; + +import { + baseRealm, + type FileExtractResponse, + type RenderRouteOptions, + type ResolvedCodeRef, + SupportedMimeType, +} from '@cardstack/runtime-common'; +import type { Realm } from '@cardstack/runtime-common/realm'; + +import type NetworkService from '@cardstack/host/services/network'; + +import { + setupLocalIndexing, + setupOnSave, + testRealmURL, + setupAcceptanceTestRealm, + SYSTEM_CARD_FIXTURE_CONTENTS, + capturePrerenderResult, +} from '../helpers'; +import { setupMockMatrix } from '../helpers/mock-matrix'; +import { setupApplicationTest } from '../helpers/setup'; + +// Minimal valid PNG: 1x1 pixel, RGBA, with IHDR, IDAT, and IEND chunks. +// Dimensions encoded in IHDR at bytes 16-19 (width=2) and 20-23 (height=3). +function makeMinimalPng(width: number, height: number): Uint8Array { + // PNG signature (8 bytes) + let signature = [137, 80, 78, 71, 13, 10, 26, 10]; + + // IHDR chunk: width, height, bit depth=8, color type=2 (RGB), compression=0, filter=0, interlace=0 + let ihdrData = new Uint8Array(13); + let ihdrView = new DataView(ihdrData.buffer); + ihdrView.setUint32(0, width); + ihdrView.setUint32(4, height); + ihdrData[8] = 8; // bit depth + ihdrData[9] = 2; // color type (RGB) + ihdrData[10] = 0; // compression + ihdrData[11] = 0; // filter + ihdrData[12] = 0; // interlace + + let ihdrChunk = buildChunk('IHDR', ihdrData); + + // Minimal IDAT chunk (empty compressed data — deflate stored block) + let idatData = new Uint8Array([0x08, 0xd7, 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x00, 0x01]); + let idatChunk = buildChunk('IDAT', idatData); + + // IEND chunk (empty) + let iendChunk = buildChunk('IEND', new Uint8Array(0)); + + // Combine all parts + let totalLength = + signature.length + + ihdrChunk.length + + idatChunk.length + + iendChunk.length; + let png = new Uint8Array(totalLength); + let offset = 0; + + png.set(signature, offset); + offset += signature.length; + png.set(ihdrChunk, offset); + offset += ihdrChunk.length; + png.set(idatChunk, offset); + offset += idatChunk.length; + png.set(iendChunk, offset); + + return png; +} + +function buildChunk(type: string, data: Uint8Array): Uint8Array { + // chunk = length (4 bytes) + type (4 bytes) + data + CRC (4 bytes) + let chunk = new Uint8Array(4 + 4 + data.length + 4); + let view = new DataView(chunk.buffer); + + // Length + view.setUint32(0, data.length); + + // Type + for (let i = 0; i < 4; i++) { + chunk[4 + i] = type.charCodeAt(i); + } + + // Data + chunk.set(data, 8); + + // CRC (simplified — not validated by our extractor, but needed for structure) + let crc = crc32(chunk.slice(4, 8 + data.length)); + view.setUint32(8 + data.length, crc); + + return chunk; +} + +function crc32(data: Uint8Array): number { + let crc = 0xffffffff; + for (let i = 0; i < data.length; i++) { + crc ^= data[i]!; + for (let j = 0; j < 8; j++) { + crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1; + } + } + return (crc ^ 0xffffffff) >>> 0; +} + +module('Acceptance | png image def', function (hooks) { + setupApplicationTest(hooks); + setupLocalIndexing(hooks); + setupOnSave(hooks); + + let mockMatrixUtils = setupMockMatrix(hooks, { + loggedInAs: '@testuser:localhost', + }); + let realm: Realm; + + const fileExtractPath = ( + url: string, + renderOptions: RenderRouteOptions, + nonce = 0, + ) => + `/render/${encodeURIComponent(url)}/${nonce}/${encodeURIComponent( + JSON.stringify(renderOptions), + )}/file-extract`; + + const fileRenderPath = ( + url: string, + renderOptions: RenderRouteOptions, + format = 'isolated', + ancestorLevel = 0, + nonce = 0, + ) => + `/render/${encodeURIComponent(url)}/${nonce}/${encodeURIComponent( + JSON.stringify(renderOptions), + )}/html/${format}/${ancestorLevel}`; + + const makeFileURL = (path: string) => new URL(path, testRealmURL).href; + + const pngDefCodeRef = (): ResolvedCodeRef => ({ + module: `${baseRealm.url}png-image-def`, + name: 'PngDef', + }); + + async function captureFileExtractResult( + expectedStatus?: 'ready' | 'error', + ): Promise { + await waitUntil( + () => { + let container = document.querySelector( + '[data-prerender-file-extract]', + ) as HTMLElement | null; + if (!container) { + return false; + } + let status = container.getAttribute( + 'data-prerender-file-extract-status', + ); + if (!status) { + return false; + } + if (expectedStatus && status !== expectedStatus) { + return false; + } + return status === 'ready' || status === 'error'; + }, + { timeout: 5000 }, + ); + + let container = document.querySelector( + '[data-prerender-file-extract]', + ) as HTMLElement | null; + if (!container) { + throw new Error( + 'captureFileExtractResult: missing [data-prerender-file-extract] container after wait', + ); + } + let pre = container.querySelector('pre'); + let text = pre?.textContent?.trim() ?? ''; + return JSON.parse(text) as FileExtractResponse; + } + + hooks.beforeEach(async function () { + let pngBytes = makeMinimalPng(2, 3); + ({ realm } = await setupAcceptanceTestRealm({ + mockMatrixUtils, + contents: { + ...SYSTEM_CARD_FIXTURE_CONTENTS, + 'sample.png': pngBytes, + 'not-a-png.png': 'This is plain text, not a PNG file.', + }, + })); + }); + + hooks.afterEach(function () { + delete (globalThis as any).__renderModel; + delete (globalThis as any).__boxelFileRenderData; + }); + + test('extracts width and height from PNG', async function (assert) { + let url = makeFileURL('sample.png'); + await visit( + fileExtractPath(url, { + fileExtract: true, + fileDefCodeRef: pngDefCodeRef(), + }), + ); + + let result = await captureFileExtractResult('ready'); + assert.strictEqual(result.status, 'ready'); + assert.strictEqual(result.searchDoc?.width, 2, 'extracts PNG width'); + assert.strictEqual(result.searchDoc?.height, 3, 'extracts PNG height'); + assert.strictEqual(result.searchDoc?.name, 'sample.png'); + assert.ok( + String(result.searchDoc?.contentType).includes('png'), + 'sets png content type', + ); + }); + + test('falls back when PngDef is used for non-PNG content', async function (assert) { + let url = makeFileURL('not-a-png.png'); + await visit( + fileExtractPath(url, { + fileExtract: true, + fileDefCodeRef: pngDefCodeRef(), + }), + ); + + let result = await captureFileExtractResult('ready'); + assert.strictEqual(result.status, 'ready'); + assert.true( + result.mismatch, + 'marks mismatch when content is not valid PNG', + ); + assert.strictEqual(result.searchDoc?.name, 'not-a-png.png'); + }); + + test('isolated template renders img with width and height attributes', async function (assert) { + let url = makeFileURL('sample.png'); + + // First extract the file to get the resource + await visit( + fileExtractPath(url, { + fileExtract: true, + fileDefCodeRef: pngDefCodeRef(), + }), + ); + let result = await captureFileExtractResult('ready'); + assert.ok(result.resource, 'extraction produced a resource'); + + // Set up file render data and visit the HTML render route + (globalThis as any).__boxelFileRenderData = { + resource: result.resource, + fileDefCodeRef: pngDefCodeRef(), + }; + + await visit( + fileRenderPath(url, { + fileRender: true, + fileDefCodeRef: pngDefCodeRef(), + }), + ); + + let { status } = await capturePrerenderResult('innerHTML'); + assert.strictEqual(status, 'ready', 'render completed'); + + let img = document.querySelector('[data-prerender] img') as HTMLImageElement | null; + assert.ok(img, 'img element is rendered'); + assert.strictEqual(img?.getAttribute('width'), '2', 'img has correct width attribute'); + assert.strictEqual(img?.getAttribute('height'), '3', 'img has correct height attribute'); + assert.ok( + img?.getAttribute('src')?.includes('sample.png'), + 'img src references the PNG file', + ); + }); + + test('indexing stores PNG metadata and file meta uses it', async function (assert) { + let fileURL = new URL('sample.png', testRealmURL); + let fileEntry = await realm.realmIndexQueryEngine.file(fileURL); + + assert.ok(fileEntry, 'file entry exists'); + assert.strictEqual( + fileEntry?.searchDoc?.width, + 2, + 'index stores PNG width', + ); + assert.strictEqual( + fileEntry?.searchDoc?.height, + 3, + 'index stores PNG height', + ); + + let network = getService('network') as NetworkService; + let response = await network.virtualNetwork.fetch(fileURL, { + headers: { Accept: SupportedMimeType.FileMeta }, + }); + + assert.true(response.ok, 'file meta request succeeds'); + + let body = await response.json(); + assert.strictEqual(body?.data?.type, 'file-meta'); + assert.ok( + String(body?.data?.attributes?.contentType).includes('png'), + 'file meta uses png content type', + ); + assert.strictEqual( + body?.data?.attributes?.width, + 2, + 'file meta includes PNG width', + ); + assert.strictEqual( + body?.data?.attributes?.height, + 3, + 'file meta includes PNG height', + ); + assert.deepEqual( + body?.data?.meta?.adoptsFrom, + pngDefCodeRef(), + 'file meta uses PNG def', + ); + }); +}); diff --git a/packages/host/tests/helpers/adapter.ts b/packages/host/tests/helpers/adapter.ts index 8d3fa14b57..44665aa0fc 100644 --- a/packages/host/tests/helpers/adapter.ts +++ b/packages/host/tests/helpers/adapter.ts @@ -57,7 +57,7 @@ class TokenExpiredError extends Error {} class JsonWebTokenError extends Error {} interface TestAdapterContents { - [path: string]: string | object; + [path: string]: string | object | Uint8Array; } let shimmedModuleIndicator = '// this file is shimmed'; @@ -255,7 +255,9 @@ export class TestRealmAdapter implements RealmAdapter { let fileRefContent: string | Uint8Array = ''; - if (path.endsWith('.json')) { + if (value instanceof Uint8Array) { + fileRefContent = value; + } else if (path.endsWith('.json')) { let cardApi = await this.#loader.import( `${baseRealm.url}card-api`, ); diff --git a/packages/host/tests/helpers/index.gts b/packages/host/tests/helpers/index.gts index a5a56aec04..7f0033551d 100644 --- a/packages/host/tests/helpers/index.gts +++ b/packages/host/tests/helpers/index.gts @@ -500,7 +500,8 @@ interface RealmContents { | LooseSingleCardDocument | RealmInfo | Record - | string; + | string + | Uint8Array; } export const SYSTEM_CARD_FIXTURE_CONTENTS: RealmContents = { diff --git a/packages/runtime-common/index-runner.ts b/packages/runtime-common/index-runner.ts index 8a28eae754..1058caa810 100644 --- a/packages/runtime-common/index-runner.ts +++ b/packages/runtime-common/index-runner.ts @@ -58,6 +58,10 @@ const FILEDEF_CODE_REF_BY_EXTENSION: Record = { module: `${baseRealm.url}markdown-file-def`, name: 'MarkdownDef', }, + '.png': { + module: `${baseRealm.url}png-image-def`, + name: 'PngDef', + }, '.mismatch': { module: './filedef-mismatch', name: 'FileDef' }, }; const BASE_FILE_DEF_CODE_REF: ResolvedCodeRef = { From 036c156e3d76c2c3a1ae7b9d1c43f1a0c739996d Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 3 Feb 2026 09:53:22 -0500 Subject: [PATCH 02/19] Fix prettier formatting in png-image-def-test Co-Authored-By: Claude Opus 4.5 --- .../tests/acceptance/png-image-def-test.gts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/host/tests/acceptance/png-image-def-test.gts b/packages/host/tests/acceptance/png-image-def-test.gts index ed78782805..2466c78b7f 100644 --- a/packages/host/tests/acceptance/png-image-def-test.gts +++ b/packages/host/tests/acceptance/png-image-def-test.gts @@ -46,7 +46,9 @@ function makeMinimalPng(width: number, height: number): Uint8Array { let ihdrChunk = buildChunk('IHDR', ihdrData); // Minimal IDAT chunk (empty compressed data — deflate stored block) - let idatData = new Uint8Array([0x08, 0xd7, 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x00, 0x01]); + let idatData = new Uint8Array([ + 0x08, 0xd7, 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x00, 0x01, + ]); let idatChunk = buildChunk('IDAT', idatData); // IEND chunk (empty) @@ -54,10 +56,7 @@ function makeMinimalPng(width: number, height: number): Uint8Array { // Combine all parts let totalLength = - signature.length + - ihdrChunk.length + - idatChunk.length + - iendChunk.length; + signature.length + ihdrChunk.length + idatChunk.length + iendChunk.length; let png = new Uint8Array(totalLength); let offset = 0; @@ -265,10 +264,20 @@ module('Acceptance | png image def', function (hooks) { let { status } = await capturePrerenderResult('innerHTML'); assert.strictEqual(status, 'ready', 'render completed'); - let img = document.querySelector('[data-prerender] img') as HTMLImageElement | null; + let img = document.querySelector( + '[data-prerender] img', + ) as HTMLImageElement | null; assert.ok(img, 'img element is rendered'); - assert.strictEqual(img?.getAttribute('width'), '2', 'img has correct width attribute'); - assert.strictEqual(img?.getAttribute('height'), '3', 'img has correct height attribute'); + assert.strictEqual( + img?.getAttribute('width'), + '2', + 'img has correct width attribute', + ); + assert.strictEqual( + img?.getAttribute('height'), + '3', + 'img has correct height attribute', + ); assert.ok( img?.getAttribute('src')?.includes('sample.png'), 'img src references the PNG file', From 24cf925976211d29c1b294af1c9bea21c05d0b20 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 3 Feb 2026 09:56:19 -0500 Subject: [PATCH 03/19] Remove duplicate Uint8Array check in test adapter The check at line 277 was unreachable since Uint8Array is already handled at line 258 in the if-else chain. Co-Authored-By: Claude Opus 4.5 --- packages/host/tests/helpers/adapter.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/host/tests/helpers/adapter.ts b/packages/host/tests/helpers/adapter.ts index 44665aa0fc..303738359d 100644 --- a/packages/host/tests/helpers/adapter.ts +++ b/packages/host/tests/helpers/adapter.ts @@ -274,8 +274,6 @@ export class TestRealmAdapter implements RealmAdapter { } else { fileRefContent = shimmedModuleIndicator; } - } else if (value instanceof Uint8Array) { - fileRefContent = value; } else { fileRefContent = value as string; } From 79974655456192e2c187e003771874cf9d6a5334 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 3 Feb 2026 11:42:25 -0500 Subject: [PATCH 04/19] Fix tests to use valid PNG bytes for PngDef extraction Tests were using string content for .png files which caused the PNG extractor to fail and fall back to ImageDef. Updated tests to use actual PNG binary data so PngDef extraction succeeds. - Add makeMinimalPng helper to test helpers - Update card-basics-test to use valid PNG bytes - Update card-endpoints-test to use valid PNG bytes and expect PngDef Co-Authored-By: Claude Opus 4.5 --- packages/host/tests/helpers/index.gts | 56 +++++++++++++++++ .../components/card-basics-test.gts | 7 ++- .../realm-server/tests/card-endpoints-test.ts | 61 +++++++++++++++++-- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/packages/host/tests/helpers/index.gts b/packages/host/tests/helpers/index.gts index 7f0033551d..7555bfc21b 100644 --- a/packages/host/tests/helpers/index.gts +++ b/packages/host/tests/helpers/index.gts @@ -998,6 +998,62 @@ export function delay(delayAmountMs: number): Promise { }); } +// Create minimal valid PNG bytes for testing (1x1 pixel by default) +export function makeMinimalPng(width = 1, height = 1): Uint8Array { + let signature = [137, 80, 78, 71, 13, 10, 26, 10]; + let ihdrData = new Uint8Array(13); + let ihdrView = new DataView(ihdrData.buffer); + ihdrView.setUint32(0, width); + ihdrView.setUint32(4, height); + ihdrData[8] = 8; // bit depth + ihdrData[9] = 2; // color type (RGB) + ihdrData[10] = 0; // compression + ihdrData[11] = 0; // filter + ihdrData[12] = 0; // interlace + let ihdrChunk = buildPngChunk('IHDR', ihdrData); + let idatData = new Uint8Array([ + 0x08, 0xd7, 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x00, 0x01, + ]); + let idatChunk = buildPngChunk('IDAT', idatData); + let iendChunk = buildPngChunk('IEND', new Uint8Array(0)); + let totalLength = + signature.length + ihdrChunk.length + idatChunk.length + iendChunk.length; + let png = new Uint8Array(totalLength); + let offset = 0; + png.set(signature, offset); + offset += signature.length; + png.set(ihdrChunk, offset); + offset += ihdrChunk.length; + png.set(idatChunk, offset); + offset += idatChunk.length; + png.set(iendChunk, offset); + return png; +} + +function buildPngChunk(type: string, data: Uint8Array): Uint8Array { + let chunk = new Uint8Array(4 + 4 + data.length + 4); + let view = new DataView(chunk.buffer); + view.setUint32(0, data.length); + for (let i = 0; i < 4; i++) { + chunk[4 + i] = type.charCodeAt(i); + } + chunk.set(data, 8); + let crc = crc32Png(chunk.slice(4, 8 + data.length)); + view.setUint32(8 + data.length, crc); + return chunk; +} + +function crc32Png(data: Uint8Array): number { + let crc = 0xffffffff; + for (let i = 0; i < data.length; i++) { + crc ^= data[i]!; + for (let j = 0; j < 8; j++) { + crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1; + } + } + return (crc ^ 0xffffffff) >>> 0; +} + // --- Created-at test utilities --- // Returns created_at (epoch seconds) from realm_file_meta for a given local file path like 'Pet/mango.json'. export async function getFileCreatedAt( diff --git a/packages/host/tests/integration/components/card-basics-test.gts b/packages/host/tests/integration/components/card-basics-test.gts index a564fe84e8..04a0347042 100644 --- a/packages/host/tests/integration/components/card-basics-test.gts +++ b/packages/host/tests/integration/components/card-basics-test.gts @@ -51,6 +51,7 @@ import { testModuleRealm, setupIntegrationTestRealm, setupLocalIndexing, + makeMinimalPng, } from '../../helpers'; import { Base64ImageField, @@ -715,7 +716,7 @@ module('Integration | card-basics', function (hooks) { mockMatrixUtils, contents: { 'test-cards.gts': { Gallery }, - 'hero.png': 'mock image bytes', + 'hero.png': makeMinimalPng(), 'Gallery/hero.json': { data: { type: 'card', @@ -781,8 +782,8 @@ module('Integration | card-basics', function (hooks) { mockMatrixUtils, contents: { 'test-cards.gts': { Gallery }, - 'first.png': 'first mock image', - 'second.png': 'second mock image', + 'first.png': makeMinimalPng(), + 'second.png': makeMinimalPng(), 'Gallery/attachments.json': { data: { type: 'card', diff --git a/packages/realm-server/tests/card-endpoints-test.ts b/packages/realm-server/tests/card-endpoints-test.ts index 169320be2d..943e377e4f 100644 --- a/packages/realm-server/tests/card-endpoints-test.ts +++ b/packages/realm-server/tests/card-endpoints-test.ts @@ -40,6 +40,55 @@ function parseSearchQuery(searchURL: URL) { return parse(searchURL.searchParams.toString()) as Record; } +// Create minimal valid PNG bytes for testing +function makeMinimalPng(): Uint8Array { + let signature = [137, 80, 78, 71, 13, 10, 26, 10]; + let ihdrData = new Uint8Array(13); + let ihdrView = new DataView(ihdrData.buffer); + ihdrView.setUint32(0, 1); // width + ihdrView.setUint32(4, 1); // height + ihdrData[8] = 8; // bit depth + ihdrData[9] = 2; // color type (RGB) + let ihdrChunk = buildPngChunk('IHDR', ihdrData); + let idatData = new Uint8Array([ + 0x08, 0xd7, 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x00, 0x01, + ]); + let idatChunk = buildPngChunk('IDAT', idatData); + let iendChunk = buildPngChunk('IEND', new Uint8Array(0)); + let totalLength = + signature.length + ihdrChunk.length + idatChunk.length + iendChunk.length; + let png = new Uint8Array(totalLength); + let offset = 0; + png.set(signature, offset); + offset += signature.length; + png.set(ihdrChunk, offset); + offset += ihdrChunk.length; + png.set(idatChunk, offset); + offset += idatChunk.length; + png.set(iendChunk, offset); + return png; +} + +function buildPngChunk(type: string, data: Uint8Array): Uint8Array { + let chunk = new Uint8Array(4 + 4 + data.length + 4); + let view = new DataView(chunk.buffer); + view.setUint32(0, data.length); + for (let i = 0; i < 4; i++) { + chunk[4 + i] = type.charCodeAt(i); + } + chunk.set(data, 8); + let crc = 0xffffffff; + let crcData = chunk.slice(4, 8 + data.length); + for (let i = 0; i < crcData.length; i++) { + crc ^= crcData[i]!; + for (let j = 0; j < 8; j++) { + crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1; + } + } + view.setUint32(8 + data.length, (crc ^ 0xffffffff) >>> 0); + return chunk; +} + module(basename(__filename), function () { module('Realm-specific Endpoints | card URLs', function (hooks) { let realmURL = new URL('http://127.0.0.1:4444/test/'); @@ -193,7 +242,7 @@ module(basename(__filename), function () { test('includes FileDef resources for file links in included payload', async function (assert) { let { testRealm: realm, request } = getRealmSetup(); - let writes = new Map([ + let writes = new Map([ [ 'gallery.gts', ` @@ -237,9 +286,9 @@ module(basename(__filename), function () { }, }), ], - ['hero.png', 'mock hero image'], - ['first.png', 'mock first image'], - ['second.png', 'mock second image'], + ['hero.png', makeMinimalPng()], + ['first.png', makeMinimalPng()], + ['second.png', makeMinimalPng()], ]); await realm.writeMany(writes); @@ -275,8 +324,8 @@ module(basename(__filename), function () { assert.strictEqual(hero?.attributes?.name, 'hero.png'); assert.strictEqual(hero?.attributes?.contentType, 'image/png'); assert.deepEqual(hero?.meta?.adoptsFrom, { - module: `${baseRealm.url}file-api`, - name: 'FileDef', + module: `${baseRealm.url}png-image-def`, + name: 'PngDef', }); assert.deepEqual( From bddde4c869d65f67966ff0606a30fb45bf536cf1 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Mon, 2 Feb 2026 22:34:42 -0500 Subject: [PATCH 05/19] Fall back to client-side search when server-side cross-realm query has errors When the server fails a cross-realm query (e.g. remote realm unreachable), it returns error metadata in the relationship. Previously the client treated the empty seed data as authoritative and showed no results. Now the client detects queryErrors in the seed, skips caching the previousQueryString, and triggers a live client-side search as a fallback. Co-Authored-By: Claude Opus 4.5 --- packages/base/card-api.gts | 6 + packages/base/query-field-support.ts | 9 + packages/host/app/resources/search.ts | 15 +- packages/host/app/services/store.ts | 6 + .../tests/acceptance/query-fields-test.gts | 350 +++++++++++++++++- 5 files changed, 384 insertions(+), 2 deletions(-) diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index cd6e05753c..05f1dba129 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -382,6 +382,12 @@ export type GetSearchResourceFuncOpts = { cards: CardDef[]; searchURL?: string; realms?: string[]; + queryErrors?: Array<{ + realm: string; + type: string; + message: string; + status?: number; + }>; }; }; export type GetSearchResourceFunc = ( diff --git a/packages/base/query-field-support.ts b/packages/base/query-field-support.ts index b96998c927..7db2f9e95a 100644 --- a/packages/base/query-field-support.ts +++ b/packages/base/query-field-support.ts @@ -31,6 +31,12 @@ interface QueryFieldState { seedSearchURL?: string | null; seedRecords?: CardDef[]; seedRealms?: string[]; + seedErrors?: Array<{ + realm: string; + type: string; + message: string; + status?: number; + }>; searchResource?: StoreSearchResource; } @@ -87,6 +93,7 @@ export function ensureQueryFieldSearchResource( cards: seedRecords, searchURL: seedSearchURL ?? undefined, realms: fieldState?.seedRealms, + queryErrors: fieldState?.seedErrors, } : undefined, }, @@ -261,6 +268,8 @@ export function captureQueryFieldSeedData( fieldState.seedRealms = fieldState.seedSearchURL ? [parseSearchURL(new URL(fieldState.seedSearchURL)).realm.href] : []; + fieldState.seedErrors = + (relationship?.meta as any)?.errors ?? undefined; } function resolveQueryAndRealm( diff --git a/packages/host/app/resources/search.ts b/packages/host/app/resources/search.ts index 1d640c250b..81417c575f 100644 --- a/packages/host/app/resources/search.ts +++ b/packages/host/app/resources/search.ts @@ -53,6 +53,12 @@ export interface Args { realms?: string[]; meta?: QueryResultsMeta; errors?: ErrorEntry[]; + queryErrors?: Array<{ + realm: string; + type: string; + message: string; + status?: number; + }>; } | undefined; owner: Owner; @@ -117,7 +123,8 @@ export class SearchResource< if (seed && !this.#seedApplied) { this.loaded = this.applySeed.perform(seed); this.#seedApplied = true; - if (seed.searchURL) { + let hasQueryErrors = seed.queryErrors && seed.queryErrors.length > 0; + if (seed.searchURL && !hasQueryErrors) { let { query: seedQuery } = parseSearchURL(seed.searchURL); this.#previousQueryString = buildQueryParamValue( normalizeQueryForSignature(seedQuery), @@ -376,6 +383,12 @@ export function getSearch( searchURL?: string; meta?: QueryResultsMeta; errors?: ErrorEntry[]; + queryErrors?: Array<{ + realm: string; + type: string; + message: string; + status?: number; + }>; } | undefined; }, diff --git a/packages/host/app/services/store.ts b/packages/host/app/services/store.ts index d8241b2324..983c180121 100644 --- a/packages/host/app/services/store.ts +++ b/packages/host/app/services/store.ts @@ -659,6 +659,12 @@ export default class StoreService extends Service implements StoreInterface { searchURL?: string; meta?: QueryResultsMeta; errors?: ErrorEntry[]; + queryErrors?: Array<{ + realm: string; + type: string; + message: string; + status?: number; + }>; }; }, ): SearchResource { diff --git a/packages/host/tests/acceptance/query-fields-test.gts b/packages/host/tests/acceptance/query-fields-test.gts index 861b501027..7a8c5bd778 100644 --- a/packages/host/tests/acceptance/query-fields-test.gts +++ b/packages/host/tests/acceptance/query-fields-test.gts @@ -13,6 +13,7 @@ import { getService } from '@universal-ember/test-support'; import { module, test } from 'qunit'; import type { Loader } from '@cardstack/runtime-common'; +import { SupportedMimeType } from '@cardstack/runtime-common'; import { testRealmURLToUsername } from '@cardstack/runtime-common/helpers/const'; import { APP_BOXEL_REALM_EVENT_TYPE } from '@cardstack/runtime-common/matrix-constants'; @@ -47,6 +48,7 @@ const QUERY_CARD_URL = `${testRealmURL}query-card`; const QUERY_CARD_2_URL = `${testRealmURL}query-card-2`; const QUERY_CARD_NESTED_URL = `${testRealmURL}query-card-nested`; const QUERY_CARD_MISSING_URL = `${testRealmURL}query-card-missing`; +const TYPE_FILTER_CARD_URL = `${testRealmURL}type-filter-card`; module( 'Acceptance | Query Fields | host respects server-populated results', @@ -68,6 +70,9 @@ module( @field name = contains(StringField); } PersonClass = Person; + class Animal extends CardDef { + @field name = contains(StringField); + } class QueryLinksField extends FieldDef { @field cardTitle = contains(StringField); @field favorite = linksTo(() => Person, { @@ -149,13 +154,36 @@ module( }; } + // A card whose query has no predicate filter, only a page size. + // normalizeQueryDefinition turns this into { type: targetRef } — an + // explicit CardTypeFilter — instead of injecting `on` into leaf predicates. + class TypeFilterCard extends CardDef { + @field matches = linksToMany(() => Person, { + query: { + page: { size: 10, number: 0 }, + }, + }); + static isolated = class Isolated extends Component< + typeof TypeFilterCard + > { + + }; + } await setupAcceptanceTestRealm({ mockMatrixUtils, contents: { - 'query-card.gts': { Person, QueryCard }, + 'query-card.gts': { Person, QueryCard, Animal }, 'query-card-nested.gts': { Person, QueryCardNested, QueryLinksField }, + 'type-filter-card.gts': { TypeFilterCard, Person }, 'Person/target.json': new Person({ name: 'Target' }), 'Person/not-target.json': new Person({ name: 'Not Target' }), + 'Animal/target.json': new Animal({ name: 'Target' }), 'query-card.json': new QueryCard({ cardTitle: 'Target', }), @@ -170,6 +198,7 @@ module( 'query-card-missing.json': new QueryCard({ cardTitle: 'Missing', }), + 'type-filter-card.json': new TypeFilterCard(), }, }); loader = getService('loader-service').loader; @@ -564,5 +593,324 @@ module( network.virtualNetwork.unmount(handler); } }); + + test('client falls back to live search when server-side cross-realm query has errors', async function (assert) { + let network = getService('network') as NetworkService; + let interceptedSearchRequests: string[] = []; + + // Pre-fetch the card JSON before mounting the handler so we can + // modify it without re-fetching from inside the handler (which + // can interfere with module loading through the virtual network). + let prefetchResponse = await network.virtualNetwork.fetch( + new Request(QUERY_CARD_URL, { + headers: { Accept: SupportedMimeType.CardJson }, + }), + ); + let cardJson = await prefetchResponse.json(); + + // Inject error metadata into the matches relationship, + // simulating a failed cross-realm query + let fakeRemoteRealm = 'https://unreachable-realm.example.com/'; + let matchesRel = cardJson.data.relationships?.matches; + if (matchesRel) { + // Clear any results that were populated — simulating server couldn't get them + matchesRel.data = []; + matchesRel.meta = { + errors: [ + { + realm: fakeRemoteRealm, + type: 'fetch-error', + message: 'Could not reach remote realm', + status: 502, + }, + ], + }; + // Remove indexed per-item relationships (matches.0, matches.1, ...) + for (let key of Object.keys(cardJson.data.relationships)) { + if (key.startsWith('matches.')) { + delete cardJson.data.relationships[key]; + } + } + } + + let modifiedBody = JSON.stringify(cardJson); + + let handler = async (request: Request) => { + let url = new URL(request.url); + + // Track client-side search requests + if (url.pathname.endsWith('/_search')) { + interceptedSearchRequests.push(request.url); + } + + // Return the pre-modified card JSON for the card GET request. + if ( + request.method === 'GET' && + request.url === QUERY_CARD_URL && + request.headers.get('Accept')?.includes('card+json') + ) { + return new Response(modifiedBody, { + status: 200, + headers: new Headers({ + 'content-type': SupportedMimeType.CardJson, + }), + }); + } + + return null; + }; + + network.virtualNetwork.mount(handler, { prepend: true }); + try { + await visitOperatorMode({ + stacks: [[{ id: QUERY_CARD_URL, format: 'isolated' }]], + }); + await settled(); + + let cardSelector = `[data-test-stack-card="${QUERY_CARD_URL}"]`; + assert.dom(cardSelector).exists('query card is rendered'); + await waitFor(`${cardSelector} [data-test-matches]`); + + assert.ok( + interceptedSearchRequests.length > 0, + 'client-side _search request was triggered as a fallback for the errored query field', + ); + + let matchElements = findAll(`${cardSelector} [data-test-match]`); + assert.deepEqual( + matchElements.map((el) => el.textContent?.trim()), + ['Target'], + 'linksToMany query field was populated via client-side fallback search', + ); + } finally { + network.virtualNetwork.unmount(handler); + } + }); + + test('fallback search preserves the type filter from the query definition', async function (assert) { + // This test verifies that when the client falls back to a live search + // due to server-side cross-realm query errors, the search correctly + // includes the type constraint. Both Person/target and Animal/target + // have name "Target", but the query field is typed as linksToMany(Person), + // so only Person cards should appear in the results. + let network = getService('network') as NetworkService; + let interceptedSearchRequests: string[] = []; + + let prefetchResponse = await network.virtualNetwork.fetch( + new Request(QUERY_CARD_URL, { + headers: { Accept: SupportedMimeType.CardJson }, + }), + ); + let cardJson = await prefetchResponse.json(); + + let fakeRemoteRealm = 'https://unreachable-realm.example.com/'; + let matchesRel = cardJson.data.relationships?.matches; + if (matchesRel) { + matchesRel.data = []; + matchesRel.meta = { + errors: [ + { + realm: fakeRemoteRealm, + type: 'fetch-error', + message: 'Could not reach remote realm', + status: 502, + }, + ], + }; + for (let key of Object.keys(cardJson.data.relationships)) { + if (key.startsWith('matches.')) { + delete cardJson.data.relationships[key]; + } + } + } + + let modifiedBody = JSON.stringify(cardJson); + + let handler = async (request: Request) => { + let url = new URL(request.url); + + if (url.pathname.endsWith('/_search')) { + interceptedSearchRequests.push(request.url); + } + + if ( + request.method === 'GET' && + request.url === QUERY_CARD_URL && + request.headers.get('Accept')?.includes('card+json') + ) { + return new Response(modifiedBody, { + status: 200, + headers: new Headers({ + 'content-type': SupportedMimeType.CardJson, + }), + }); + } + + return null; + }; + + network.virtualNetwork.mount(handler, { prepend: true }); + try { + await visitOperatorMode({ + stacks: [[{ id: QUERY_CARD_URL, format: 'isolated' }]], + }); + await settled(); + + let cardSelector = `[data-test-stack-card="${QUERY_CARD_URL}"]`; + assert.dom(cardSelector).exists('query card is rendered'); + await waitFor(`${cardSelector} [data-test-matches]`); + + assert.ok( + interceptedSearchRequests.length > 0, + 'client-side _search request was triggered as a fallback', + ); + + let matchElements = findAll(`${cardSelector} [data-test-match]`); + assert.deepEqual( + matchElements.map((el) => el.textContent?.trim()), + ['Target'], + 'only Person cards appear — Animal/target with same name is excluded by the type filter', + ); + } finally { + network.virtualNetwork.unmount(handler); + } + }); + + test('explicit CardTypeFilter hydrates from server without re-fetch', async function (assert) { + // TypeFilterCard has a linksToMany query with no filter predicates, + // so normalizeQueryDefinition produces { type: targetRef } — a pure + // CardTypeFilter. This is the happy-path: server-populated results + // are used directly with no client-side search needed. + let network = getService('network') as NetworkService; + let interceptedSearchRequests: string[] = []; + let handler = async (request: Request) => { + let url = new URL(request.url); + if (url.pathname.endsWith('/_search')) { + interceptedSearchRequests.push(request.url); + } + return null; + }; + + network.virtualNetwork.mount(handler, { prepend: true }); + try { + await visitOperatorMode({ + stacks: [[{ id: TYPE_FILTER_CARD_URL, format: 'isolated' }]], + }); + await settled(); + + let cardSelector = `[data-test-stack-card="${TYPE_FILTER_CARD_URL}"]`; + assert.dom(cardSelector).exists('type-filter card is rendered'); + await waitFor(`${cardSelector} [data-test-matches]`); + + assert.strictEqual( + interceptedSearchRequests.length, + 0, + 'no _search requests were triggered — server-populated results were used directly', + ); + + let matchElements = findAll(`${cardSelector} [data-test-match]`); + let matchNames = matchElements + .map((el) => el.textContent?.trim()) + .sort(); + assert.deepEqual( + matchNames, + ['Not Target', 'Target'], + 'all Person cards are hydrated from server response — Animal/target is excluded by CardTypeFilter', + ); + } finally { + network.virtualNetwork.unmount(handler); + } + }); + + test('fallback search works with an explicit CardTypeFilter (no predicate)', async function (assert) { + // TypeFilterCard has a linksToMany query with no filter predicates, + // so normalizeQueryDefinition produces { type: targetRef } — a pure + // CardTypeFilter. This test verifies the fallback search preserves + // that filter and returns only Person cards (not Animal cards). + let network = getService('network') as NetworkService; + let interceptedSearchRequests: string[] = []; + + let prefetchResponse = await network.virtualNetwork.fetch( + new Request(TYPE_FILTER_CARD_URL, { + headers: { Accept: SupportedMimeType.CardJson }, + }), + ); + let cardJson = await prefetchResponse.json(); + + let fakeRemoteRealm = 'https://unreachable-realm.example.com/'; + let matchesRel = cardJson.data.relationships?.matches; + if (matchesRel) { + matchesRel.data = []; + matchesRel.meta = { + errors: [ + { + realm: fakeRemoteRealm, + type: 'fetch-error', + message: 'Could not reach remote realm', + status: 502, + }, + ], + }; + for (let key of Object.keys(cardJson.data.relationships)) { + if (key.startsWith('matches.')) { + delete cardJson.data.relationships[key]; + } + } + } + + let modifiedBody = JSON.stringify(cardJson); + + let handler = async (request: Request) => { + let url = new URL(request.url); + + if (url.pathname.endsWith('/_search')) { + interceptedSearchRequests.push(request.url); + } + + if ( + request.method === 'GET' && + request.url === TYPE_FILTER_CARD_URL && + request.headers.get('Accept')?.includes('card+json') + ) { + return new Response(modifiedBody, { + status: 200, + headers: new Headers({ + 'content-type': SupportedMimeType.CardJson, + }), + }); + } + + return null; + }; + + network.virtualNetwork.mount(handler, { prepend: true }); + try { + await visitOperatorMode({ + stacks: [[{ id: TYPE_FILTER_CARD_URL, format: 'isolated' }]], + }); + await settled(); + + let cardSelector = `[data-test-stack-card="${TYPE_FILTER_CARD_URL}"]`; + assert.dom(cardSelector).exists('type-filter card is rendered'); + await waitFor(`${cardSelector} [data-test-matches]`); + + assert.ok( + interceptedSearchRequests.length > 0, + 'client-side _search request was triggered as a fallback', + ); + + let matchElements = findAll(`${cardSelector} [data-test-match]`); + let matchNames = matchElements + .map((el) => el.textContent?.trim()) + .sort(); + assert.deepEqual( + matchNames, + ['Not Target', 'Target'], + 'all Person cards are returned by the type filter — Animal/target is excluded', + ); + } finally { + network.virtualNetwork.unmount(handler); + } + }); }, ); From b9ed547610f5c707390ea9908cb6ab6e6ffb527d Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 3 Feb 2026 12:16:19 -0500 Subject: [PATCH 06/19] Use .txt files in FileDef render tests to avoid PngDef behavior The FileDef render tests were using .png files, but with the new PngDef implementation, PNG files are indexed as PngDef/ImageDef which renders a background image instead of showing the filename as text. Changed to .txt files so the tests properly verify FileDef's text-based rendering. Co-Authored-By: Claude Opus 4.5 --- .../components/card-basics-test.gts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/host/tests/integration/components/card-basics-test.gts b/packages/host/tests/integration/components/card-basics-test.gts index 04a0347042..a5928c364b 100644 --- a/packages/host/tests/integration/components/card-basics-test.gts +++ b/packages/host/tests/integration/components/card-basics-test.gts @@ -51,7 +51,6 @@ import { testModuleRealm, setupIntegrationTestRealm, setupLocalIndexing, - makeMinimalPng, } from '../../helpers'; import { Base64ImageField, @@ -716,7 +715,7 @@ module('Integration | card-basics', function (hooks) { mockMatrixUtils, contents: { 'test-cards.gts': { Gallery }, - 'hero.png': makeMinimalPng(), + 'hero.txt': 'some file content', 'Gallery/hero.json': { data: { type: 'card', @@ -726,10 +725,10 @@ module('Integration | card-basics', function (hooks) { relationships: { hero: { links: { - self: `${testRealmURL}hero.png`, + self: `${testRealmURL}hero.txt`, }, data: { - id: `${testRealmURL}hero.png`, + id: `${testRealmURL}hero.txt`, type: 'file-meta', }, }, @@ -753,13 +752,13 @@ module('Integration | card-basics', function (hooks) { await waitUntil(() => document .querySelector('[data-test-gallery-fitted]') - ?.textContent?.includes('hero.png'), + ?.textContent?.includes('hero.txt'), ); assert .dom('[data-test-gallery-fitted]') .includesText( - 'hero.png', + 'hero.txt', 'FileDef renders delegated view from file meta', ); }); @@ -782,8 +781,8 @@ module('Integration | card-basics', function (hooks) { mockMatrixUtils, contents: { 'test-cards.gts': { Gallery }, - 'first.png': makeMinimalPng(), - 'second.png': makeMinimalPng(), + 'first.txt': 'first file content', + 'second.txt': 'second file content', 'Gallery/attachments.json': { data: { type: 'card', @@ -793,19 +792,19 @@ module('Integration | card-basics', function (hooks) { relationships: { 'attachments.0': { links: { - self: `${testRealmURL}first.png`, + self: `${testRealmURL}first.txt`, }, data: { - id: `${testRealmURL}first.png`, + id: `${testRealmURL}first.txt`, type: 'file-meta', }, }, 'attachments.1': { links: { - self: `${testRealmURL}second.png`, + self: `${testRealmURL}second.txt`, }, data: { - id: `${testRealmURL}second.png`, + id: `${testRealmURL}second.txt`, type: 'file-meta', }, }, @@ -831,7 +830,7 @@ module('Integration | card-basics', function (hooks) { document.querySelector( '[data-test-plural-view-field="attachments"]', )?.textContent ?? ''; - return text.includes('first.png') && text.includes('second.png'); + return text.includes('first.txt') && text.includes('second.txt'); }); assert @@ -840,13 +839,13 @@ module('Integration | card-basics', function (hooks) { assert .dom('[data-test-plural-view-field="attachments"]') .includesText( - 'first.png', + 'first.txt', 'FileDef renders delegated view from file meta', ); assert .dom('[data-test-plural-view-field="attachments"]') .includesText( - 'second.png', + 'second.txt', 'FileDef renders delegated view from file meta', ); }); From 64a5e57e3a8d4353bdbdfaea86ea7a9050a7395a Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 3 Feb 2026 13:56:01 -0500 Subject: [PATCH 07/19] Add test for authenticated image rendering in browser --- .../tests/acceptance/png-image-def-test.gts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/host/tests/acceptance/png-image-def-test.gts b/packages/host/tests/acceptance/png-image-def-test.gts index 2466c78b7f..bb592a9974 100644 --- a/packages/host/tests/acceptance/png-image-def-test.gts +++ b/packages/host/tests/acceptance/png-image-def-test.gts @@ -329,4 +329,62 @@ module('Acceptance | png image def', function (hooks) { 'file meta uses PNG def', ); }); + + test('authenticated images display in browser', async function (assert) { + let url = makeFileURL('sample.png'); + + // First extract the file to get the resource + await visit( + fileExtractPath(url, { + fileExtract: true, + fileDefCodeRef: pngDefCodeRef(), + }), + ); + let result = await captureFileExtractResult('ready'); + assert.ok(result.resource, 'extraction produced a resource'); + + // Set up file render data and visit the HTML render route + (globalThis as any).__boxelFileRenderData = { + resource: result.resource, + fileDefCodeRef: pngDefCodeRef(), + }; + + await visit( + fileRenderPath(url, { + fileRender: true, + fileDefCodeRef: pngDefCodeRef(), + }), + ); + + let { status } = await capturePrerenderResult('innerHTML'); + assert.strictEqual(status, 'ready', 'render completed'); + + let img = document.querySelector( + '[data-prerender] .image-isolated__img', + ) as HTMLImageElement | null; + assert.ok(img, 'img element is rendered'); + assert.ok( + img?.getAttribute('src')?.includes('sample.png'), + 'img src references the PNG file', + ); + + // Wait for the image to actually load and verify it has non-zero dimensions. + // This assertion will fail if the browser cannot fetch the image (e.g., 401 errors + // due to missing authentication cookies). Once cookie-based auth is implemented, + // this test should pass. + await waitUntil(() => img!.naturalWidth > 0, { + timeout: 5000, + timeoutMessage: + 'Image failed to load - naturalWidth remained 0. This likely indicates an authentication issue preventing the browser from fetching the image.', + }); + + assert.ok( + img!.naturalWidth > 0, + 'Image loaded successfully with non-zero width', + ); + assert.ok( + img!.naturalHeight > 0, + 'Image loaded successfully with non-zero height', + ); + }); }); From 00cf4dc60f2f7439d4fe9d8175e0fbf8141e573a Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 3 Feb 2026 13:56:13 -0500 Subject: [PATCH 08/19] Add PngDefPlayground implementation with image gallery and format comparison --- .../ae41b3ed-512e-4978-8925-1aad8a3d3d93.json | 53 ++++ .../PngDefPlayground/mango-demo.json | 35 +++ .../PngDefPlayground/mango.png | Bin 0 -> 114705 bytes .../experiments-realm/png-def-playground.gts | 249 ++++++++++++++++++ 4 files changed, 337 insertions(+) create mode 100644 packages/experiments-realm/PngDefPlayground/ae41b3ed-512e-4978-8925-1aad8a3d3d93.json create mode 100644 packages/experiments-realm/PngDefPlayground/mango-demo.json create mode 100644 packages/experiments-realm/PngDefPlayground/mango.png create mode 100644 packages/experiments-realm/png-def-playground.gts diff --git a/packages/experiments-realm/PngDefPlayground/ae41b3ed-512e-4978-8925-1aad8a3d3d93.json b/packages/experiments-realm/PngDefPlayground/ae41b3ed-512e-4978-8925-1aad8a3d3d93.json new file mode 100644 index 0000000000..e030c52e50 --- /dev/null +++ b/packages/experiments-realm/PngDefPlayground/ae41b3ed-512e-4978-8925-1aad8a3d3d93.json @@ -0,0 +1,53 @@ +{ + "data": { + "type": "card", + "attributes": { + "title": "Image Gallery Demo", + "description": "Showcasing ImageDef/PngDef with various images from the experiments realm" + }, + "relationships": { + "featuredImage": { + "links": { + "self": "./mango.png" + }, + "data": { + "type": "file-meta", + "id": "./mango.png" + } + }, + "gallery.0": { + "links": { + "self": "./mango.png" + }, + "data": { + "type": "file-meta", + "id": "./mango.png" + } + }, + "gallery.1": { + "links": { + "self": "../logo.png" + }, + "data": { + "type": "file-meta", + "id": "../logo.png" + } + }, + "gallery.2": { + "links": { + "self": "../FileLinksExample/mango.png" + }, + "data": { + "type": "file-meta", + "id": "../FileLinksExample/mango.png" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../png-def-playground", + "name": "PngDefPlayground" + } + } + } +} diff --git a/packages/experiments-realm/PngDefPlayground/mango-demo.json b/packages/experiments-realm/PngDefPlayground/mango-demo.json new file mode 100644 index 0000000000..e735492f7e --- /dev/null +++ b/packages/experiments-realm/PngDefPlayground/mango-demo.json @@ -0,0 +1,35 @@ +{ + "data": { + "type": "card", + "attributes": { + "title": "PNG Image Playground", + "description": "Demonstrating PngDef capabilities including automatic dimension extraction and various display formats" + }, + "relationships": { + "featuredImage": { + "links": { + "self": "./mango.png" + }, + "data": { + "type": "file-meta", + "id": "./mango.png" + } + }, + "gallery.0": { + "links": { + "self": "./mango.png" + }, + "data": { + "type": "file-meta", + "id": "./mango.png" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../png-def-playground", + "name": "PngDefPlayground" + } + } + } +} diff --git a/packages/experiments-realm/PngDefPlayground/mango.png b/packages/experiments-realm/PngDefPlayground/mango.png new file mode 100644 index 0000000000000000000000000000000000000000..0e1efd55f34603cec8318e5e1b8a31575b77c50e GIT binary patch literal 114705 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 aalj#Y#4KPLbF6|1{21sN>rybbq5lJI>Lg77 literal 0 HcmV?d00001 diff --git a/packages/experiments-realm/png-def-playground.gts b/packages/experiments-realm/png-def-playground.gts new file mode 100644 index 0000000000..c4da4d77f2 --- /dev/null +++ b/packages/experiments-realm/png-def-playground.gts @@ -0,0 +1,249 @@ +import { + CardDef, + Component, + StringField, + contains, + field, + linksTo, + linksToMany, +} from 'https://cardstack.com/base/card-api'; +import { gt } from '@cardstack/boxel-ui/helpers'; +import { ImageDef } from 'https://cardstack.com/base/image-file-def'; + +/** + * Playground card for demonstrating ImageDef/PngDef capabilities. + * + * This card shows how ImageDef (and its subclass PngDef): + * - Automatically extracts image dimensions (width, height) + * - Renders images in different formats (isolated, embedded, atom, fitted) + * - Works with linksTo and linksToMany fields + */ +export class PngDefPlayground extends CardDef { + static displayName = 'Image Def Playground'; + + @field title = contains(StringField); + @field description = contains(StringField); + + // Single image link (accepts ImageDef or PngDef) + @field featuredImage = linksTo(ImageDef); + + // Multiple image links + @field gallery = linksToMany(ImageDef); + + static isolated = class Isolated extends Component { + + }; +} From e531f0897621c16e24f20e940f581cada91c482b Mon Sep 17 00:00:00 2001 From: tintinthong Date: Wed, 4 Feb 2026 20:38:17 +0800 Subject: [PATCH 09/19] add invite user to room command (#3946) * add invite user to room command * add missing inviteUserToRoom method in matrixService * add missing input to command * fix lint --- packages/base/command.gts | 19 +++- .../catalog-app/listing/listing.gts | 2 +- .../host/app/commands/create-listing-pr.ts | 13 ++- packages/host/app/commands/index.ts | 6 ++ .../host/app/commands/invite-user-to-room.ts | 34 ++++++ packages/host/app/services/matrix-service.ts | 28 +++++ .../host/tests/helpers/mock-matrix/_client.ts | 22 +++- .../commands/invite-user-to-room-test.gts | 102 ++++++++++++++++++ 8 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 packages/host/app/commands/invite-user-to-room.ts create mode 100644 packages/host/tests/integration/commands/invite-user-to-room-test.gts diff --git a/packages/base/command.gts b/packages/base/command.gts index 03b32fe9fe..d79224ba87 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -249,6 +249,11 @@ export class CreateAIAssistantRoomResult extends CardDef { @field roomId = contains(StringField); } +export class InviteUserToRoomInput extends CardDef { + @field roomId = contains(StringField); + @field userId = contains(StringField); +} + export class RegisterBotInput extends CardDef { @field username = contains(StringField); } @@ -363,7 +368,7 @@ export class ListingInstallResult extends CardDef { export class CreateListingPRInput extends CardDef { @field roomId = contains(StringField); @field realm = contains(RealmField); - @field listing = linksTo(CardDef); + @field listingId = contains(StringField); } export class CreateListingPRResult extends CardDef { @@ -374,6 +379,12 @@ export class CreateListingPRResult extends CardDef { @field prNumber = contains(NumberField); } +export class CreateListingPRRequestInput extends CardDef { + @field roomId = contains(StringField); + @field realm = contains(RealmField); + @field listingId = contains(StringField); +} + export class ListingCreateInput extends CardDef { @field openCardId = contains(StringField); @field codeRef = contains(CodeRefField); @@ -419,6 +430,12 @@ export class GetEventsFromRoomResult extends CardDef { @field matrixEvents = containsMany(JsonField); } +export class SendBotTriggerEventInput extends CardDef { + @field roomId = contains(StringField); + @field type = contains(StringField); + @field input = contains(JsonField); +} + export class PreviewFormatInput extends CardDef { @field cardId = contains(StringField); @field format = contains(StringField); diff --git a/packages/catalog-realm/catalog-app/listing/listing.gts b/packages/catalog-realm/catalog-app/listing/listing.gts index d7de40255d..159791951a 100644 --- a/packages/catalog-realm/catalog-app/listing/listing.gts +++ b/packages/catalog-realm/catalog-app/listing/listing.gts @@ -677,7 +677,7 @@ export class Listing extends CardDef { label: 'Make a PR', action: async () => { await new CreateListingPRCommand(commandContext).execute({ - listing: this, + listingId: this.id, realm: this[realmURL]!.href, }); }, diff --git a/packages/host/app/commands/create-listing-pr.ts b/packages/host/app/commands/create-listing-pr.ts index 23cd4462dd..9c5bd7bff4 100644 --- a/packages/host/app/commands/create-listing-pr.ts +++ b/packages/host/app/commands/create-listing-pr.ts @@ -65,14 +65,14 @@ export default class CreateListingPRCommand extends HostBaseCommand< return CreateListingPRResult; } - requireInputFields = ['realm', 'listing']; + requireInputFields = ['realm', 'listingId']; protected async run( input: BaseCommandModule.CreateListingPRInput, ): Promise { await this.matrixService.ready; - let { listing: listingInput, realm } = input; + let { listingId, realm } = input; let realmUrls = this.realmServer.availableRealmURLs; let realmUrl = new RealmPaths(new URL(realm)).url; @@ -80,8 +80,15 @@ export default class CreateListingPRCommand extends HostBaseCommand< throw new Error(`Invalid realm: ${realmUrl}`); } + if (!listingId) { + throw new Error('Missing listingId for CreateListingPR'); + } + // Listing type is from catalog; base command cannot express that type - const listing = listingInput as Listing; + const listing = (await this.store.get(listingId)) as Listing; + if (!listing) { + throw new Error(`Listing not found: ${listingId}`); + } const snapshotId = uuidv4(); const branch = this.generateBranchName(listing, snapshotId); diff --git a/packages/host/app/commands/index.ts b/packages/host/app/commands/index.ts index b7f9cc430c..79dc27836a 100644 --- a/packages/host/app/commands/index.ts +++ b/packages/host/app/commands/index.ts @@ -19,6 +19,7 @@ import * as GenerateThemeExampleCommandModule from './generate-theme-example'; import * as GetAllRealmMetasCommandModule from './get-all-realm-metas'; import * as GetCardCommandModule from './get-card'; import * as GetEventsFromRoomCommandModule from './get-events-from-room'; +import * as InviteUserToRoomCommandModule from './invite-user-to-room'; import * as LintAndFixCommandModule from './lint-and-fix'; import * as ListingBuildCommandModule from './listing-action-build'; import * as ListingInitCommandModule from './listing-action-init'; @@ -117,6 +118,10 @@ export function shimHostCommands(virtualNetwork: VirtualNetwork) { '@cardstack/boxel-host/commands/get-events-from-room', GetEventsFromRoomCommandModule, ); + virtualNetwork.shimModule( + '@cardstack/boxel-host/commands/invite-user-to-room', + InviteUserToRoomCommandModule, + ); virtualNetwork.shimModule( '@cardstack/boxel-host/commands/lint-and-fix', LintAndFixCommandModule, @@ -332,6 +337,7 @@ export const HostCommandClasses: (typeof HostBaseCommand)[] = [ GetAllRealmMetasCommandModule.default, GetCardCommandModule.default, GetEventsFromRoomCommandModule.default, + InviteUserToRoomCommandModule.default, LintAndFixCommandModule.default, ListingBuildCommandModule.default, ListingInitCommandModule.default, diff --git a/packages/host/app/commands/invite-user-to-room.ts b/packages/host/app/commands/invite-user-to-room.ts new file mode 100644 index 0000000000..8804fb894e --- /dev/null +++ b/packages/host/app/commands/invite-user-to-room.ts @@ -0,0 +1,34 @@ +import { service } from '@ember/service'; + +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +import HostBaseCommand from '../lib/host-base-command'; + +import type MatrixService from '../services/matrix-service'; + +export default class InviteUserToRoomCommand extends HostBaseCommand< + typeof BaseCommandModule.InviteUserToRoomInput +> { + @service declare private matrixService: MatrixService; + + static actionVerb = 'Invite'; + + async getInputType() { + let commandModule = await this.loadCommandModule(); + const { InviteUserToRoomInput } = commandModule; + return InviteUserToRoomInput; + } + + requireInputFields = ['roomId', 'userId']; + + protected async run( + input: BaseCommandModule.InviteUserToRoomInput, + ): Promise { + await this.matrixService.ready; + let userId = this.matrixService.getFullUserId(input.userId); + if (await this.matrixService.isUserInRoom(input.roomId, userId)) { + throw new Error(`user already in room: ${userId}`); + } + await this.matrixService.inviteUserToRoom(input.roomId, userId); + } +} diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 3e7a03d86e..5d16833ef2 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -406,6 +406,27 @@ export default class MatrixService extends Service { return `@${botRunnerUsername}:${server}`; } + getFullUserId(username: string) { + if (username.includes(':')) { + return username; + } + let server = this.userId?.split(':')[1]; + if (!server) { + throw new Error('Matrix server is unavailable for user id'); + } + let localpart = username.startsWith('@') ? username.slice(1) : username; + return `@${localpart}:${server}`; + } + + async isUserInRoom(roomId: string, userId: string) { + try { + let state = await this.getStateEvent(roomId, 'm.room.member', userId); + return state?.membership === 'invite' || state?.membership === 'join'; + } catch (_error) { + return false; + } + } + get userName() { return this.userId ? getMatrixUsername(this.userId) : null; } @@ -1367,6 +1388,13 @@ export default class MatrixService extends Service { }); } + async inviteUserToRoom(roomId: string, userId: string) { + let roomData = this.ensureRoomData(roomId); + await roomData.mutex.dispatch(async () => { + return this.client.invite(roomId, userId); + }); + } + async getStateEvent( roomId: string, eventType: string, diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index 96d5f95c7a..954ee98780 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -345,11 +345,27 @@ export class MockClient implements ExtendedClient { } invite( - _roomId: string, - _userId: string, + roomId: string, + userId: string, _reason?: string | undefined, ): Promise<{}> { - throw new Error('Method not implemented.'); + let sender = + this.loggedInAs ?? this.clientOpts.userId ?? '@test_user:localhost'; + let timestamp = Date.now(); + this.serverState.setRoomState( + sender, + roomId, + 'm.room.member', + { + displayname: userId, + membership: 'invite', + membershipTs: timestamp, + membershipInitiator: sender, + }, + userId, + timestamp, + ); + return Promise.resolve({}); } joinRoom( diff --git a/packages/host/tests/integration/commands/invite-user-to-room-test.gts b/packages/host/tests/integration/commands/invite-user-to-room-test.gts new file mode 100644 index 0000000000..84a830a2e2 --- /dev/null +++ b/packages/host/tests/integration/commands/invite-user-to-room-test.gts @@ -0,0 +1,102 @@ +import { getOwner } from '@ember/owner'; +import type { RenderingTestContext } from '@ember/test-helpers'; + +import { getService } from '@universal-ember/test-support'; +import { module, test } from 'qunit'; + +import InviteUserToRoomCommand from '@cardstack/host/commands/invite-user-to-room'; + +import type MatrixService from '@cardstack/host/services/matrix-service'; +import RealmService from '@cardstack/host/services/realm'; + +import { + setupIntegrationTestRealm, + setupLocalIndexing, + testRealmURL, + testRealmInfo, +} from '../../helpers'; + +import { setupMockMatrix } from '../../helpers/mock-matrix'; +import { setupRenderingTest } from '../../helpers/setup'; + +class StubRealmService extends RealmService { + get defaultReadableRealm() { + return { + path: testRealmURL, + info: testRealmInfo, + }; + } +} + +module('Integration | commands | invite-user-to-room', function (hooks) { + setupRenderingTest(hooks); + setupLocalIndexing(hooks); + + let mockMatrixUtils = setupMockMatrix(hooks, { + loggedInAs: '@testuser:localhost', + activeRealms: [testRealmURL], + }); + + let { createAndJoinRoom, getRoomState } = mockMatrixUtils; + + hooks.beforeEach(function (this: RenderingTestContext) { + getOwner(this)!.register('service:realm', StubRealmService); + }); + + hooks.beforeEach(async function () { + await setupIntegrationTestRealm({ + mockMatrixUtils, + contents: {}, + }); + }); + + test('invites a user to a room', async function (assert) { + let roomId = createAndJoinRoom({ + sender: '@testuser:localhost', + name: 'room-test', + }); + let commandService = getService('command-service'); + let matrixService = getService('matrix-service') as MatrixService; + + let command = new InviteUserToRoomCommand(commandService.commandContext); + await command.execute({ + roomId, + userId: 'bot-runner', + }); + + let botRunnerUserId = matrixService.getFullUserId('bot-runner'); + let membershipEvent = getRoomState( + roomId, + 'm.room.member', + botRunnerUserId, + ); + assert.strictEqual( + membershipEvent.membership, + 'invite', + 'bot-runner invited to room', + ); + }); + + test('rejects inviting a user twice', async function (assert) { + let roomId = createAndJoinRoom({ + sender: '@testuser:localhost', + name: 'room-test', + }); + let commandService = getService('command-service'); + + let command = new InviteUserToRoomCommand(commandService.commandContext); + await command.execute({ + roomId, + userId: 'bot-runner', + }); + + await assert.rejects( + command.execute({ + roomId, + userId: 'bot-runner', + }), + /user already in room/, + 'rejects inviting a user that is already in the room', + ); + }); +}); From 8a4f09bdf5fcee2eca36e9c9057039f4b2de8b55 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Mon, 26 Jan 2026 12:19:22 +0100 Subject: [PATCH 10/19] When inserting a new subscription cycle, read the actual subscription start/end period, not invoice period --- .../billing/stripe-webhook-handlers/index.ts | 2 + .../payment-succeeded.ts | 53 ++++++++++++++++--- packages/realm-server/tests/billing-test.ts | 10 +++- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/billing/stripe-webhook-handlers/index.ts b/packages/billing/stripe-webhook-handlers/index.ts index 0ee1145f52..b3a36bc9b4 100644 --- a/packages/billing/stripe-webhook-handlers/index.ts +++ b/packages/billing/stripe-webhook-handlers/index.ts @@ -37,6 +37,8 @@ export type StripeInvoicePaymentSucceededWebhookEvent = StripeEvent & { data: Array<{ amount: number; description: string; + type?: string; + proration?: boolean; price: { product: string; }; diff --git a/packages/billing/stripe-webhook-handlers/payment-succeeded.ts b/packages/billing/stripe-webhook-handlers/payment-succeeded.ts index 9b13df4711..167150d63b 100644 --- a/packages/billing/stripe-webhook-handlers/payment-succeeded.ts +++ b/packages/billing/stripe-webhook-handlers/payment-succeeded.ts @@ -26,6 +26,33 @@ import type { PgAdapter } from '@cardstack/postgres'; import { TransactionManager } from '@cardstack/postgres'; import { ProrationCalculator } from '../proration-calculator'; +function getInvoiceSubscriptionPeriod( + event: StripeInvoicePaymentSucceededWebhookEvent, + planStripeId: string, +) { + let lineItem = event.data.object.lines.data.find( + (line) => + line.amount >= 0 && + line.type === 'subscription' && + line.proration !== true && + line.price?.product === planStripeId && + line.period?.start && + line.period?.end, + ); + + if (lineItem?.period) { + return { + periodStart: lineItem.period.start, + periodEnd: lineItem.period.end, + }; + } + + return { + periodStart: event.data.object.period_start, + periodEnd: event.data.object.period_end, + }; +} + export async function handlePaymentSucceeded( dbAdapter: DBAdapter, event: StripeInvoicePaymentSucceededWebhookEvent, @@ -49,6 +76,11 @@ export async function handlePaymentSucceeded( throw new Error(`No plan found for product id: ${productId}`); } + let { periodStart, periodEnd } = getInvoiceSubscriptionPeriod( + event, + plan.stripePlanId, + ); + // When user first signs up for a plan, our checkout.session.completed handler takes care of assigning the user a stripe customer id. // Stripe customer id is needed so that we can recognize the user when their subscription is renewed, or canceled. // The mentioned webhook should be sent before this one, but if there are any network or processing delays, @@ -75,12 +107,18 @@ export async function handlePaymentSucceeded( user, plan, creditAllowance: plan.creditsIncluded, - periodStart: event.data.object.period_start, - periodEnd: event.data.object.period_end, + periodStart, + periodEnd, event, }); } else if (billingReason === 'subscription_cycle') { - await createSubscriptionCycle(dbAdapter, user, plan, event); + await createSubscriptionCycle( + dbAdapter, + user, + plan, + periodStart, + periodEnd, + ); } else if (billingReason === 'subscription_update') { await updateSubscription(dbAdapter, user, plan, event); } @@ -193,8 +231,9 @@ async function updateSubscription( async function createSubscriptionCycle( dbAdapter: DBAdapter, user: { id: string }, - plan: { creditsIncluded: number }, - event: StripeInvoicePaymentSucceededWebhookEvent, + plan: Plan, + periodStart: number, + periodEnd: number, ) { let currentActiveSubscription = await getCurrentActiveSubscription( dbAdapter, @@ -226,8 +265,8 @@ async function createSubscriptionCycle( let newSubscriptionCycle = await insertSubscriptionCycle(dbAdapter, { subscriptionId: currentActiveSubscription.id, - periodStart: event.data.object.period_start, - periodEnd: event.data.object.period_end, + periodStart, + periodEnd, }); await addToCreditsLedger(dbAdapter, { diff --git a/packages/realm-server/tests/billing-test.ts b/packages/realm-server/tests/billing-test.ts index 4aa476b92d..84c0385a42 100644 --- a/packages/realm-server/tests/billing-test.ts +++ b/packages/realm-server/tests/billing-test.ts @@ -703,7 +703,13 @@ module(basename(__filename), function () { data: [ { amount: 1200, + type: 'subscription', + proration: false, price: { product: 'prod_creator' }, + period: { + start: 20, + end: 30, + }, }, ], }, @@ -735,8 +741,8 @@ module(basename(__filename), function () { // Assert both subscription cycles have the correct period start and end assert.strictEqual(subscriptionCycles[0].periodStart, 1); assert.strictEqual(subscriptionCycles[0].periodEnd, 2); - assert.strictEqual(subscriptionCycles[1].periodStart, 2); - assert.strictEqual(subscriptionCycles[1].periodEnd, 3); + assert.strictEqual(subscriptionCycles[1].periodStart, 20); + assert.strictEqual(subscriptionCycles[1].periodEnd, 30); // Assert that the ledger has the correct sum of credits going in and out availableCredits = await sumUpCreditsLedger(dbAdapter, { From 78dec906a79b27840c8291860bf8a170eab1eb8d Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Tue, 27 Jan 2026 14:28:42 +0100 Subject: [PATCH 11/19] Prefer an error over fallback to incorrect values --- .../billing/stripe-webhook-handlers/payment-succeeded.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/billing/stripe-webhook-handlers/payment-succeeded.ts b/packages/billing/stripe-webhook-handlers/payment-succeeded.ts index 167150d63b..a284af3091 100644 --- a/packages/billing/stripe-webhook-handlers/payment-succeeded.ts +++ b/packages/billing/stripe-webhook-handlers/payment-succeeded.ts @@ -47,10 +47,9 @@ function getInvoiceSubscriptionPeriod( }; } - return { - periodStart: event.data.object.period_start, - periodEnd: event.data.object.period_end, - }; + throw new Error( + 'Expected subscription period to be present in payment succeeded webhook event', + ); } export async function handlePaymentSucceeded( From 5522aa2968be968f88058c548e272d7aae699af7 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Wed, 28 Jan 2026 12:39:49 +0100 Subject: [PATCH 12/19] Adjust tests --- packages/realm-server/tests/billing-test.ts | 20 +++++++++++++++++++ .../server-endpoints/stripe-webhook-test.ts | 12 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/packages/realm-server/tests/billing-test.ts b/packages/realm-server/tests/billing-test.ts index 84c0385a42..0951b48efa 100644 --- a/packages/realm-server/tests/billing-test.ts +++ b/packages/realm-server/tests/billing-test.ts @@ -147,7 +147,13 @@ module(basename(__filename), function () { data: [ { amount: 0, + type: 'subscription', + proration: false, price: { product: 'prod_free' }, + period: { + start: 1635873600, + end: 1638465600, + }, }, ], }, @@ -310,6 +316,8 @@ module(basename(__filename), function () { data: [ { amount: creatorPlan.monthlyPrice * 100, + type: 'subscription', + proration: false, price: { product: 'prod_creator' }, period: { start: 1, end: 2 }, }, @@ -405,12 +413,16 @@ module(basename(__filename), function () { { amount: -amountCreditedForUnusedTimeOnPreviousPlan, description: 'Unused time on Creator plan', + type: 'subscription', + proration: false, price: { product: 'prod_creator' }, period: { start: 3, end: 4 }, }, { amount: amountCreditedForRemainingTimeOnNewPlan, description: 'Remaining time on Power User plan', + type: 'subscription', + proration: false, price: { product: 'prod_power_user' }, period: { start: 4, end: 5 }, }, @@ -500,6 +512,8 @@ module(basename(__filename), function () { data: [ { amount: creatorPlan.monthlyPrice * 100, + type: 'subscription', + proration: false, price: { product: 'prod_creator' }, period: { start: 5, end: 6 }, }, @@ -578,7 +592,13 @@ module(basename(__filename), function () { data: [ { amount: 0, + type: 'subscription', + proration: false, price: { product: 'prod_free' }, + period: { + start: 1635873600, + end: 1638465600, + }, }, ], }, diff --git a/packages/realm-server/tests/server-endpoints/stripe-webhook-test.ts b/packages/realm-server/tests/server-endpoints/stripe-webhook-test.ts index a34d556d2b..e731aceceb 100644 --- a/packages/realm-server/tests/server-endpoints/stripe-webhook-test.ts +++ b/packages/realm-server/tests/server-endpoints/stripe-webhook-test.ts @@ -153,7 +153,10 @@ module(`server-endpoints/${basename(__filename)}`, function () { data: [ { amount: 12, + type: 'subscription', + proration: false, price: { product: 'prod_creator' }, + period: { start: 1635873600, end: 1638465600 }, }, ], }, @@ -210,7 +213,10 @@ module(`server-endpoints/${basename(__filename)}`, function () { data: [ { amount: 0, + type: 'subscription', + proration: false, price: { product: 'prod_free' }, + period: { start: 1635873600, end: 1638465600 }, }, ], }, @@ -377,7 +383,10 @@ module(`server-endpoints/${basename(__filename)}`, function () { data: [ { amount: 12, + type: 'subscription', + proration: false, price: { product: 'prod_creator' }, + period: { start: 1635873600, end: 1638465600 }, }, ], }, @@ -561,7 +570,10 @@ module(`server-endpoints/${basename(__filename)}`, function () { data: [ { amount: 0, + type: 'subscription', + proration: false, price: { product: 'prod_free' }, + period: { start: 1635873600, end: 1638465600 }, }, ], }, From ab505db88c87808e650fd937b6ffc142d15a7899 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Wed, 4 Feb 2026 13:13:26 +0100 Subject: [PATCH 13/19] Improve error msg --- packages/billing/stripe-webhook-handlers/payment-succeeded.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/billing/stripe-webhook-handlers/payment-succeeded.ts b/packages/billing/stripe-webhook-handlers/payment-succeeded.ts index a284af3091..d187005582 100644 --- a/packages/billing/stripe-webhook-handlers/payment-succeeded.ts +++ b/packages/billing/stripe-webhook-handlers/payment-succeeded.ts @@ -48,7 +48,7 @@ function getInvoiceSubscriptionPeriod( } throw new Error( - 'Expected subscription period to be present in payment succeeded webhook event', + `Expected subscription period to be present in payment succeeded webhook event (event id: ${event.id}, invoice id: ${event.data.object.id}, subscription id: ${event.data.object.subscription}, plan stripe id: ${planStripeId})`, ); } From 97dcef8cf32c44bbaf4d5a6f1d36cb2d69d0480c Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 4 Feb 2026 18:10:44 +0100 Subject: [PATCH 14/19] Change realm download to not use in-memory blob (#3944) --- .../code-submode/left-panel-toggle.gts | 59 ++++------ packages/host/app/lib/download-realm.ts | 14 --- packages/matrix/tests/download-realm.spec.ts | 74 ++++++++++++ .../handlers/handle-download-realm.ts | 25 ++++- .../server-endpoints/download-realm-test.ts | 106 ++++++++++++++++++ packages/runtime-common/url-signature.ts | 65 +++++++++++ 6 files changed, 291 insertions(+), 52 deletions(-) create mode 100644 packages/matrix/tests/download-realm.spec.ts create mode 100644 packages/runtime-common/url-signature.ts diff --git a/packages/host/app/components/operator-mode/code-submode/left-panel-toggle.gts b/packages/host/app/components/operator-mode/code-submode/left-panel-toggle.gts index 27d3de1f15..dd32657f5f 100644 --- a/packages/host/app/components/operator-mode/code-submode/left-panel-toggle.gts +++ b/packages/host/app/components/operator-mode/code-submode/left-panel-toggle.gts @@ -10,17 +10,15 @@ import { Button as BoxelButton } from '@cardstack/boxel-ui/components'; import { cn, not } from '@cardstack/boxel-ui/helpers'; import { Download } from '@cardstack/boxel-ui/icons'; +import { createURLSignature } from '@cardstack/runtime-common/url-signature'; + import RealmDropdown from '@cardstack/host/components/realm-dropdown'; -// These were inline but caused the template to have spurious Glint errors -import { - extractFilename, - fallbackDownloadName, -} from '@cardstack/host/lib/download-realm'; +// This was inline but caused the template to have spurious Glint errors +import { fallbackDownloadName } from '@cardstack/host/lib/download-realm'; import RestoreScrollPosition from '@cardstack/host/modifiers/restore-scroll-position'; -import type NetworkService from '@cardstack/host/services/network'; import type { FileView } from '@cardstack/host/services/operator-mode-state-service'; import type OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service'; import type RealmService from '@cardstack/host/services/realm'; @@ -47,7 +45,6 @@ interface Signature { export default class CodeSubmodeLeftPanelToggle extends Component { @service declare operatorModeStateService: OperatorModeStateService; @service declare private recentFilesService: RecentFilesService; - @service declare private network: NetworkService; @service declare private realm: RealmService; private notifyFileBrowserIsVisible: (() => void) | undefined; @@ -107,43 +104,31 @@ export default class CodeSubmodeLeftPanelToggle extends Component { this.switchRealm(realmItem.path); }; - private get downloadRealmURL() { + private get downloadFilename() { + return fallbackDownloadName(new URL(this.args.realmURL)); + } + + downloadRealm = async (event: Event) => { + event.preventDefault(); + let downloadURL = new URL('/_download-realm', this.args.realmURL); downloadURL.searchParams.set('realm', this.args.realmURL); - return downloadURL.href; - } - private triggerDownload(blob: Blob, filename: string) { - let blobUrl = URL.createObjectURL(blob); + let token = this.realm.token(this.args.realmURL); + if (token) { + downloadURL.searchParams.set('token', token); + // Add signature binding the token to this specific URL + let sig = await createURLSignature(token, downloadURL); + downloadURL.searchParams.set('sig', sig); + } + + // Use an anchor element to trigger native browser download (streams without loading into memory) let downloadLink = document.createElement('a'); - downloadLink.href = blobUrl; - downloadLink.download = filename; + downloadLink.href = downloadURL.href; + downloadLink.download = this.downloadFilename; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); - URL.revokeObjectURL(blobUrl); - } - - downloadRealm = async (event: Event) => { - event.preventDefault(); - try { - let token = this.realm.token(this.args.realmURL); - let response = await this.network.authedFetch(this.downloadRealmURL, { - headers: token ? { Authorization: token } : {}, - }); - if (!response.ok) { - throw new Error( - `Failed to download realm: ${response.status} ${response.statusText}`, - ); - } - let blob = await response.blob(); - let filename = - extractFilename(response.headers.get('content-disposition')) ?? - fallbackDownloadName(new URL(this.args.realmURL)); - this.triggerDownload(blob, filename); - } catch (error) { - console.error('Error downloading realm:', error); - } };