diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f0c30c537..6da671e7c4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -483,6 +483,7 @@ jobs: "types-endpoint-test.ts", "server-endpoints/authentication-test.ts", "server-endpoints/bot-registration-test.ts", + "server-endpoints/download-realm-test.ts", "server-endpoints/index-responses-test.ts", "server-endpoints/maintenance-endpoints-test.ts", "server-endpoints/queue-status-test.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 f12004fbc8..27d3de1f15 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 @@ -6,12 +6,24 @@ import Component from '@glimmer/component'; import FileCheck from '@cardstack/boxel-icons/file-check'; import FolderTree from '@cardstack/boxel-icons/folder-tree'; +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 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'; + 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'; import type RecentFilesService from '@cardstack/host/services/recent-files-service'; import InnerContainer from './inner-container'; @@ -35,6 +47,8 @@ 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; @@ -93,6 +107,45 @@ export default class CodeSubmodeLeftPanelToggle extends Component { this.switchRealm(realmItem.path); }; + private get downloadRealmURL() { + 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 downloadLink = document.createElement('a'); + downloadLink.href = blobUrl; + downloadLink.download = filename; + 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); + } + }; + } diff --git a/packages/host/app/lib/download-realm.ts b/packages/host/app/lib/download-realm.ts new file mode 100644 index 0000000000..c0238aa6a0 --- /dev/null +++ b/packages/host/app/lib/download-realm.ts @@ -0,0 +1,23 @@ +export function extractFilename( + contentDisposition: string | null, +): string | null { + if (!contentDisposition) { + return null; + } + let utf8Match = contentDisposition.match(/filename\\*=UTF-8''([^;]+)/i); + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1]); + } + let match = contentDisposition.match(/filename="?([^";]+)"?/i); + return match?.[1] ?? null; +} + +export function fallbackDownloadName(realmURL: URL) { + let segments = realmURL.pathname.split('/').filter(Boolean); + let base = + segments.length >= 2 + ? segments.slice(-2).join('-') + : (segments[0] ?? realmURL.hostname); + base = base.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, ''); + return base.length > 0 ? `${base}.zip` : 'realm.zip'; +} diff --git a/packages/realm-server/handlers/handle-download-realm.ts b/packages/realm-server/handlers/handle-download-realm.ts new file mode 100644 index 0000000000..b2002edac7 --- /dev/null +++ b/packages/realm-server/handlers/handle-download-realm.ts @@ -0,0 +1,259 @@ +import type Koa from 'koa'; +import type { DBAdapter, Realm } from '@cardstack/runtime-common'; +import { + ensureTrailingSlash, + fetchUserPermissions, + logger, + param, + query, + PUBLISHED_DIRECTORY_NAME, + RealmPaths, +} from '@cardstack/runtime-common'; +import { AuthenticationError } from '@cardstack/runtime-common/router'; +import { parseRealmsParam } from '@cardstack/runtime-common/search-utils'; +import archiver from 'archiver'; +import { existsSync, statSync } from 'fs-extra'; +import { join, resolve, sep } from 'path'; +import type { CreateRoutesArgs } from '../routes'; +import { retrieveTokenClaim } from '../utils/jwt'; +import { + buildReadableRealms, + getPublishedRealmURLs, +} from '../utils/realm-readability'; +import { + fetchRequestFromContext, + sendResponseForBadRequest, + sendResponseForForbiddenRequest, + sendResponseForNotFound, + sendResponseForSystemError, + sendResponseForUnauthorizedRequest, +} from '../middleware'; + +const log = logger('download-realm'); + +type PublishedRealmRow = { + id: string; +}; + +export default function handleDownloadRealm({ + dbAdapter, + realmSecretSeed, + realms, + realmsRootPath, + serverURL, +}: CreateRoutesArgs): (ctxt: Koa.Context, next: Koa.Next) => Promise { + return async function (ctxt: Koa.Context, _next: Koa.Next) { + let request = await fetchRequestFromContext(ctxt); + let url = new URL(request.url); + + let realmList = parseRealmsParam(url); + if (realmList.length === 0) { + let realmParam = + url.searchParams.get('realm') ?? url.searchParams.get('realmURL'); + if (realmParam) { + realmList = [ensureTrailingSlash(realmParam)]; + } + } + + if (realmList.length !== 1) { + await sendResponseForBadRequest( + ctxt, + 'A single realm must be specified via ?realm= or ?realms=', + ); + return; + } + + let realmURL = ensureTrailingSlash(realmList[0]); + let parsedRealmURL: URL; + try { + parsedRealmURL = new URL(realmURL); + realmURL = ensureTrailingSlash(parsedRealmURL.href); + } catch { + await sendResponseForBadRequest( + ctxt, + `Invalid realm URL supplied: ${realmURL}`, + ); + return; + } + if (!hasRealm(realms, realmURL)) { + await sendResponseForNotFound(ctxt, `Realm not found: ${realmURL}`); + return; + } + + let publishedRealmURLs = await getPublishedRealmURLs(dbAdapter, [realmURL]); + let authorization = ctxt.req.headers['authorization']; + let readableRealms: Set; + if (!authorization) { + let publicPermissions = await fetchUserPermissions(dbAdapter, { + userId: '*', + onlyOwnRealms: false, + }); + readableRealms = buildReadableRealms( + publicPermissions, + publishedRealmURLs, + ); + if (!readableRealms.has(realmURL)) { + await sendResponseForUnauthorizedRequest( + ctxt, + `Authorization required for realm: ${realmURL}`, + ); + return; + } + } else { + try { + let token = retrieveTokenClaim(authorization, realmSecretSeed); + let permissions = await fetchUserPermissions(dbAdapter, { + userId: token.user, + onlyOwnRealms: false, + }); + readableRealms = buildReadableRealms(permissions, publishedRealmURLs); + if (!readableRealms.has(realmURL)) { + await sendResponseForForbiddenRequest( + ctxt, + `Insufficient permissions to read realm: ${realmURL}`, + ); + return; + } + } catch (e) { + if (e instanceof AuthenticationError) { + await sendResponseForUnauthorizedRequest(ctxt, e.message); + return; + } + throw e; + } + } + + let realmPath = await resolveRealmPath({ + dbAdapter, + realmURL, + realmsRootPath, + serverURL, + }); + if (!realmPath) { + await sendResponseForNotFound( + ctxt, + `Realm is not stored in realmsRootPath: ${realmURL}`, + ); + return; + } + + if (!existsSync(realmPath) || !statSync(realmPath).isDirectory()) { + await sendResponseForNotFound( + ctxt, + `Realm files not found on disk for ${realmURL}`, + ); + return; + } + + let filename = `${buildArchiveName(parsedRealmURL)}.zip`; + let archive = archiver('zip', { zlib: { level: 9 } }); + archive.on('warning', (warning) => { + log.warn(`Zip warning for ${realmURL}: ${warning}`); + }); + archive.on('error', (error) => { + log.error(`Zip error for ${realmURL}: ${error}`); + ctxt.res.destroy(error as Error); + }); + + ctxt.status = 200; + ctxt.set('content-type', 'application/zip'); + ctxt.set('content-disposition', `attachment; filename="${filename}"`); + ctxt.respond = false; + + archive.pipe(ctxt.res); + + try { + archive.directory(realmPath, false); + await archive.finalize(); + } catch (error) { + log.error(`Failed to create archive for ${realmURL}: ${error}`); + if (!ctxt.res.headersSent) { + await sendResponseForSystemError( + ctxt, + `Failed to stream realm archive for ${realmURL}`, + ); + } else { + ctxt.res.destroy(error as Error); + } + } + }; +} + +function hasRealm(realms: Realm[], realmURL: string): boolean { + return realms.some((realm) => ensureTrailingSlash(realm.url) === realmURL); +} + +async function resolveRealmPath({ + dbAdapter, + realmURL, + realmsRootPath, + serverURL, +}: { + dbAdapter: DBAdapter; + realmURL: string; + realmsRootPath: string; + serverURL: string; +}): Promise { + let published = (await query(dbAdapter, [ + 'SELECT id FROM published_realms WHERE published_realm_url =', + param(realmURL), + ])) as PublishedRealmRow[]; + if (published.length > 0) { + return join(realmsRootPath, PUBLISHED_DIRECTORY_NAME, published[0].id); + } + + let realmPath = realmPathFromServerURL({ + realmURL, + realmsRootPath, + serverURL, + }); + if (!realmPath) { + return null; + } + + let root = resolve(realmsRootPath); + let resolvedRealmPath = resolve(realmPath); + if ( + resolvedRealmPath !== root && + !resolvedRealmPath.startsWith(`${root}${sep}`) + ) { + return null; + } + + return realmPath; +} + +function realmPathFromServerURL({ + realmURL, + realmsRootPath, + serverURL, +}: { + realmURL: string; + realmsRootPath: string; + serverURL: string; +}): string | null { + let serverRoot = new RealmPaths(new URL(ensureTrailingSlash(serverURL))); + let localPath: string; + try { + localPath = serverRoot.local(new URL(realmURL)); + } catch { + return null; + } + + let parts = localPath.split('/').filter(Boolean); + if (parts.length < 1) { + return null; + } + + return resolve(join(realmsRootPath, ...parts)); +} + +function buildArchiveName(realmURL: URL): string { + let segments = realmURL.pathname.split('/').filter(Boolean); + let base = + segments.length >= 2 + ? segments.slice(-2).join('-') + : (segments[0] ?? realmURL.hostname); + base = base.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, ''); + return base.length > 0 ? base : 'realm'; +} diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index 25cef2730d..7fe71b0a5f 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -16,6 +16,7 @@ "@koa/router": "catalog:", "@octokit/rest": "catalog:", "@sentry/node": "catalog:", + "@types/archiver": "catalog:", "@types/flat": "catalog:", "@types/fs-extra": "catalog:", "@types/js-yaml": "catalog:", @@ -34,10 +35,12 @@ "@types/qunit": "catalog:", "@types/sane": "catalog:", "@types/sinon": "catalog:", + "@types/superagent": "catalog:", "@types/supertest": "catalog:", "@types/tmp": "catalog:", "@types/uuid": "catalog:", "@types/yargs": "catalog:", + "archiver": "catalog:", "concurrently": "catalog:", "content-tag": "catalog:", "cron": "catalog:", diff --git a/packages/realm-server/routes.ts b/packages/realm-server/routes.ts index 0267aed5e0..03e52ecb8f 100644 --- a/packages/realm-server/routes.ts +++ b/packages/realm-server/routes.ts @@ -42,6 +42,7 @@ import handleSearchPrerendered from './handlers/handle-search-prerendered'; import handleRealmInfo from './handlers/handle-realm-info'; import { multiRealmAuthorization } from './middleware/multi-realm-authorization'; import handleGitHubPRRequest from './handlers/handle-github-pr'; +import handleDownloadRealm from './handlers/handle-download-realm'; import { handleBotRegistrationRequest, handleBotRegistrationsRequest, @@ -241,6 +242,7 @@ export function createRoutes(args: CreateRoutesArgs) { jwtMiddleware(args.realmSecretSeed), handleGitHubPRRequest(args), ); + router.get('/_download-realm', handleDownloadRealm(args)); router.post( '/_bot-registration', jwtMiddleware(args.realmSecretSeed), diff --git a/packages/realm-server/tests/index.ts b/packages/realm-server/tests/index.ts index a330cf9584..f713252c9d 100644 --- a/packages/realm-server/tests/index.ts +++ b/packages/realm-server/tests/index.ts @@ -144,6 +144,7 @@ import './realm-endpoints/user-test'; import './search-prerendered-test'; import './server-endpoints/authentication-test'; import './server-endpoints/bot-registration-test'; +import './server-endpoints/download-realm-test'; import './server-endpoints/index-responses-test'; import './server-endpoints/maintenance-endpoints-test'; import './server-endpoints/queue-status-test'; diff --git a/packages/realm-server/tests/server-endpoints/download-realm-test.ts b/packages/realm-server/tests/server-endpoints/download-realm-test.ts new file mode 100644 index 0000000000..22fb8c4999 --- /dev/null +++ b/packages/realm-server/tests/server-endpoints/download-realm-test.ts @@ -0,0 +1,89 @@ +import { module, test } from 'qunit'; +import { basename } from 'path'; +import { setupServerEndpointsTest, testRealm2URL } from './helpers'; +import type { Response } from 'superagent'; + +function binaryParser( + res: Response, + callback: (err: Error | null, body: Buffer) => void, +) { + let data = ''; + + res.setEncoding('binary'); + + res.on('data', (chunk: string) => { + data += chunk; + }); + + res.on('end', () => { + callback(null, Buffer.from(data, 'binary')); + }); +} + +module(`server-endpoints/${basename(__filename)}`, function (hooks) { + let context = setupServerEndpointsTest(hooks); + + test('downloads realm as a zip archive', async function (assert) { + let response = await context.request2 + .get('/_download-realm') + .query({ realm: testRealm2URL.href }) + .buffer(true) + .parse(binaryParser); + + let bodyPreview = response.body?.toString?.('utf8') ?? response.text ?? ''; + assert.strictEqual(response.status, 200, bodyPreview.slice(0, 200)); + assert.strictEqual( + response.headers['content-type'], + 'application/zip', + 'serves a zip archive', + ); + assert.ok( + response.headers['content-disposition']?.includes('.zip'), + 'includes attachment filename', + ); + assert.ok(response.body instanceof Buffer, 'response body is a Buffer'); + assert.strictEqual( + response.body.subarray(0, 2).toString('utf8'), + 'PK', + 'zip file signature is present', + ); + assert.ok( + response.body.includes(Buffer.from('.realm.json')), + 'archive includes realm files', + ); + }); + + test('requires auth when realm is not public', async function (assert) { + await context.dbAdapter.execute( + `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealm2URL.href}' AND username = '*'`, + ); + + let response = await context.request2 + .get('/_download-realm') + .query({ realm: testRealm2URL.href }); + + assert.strictEqual(response.status, 401, 'returns unauthorized'); + }); + + test('returns 400 when realm is missing from query params', async function (assert) { + let response = await context.request2.get('/_download-realm'); + + assert.strictEqual(response.status, 400, 'returns bad request'); + assert.ok( + response.body.errors?.[0]?.includes('single realm must be specified'), + 'explains required realm parameter', + ); + }); + + test('returns 404 when realm is not registered on the server', async function (assert) { + let response = await context.request2 + .get('/_download-realm') + .query({ realm: 'http://127.0.0.1:4445/missing/' }); + + assert.strictEqual(response.status, 404, 'returns not found'); + assert.ok( + response.body.errors?.[0]?.includes('Realm not found'), + 'explains missing realm', + ); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7b808080d..dea5f3b9c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,6 +135,9 @@ catalogs: '@sqlite.org/sqlite-wasm': specifier: 3.45.1-build1 version: 3.45.1-build1 + '@types/archiver': + specifier: ^7.0.0 + version: 7.0.0 '@types/babel__core': specifier: ^7.1.19 version: 7.20.5 @@ -234,6 +237,9 @@ catalogs: '@types/stream-json': specifier: ^1.7.3 version: 1.7.8 + '@types/superagent': + specifier: ^8.1.9 + version: 8.1.9 '@types/supertest': specifier: ^2.0.12 version: 2.0.16 @@ -261,6 +267,9 @@ catalogs: ajv: specifier: ^8.17.1 version: 8.17.1 + archiver: + specifier: ^7.0.0 + version: 7.0.1 awesome-phonenumber: specifier: ^7.2.0 version: 7.6.0 @@ -2071,7 +2080,7 @@ importers: version: link:../runtime-common '@cardstack/view-transitions': specifier: 'catalog:' - version: 0.2.0(@babel/core@7.28.6)(ember-modifier@4.1.0(ember-source@5.4.1(patch_hash=f4d53cc0efd30368b913bdc898f342658768eaf0a8fb44678c0a07cf3aac24c1)(@babel/core@7.28.6)(@glimmer/component@2.0.0)(@glint/template@1.3.0)(rsvp@4.8.5))) + version: 0.2.0(@babel/core@7.28.6)(ember-modifier@4.1.0(ember-source@5.4.1(patch_hash=f4d53cc0efd30368b913bdc898f342658768eaf0a8fb44678c0a07cf3aac24c1)(@babel/core@7.28.6)(@glimmer/component@2.0.0)(@glint/template@1.3.0)(rsvp@4.8.5)(webpack@5.104.1))) '@types/lodash': specifier: 'catalog:' version: 4.17.23 @@ -2147,7 +2156,7 @@ importers: version: link:../runtime-common '@cardstack/view-transitions': specifier: 'catalog:' - version: 0.2.0(@babel/core@7.28.6)(ember-modifier@4.1.0(ember-source@5.4.1(patch_hash=f4d53cc0efd30368b913bdc898f342658768eaf0a8fb44678c0a07cf3aac24c1)(@babel/core@7.28.6)(@glimmer/component@2.0.0)(@glint/template@1.3.0)(rsvp@4.8.5))) + version: 0.2.0(@babel/core@7.28.6)(ember-modifier@4.1.0(ember-source@5.4.1(patch_hash=f4d53cc0efd30368b913bdc898f342658768eaf0a8fb44678c0a07cf3aac24c1)(@babel/core@7.28.6)(@glimmer/component@2.0.0)(@glint/template@1.3.0)(rsvp@4.8.5)(webpack@5.104.1))) '@ember/optional-features': specifier: ^2.0.0 version: 2.3.0 @@ -2726,6 +2735,9 @@ importers: '@sentry/node': specifier: 'catalog:' version: 8.55.0 + '@types/archiver': + specifier: 'catalog:' + version: 7.0.0 '@types/flat': specifier: 'catalog:' version: 5.0.5 @@ -2780,6 +2792,9 @@ importers: '@types/sinon': specifier: 'catalog:' version: 17.0.4 + '@types/superagent': + specifier: 'catalog:' + version: 8.1.9 '@types/supertest': specifier: 'catalog:' version: 2.0.16 @@ -2792,6 +2807,9 @@ importers: '@types/yargs': specifier: 'catalog:' version: 17.0.35 + archiver: + specifier: 'catalog:' + version: 7.0.1 concurrently: specifier: 'catalog:' version: 8.2.2 @@ -6053,6 +6071,9 @@ packages: '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} + '@types/archiver@7.0.0': + resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==} + '@types/babel__code-frame@7.27.0': resolution: {integrity: sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==} @@ -6276,6 +6297,9 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/readdir-glob@1.1.5': + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} @@ -6623,6 +6647,10 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -6789,6 +6817,14 @@ packages: arch@2.2.0: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -7409,6 +7445,10 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -7768,6 +7808,10 @@ packages: component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -8062,6 +8106,15 @@ packages: countries-list@3.2.2: resolution: {integrity: sha512-ABJ/RWQBrPWy+hRuZoW+0ooK8p65Eo3WmUZwHm6v4wmfSPznNAKzjy3+UUYrJK2v3182BVsgWxdB6ROidj39kw==} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + create-ecdh@4.0.4: resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} @@ -9361,6 +9414,10 @@ packages: event-stream@3.3.4: resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -10759,6 +10816,10 @@ packages: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} engines: {node: '> 0.8'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + lcid@3.1.1: resolution: {integrity: sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==} engines: {node: '>=8'} @@ -12258,6 +12319,13 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + recast@0.18.10: resolution: {integrity: sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==} engines: {node: '>= 4'} @@ -14131,6 +14199,10 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -15086,7 +15158,7 @@ snapshots: '@cardstack/requirejs-monaco-ember-polyfill@0.0.1': {} - '@cardstack/view-transitions@0.2.0(@babel/core@7.28.6)(ember-modifier@4.1.0(ember-source@5.4.1(patch_hash=f4d53cc0efd30368b913bdc898f342658768eaf0a8fb44678c0a07cf3aac24c1)(@babel/core@7.28.6)(@glimmer/component@2.0.0)(@glint/template@1.3.0)(rsvp@4.8.5)))': + '@cardstack/view-transitions@0.2.0(@babel/core@7.28.6)(ember-modifier@4.1.0(ember-source@5.4.1(patch_hash=f4d53cc0efd30368b913bdc898f342658768eaf0a8fb44678c0a07cf3aac24c1)(@babel/core@7.28.6)(@glimmer/component@2.0.0)(@glint/template@1.3.0)(rsvp@4.8.5)(webpack@5.104.1)))': dependencies: '@embroider/addon-shim': 1.10.2 decorator-transforms: 2.3.1(@babel/core@7.28.6) @@ -17324,6 +17396,10 @@ snapshots: dependencies: '@types/node': 25.0.8 + '@types/archiver@7.0.0': + dependencies: + '@types/readdir-glob': 1.1.5 + '@types/babel__code-frame@7.27.0': {} '@types/babel__core@7.20.5': @@ -17594,6 +17670,10 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/readdir-glob@1.1.5': + dependencies: + '@types/node': 25.0.8 + '@types/responselike@1.0.3': dependencies: '@types/node': 25.0.8 @@ -18053,6 +18133,10 @@ snapshots: abbrev@1.1.1: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -18201,6 +18285,29 @@ snapshots: arch@2.2.0: {} + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + are-we-there-yet@3.0.1: dependencies: delegates: 1.0.0 @@ -19164,6 +19271,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -19516,6 +19625,14 @@ snapshots: component-emitter@1.3.1: {} + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -19663,6 +19780,13 @@ snapshots: countries-list@3.2.2: {} + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + create-ecdh@4.0.4: dependencies: bn.js: 4.12.2 @@ -22242,6 +22366,8 @@ snapshots: stream-combiner: 0.0.4 through: 2.3.8 + event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} @@ -24020,6 +24146,10 @@ snapshots: lazy-ass@1.6.0: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + lcid@3.1.1: dependencies: invert-kv: 3.0.1 @@ -25609,6 +25739,18 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + recast@0.18.10: dependencies: ast-types: 0.13.3 @@ -27789,4 +27931,10 @@ snapshots: yoctocolors-cjs@2.1.3: {} + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1a255ac60e..f92fde2f74 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,97 +5,100 @@ packages: - vendor/* catalog: - '@actions/core': ^1.2.6 - '@actions/github': ^4.0.0 - '@aws-crypto/sha256-js': ^5.2.0 - '@babel/core': ^7.26.10 - '@babel/generator': ^7.17.8 - '@babel/helper-module-imports': ^7.18.2 - '@babel/helper-module-transforms': ^7.18.9 - '@babel/parser': 7.27.0 - '@babel/plugin-proposal-class-properties': ^7.18.6 - '@babel/plugin-proposal-decorators': ^7.23.2 - '@babel/plugin-syntax-class-properties': ^7.12.13 - '@babel/plugin-syntax-decorators': ^7.17.12 - '@babel/plugin-syntax-typescript': ^7.17.12 - '@babel/plugin-transform-class-properties': ^7.22.5 - '@babel/plugin-transform-class-static-block': ^7.22.11 - '@babel/plugin-transform-modules-amd': ^7.13.0 - '@babel/plugin-transform-typescript': ^7.16.8 - '@babel/preset-typescript': ^7.24.7 - '@babel/runtime': ^7.22.11 - '@babel/traverse': 7.27.0 - '@cardstack/requirejs-monaco-ember-polyfill': ^0.0.1 - '@cardstack/view-transitions': ^0.2.0 - '@ember/string': ^4.0.1 - '@ember/test-waiters': ^4.1.1 - '@eslint/eslintrc': ^2.1.4 - '@eslint/js': ^8.57.1 - '@floating-ui/dom': ^1.6.3 - '@glimmer/component': ^2.0.0 - '@glint/environment-ember-loose': ^1.5.2 - '@koa/cors': ^4.0.0 - '@koa/router': ^14.0.0 - '@lucide/lab': ^0.1.2 - '@lukeed/uuid': ^2.0.1 - '@octokit/rest': ^22.0.1 - '@percy/cli': ^1.31.1 - '@percy/ember': ^5.0.0 - '@playwright/test': ^1.54.0 - '@rollup/plugin-babel': ^6.0.4 - '@sentry/node': ^8.31.0 - '@simple-dom/interface': ^1.4.0 - '@simple-dom/parser': ^1.4.0 - '@simple-dom/serializer': ^1.4.0 - '@simple-dom/void-map': ^1.4.0 - '@sinonjs/fake-timers': ^11.2.2 - '@sqlite.org/sqlite-wasm': 3.45.1-build1 - '@tabler/icons': ^3.19.0 - '@types/babel__core': ^7.1.19 - '@types/babel__generator': ^7.6.4 - '@types/babel__traverse': ^7.14.2 - '@types/diff': ^5.0.2 - '@types/dompurify': ^3.0.2 - '@types/eslint': 8.56.5 - '@types/flat': ^5.0.5 - '@types/fs-extra': ^11.0.4 - '@types/htmlbars-inline-precompile': ^3.0.3 - '@types/indefinite': ^2.3.4 - '@types/js-string-escape': ^1.0.1 - '@types/js-yaml': ^4.0.9 - '@types/jsdom': ^21.1.1 - '@types/jsonwebtoken': ^9.0.5 - '@types/koa': ^2.13.5 - '@types/koa-compose': ^3.2.5 - '@types/koa__cors': ^4.0.0 - '@types/koa__router': ^12.0.0 - '@types/line-column': ^1.0.0 - '@types/lodash': ^4.17.15 - '@types/matrix-js-sdk': ^11.0.1 - '@types/mime-types': ^2.1.1 - '@types/ms': ^2.1.0 - '@types/node': ^24.3.0 - '@types/pg': ^8.11.5 - '@types/pluralize': ^0.0.30 - '@types/qs': ^6.9.17 - '@types/qunit': ^2.19.12 - '@types/rsvp': ^4.0.9 - '@types/sane': ^2.0.1 - '@types/sinon': ^17.0.3 - '@types/sinonjs__fake-timers': ^8.1.5 - '@types/statuses': ^2.0.5 - '@types/stream-chain': ^2.0.1 - '@types/stream-json': ^1.7.3 - '@types/string.prototype.matchall': ^4.0.1 - '@types/supertest': ^2.0.12 - '@types/tmp': ^0.2.3 - '@types/uuid': ^9.0.8 - '@types/yargs': ^17.0.10 - '@typescript-eslint/eslint-plugin': ^7.18.0 - '@typescript-eslint/parser': ^7.18.0 - '@universal-ember/test-support': ^0.5.1 - '@vscode/vsce': ^3.1.0 + "@actions/core": ^1.2.6 + "@actions/github": ^4.0.0 + "@aws-crypto/sha256-js": ^5.2.0 + "@babel/core": ^7.26.10 + "@babel/generator": ^7.17.8 + "@babel/helper-module-imports": ^7.18.2 + "@babel/helper-module-transforms": ^7.18.9 + "@babel/parser": 7.27.0 + "@babel/plugin-proposal-class-properties": ^7.18.6 + "@babel/plugin-proposal-decorators": ^7.23.2 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-decorators": ^7.17.12 + "@babel/plugin-syntax-typescript": ^7.17.12 + "@babel/plugin-transform-class-properties": ^7.22.5 + "@babel/plugin-transform-class-static-block": ^7.22.11 + "@babel/plugin-transform-modules-amd": ^7.13.0 + "@babel/plugin-transform-typescript": ^7.16.8 + "@babel/preset-typescript": ^7.24.7 + "@babel/runtime": ^7.22.11 + "@babel/traverse": 7.27.0 + "@cardstack/requirejs-monaco-ember-polyfill": ^0.0.1 + "@cardstack/view-transitions": ^0.2.0 + "@ember/string": ^4.0.1 + "@ember/test-waiters": ^4.1.1 + "@eslint/eslintrc": ^2.1.4 + "@eslint/js": ^8.57.1 + "@floating-ui/dom": ^1.6.3 + "@glimmer/component": ^2.0.0 + "@glint/environment-ember-loose": ^1.5.2 + "@koa/cors": ^4.0.0 + "@koa/router": ^14.0.0 + "@lucide/lab": ^0.1.2 + "@lukeed/uuid": ^2.0.1 + "@octokit/rest": ^22.0.1 + "@percy/cli": ^1.31.1 + "@percy/ember": ^5.0.0 + "@playwright/test": ^1.54.0 + "@rollup/plugin-babel": ^6.0.4 + "@sentry/node": ^8.31.0 + "@simple-dom/interface": ^1.4.0 + "@simple-dom/parser": ^1.4.0 + "@simple-dom/serializer": ^1.4.0 + "@simple-dom/void-map": ^1.4.0 + "@sinonjs/fake-timers": ^11.2.2 + "@sqlite.org/sqlite-wasm": 3.45.1-build1 + "@tabler/icons": ^3.19.0 + "@types/archiver": ^7.0.0 + "@types/babel__core": ^7.1.19 + "@types/babel__generator": ^7.6.4 + "@types/babel__traverse": ^7.14.2 + "@types/diff": ^5.0.2 + "@types/dompurify": ^3.0.2 + "@types/eslint": 8.56.5 + "@types/flat": ^5.0.5 + "@types/fs-extra": ^11.0.4 + "@types/htmlbars-inline-precompile": ^3.0.3 + "@types/indefinite": ^2.3.4 + "@types/js-string-escape": ^1.0.1 + "@types/js-yaml": ^4.0.9 + "@types/jsdom": ^21.1.1 + "@types/jsonwebtoken": ^9.0.5 + "@types/koa": ^2.13.5 + "@types/koa-compose": ^3.2.5 + "@types/koa__cors": ^4.0.0 + "@types/koa__router": ^12.0.0 + "@types/line-column": ^1.0.0 + "@types/lodash": ^4.17.15 + "@types/matrix-js-sdk": ^11.0.1 + "@types/mime-types": ^2.1.1 + "@types/ms": ^2.1.0 + "@types/node": ^24.3.0 + "@types/pg": ^8.11.5 + "@types/pluralize": ^0.0.30 + "@types/qs": ^6.9.17 + "@types/qunit": ^2.19.12 + "@types/rsvp": ^4.0.9 + "@types/sane": ^2.0.1 + "@types/sinon": ^17.0.3 + "@types/sinonjs__fake-timers": ^8.1.5 + "@types/statuses": ^2.0.5 + "@types/stream-chain": ^2.0.1 + "@types/superagent": ^8.1.9 + "@types/stream-json": ^1.7.3 + "@types/string.prototype.matchall": ^4.0.1 + "@types/supertest": ^2.0.12 + "@types/tmp": ^0.2.3 + "@types/uuid": ^9.0.8 + "@types/yargs": ^17.0.10 + "@typescript-eslint/eslint-plugin": ^7.18.0 + "@typescript-eslint/parser": ^7.18.0 + "@universal-ember/test-support": ^0.5.1 + "@vscode/vsce": ^3.1.0 ajv: ^8.17.1 + archiver: ^7.0.0 awesome-phonenumber: ^7.2.0 babel-eslint: ^10.1.0 babel-plugin-dynamic-import-node: ^2.3.3