// @pdsjs/sites - the account page's read-only view of deployed sites // // Fulfills core's SiteBrowserPort. Everything comes from the deploy records // alone: sizes are summed from blob-ref metadata, so listing sites never // reads a blob. import { cborDecode } from '@pdsjs/core/repo'; import { isValidSiteName } from './hostname.js'; import { SITE_COLLECTION } from './lexicons.js'; import { loadSite } from './manifest.js'; /** Upper bound on sites listed; far above anything a subdomain-per-site * scheme can use in practice. */ const SITE_LIMIT = 500; /** * @param {Object} ctx * @param {import('@pdsjs/core/ports').ActorStoragePort} ctx.actorStorage * @param {() => Promise} ctx.getDid - the hosted account's DID * @param {string} ctx.siteDomain - apex domain sites serve under * @param {string} [ctx.apexName] - site name bound to the apex (default "home") * @returns {import('./ports.js').SiteBrowserPort} */ export function createSiteBrowser(ctx) { const { actorStorage, getDid, siteDomain } = ctx; if (!actorStorage) throw new Error('createSiteBrowser requires actorStorage'); if (!getDid) throw new Error('createSiteBrowser requires getDid'); if (!siteDomain) throw new Error('createSiteBrowser requires siteDomain'); const apexName = ctx.apexName || 'home'; return { domain: siteDomain, apexName, async listSites() { /** @type {import('./ports.js').SiteRow[]} */ const rows = []; /** @type {string|null} */ let cursor = null; while (rows.length < SITE_LIMIT) { const page = await actorStorage.listRecords( SITE_COLLECTION, cursor, 100, ); for (const record of page.records) { const name = record.uri.split('/').pop() || ''; const value = /** @type {{files?: unknown, createdAt?: unknown}} */ ( cborDecode( record.value instanceof Uint8Array ? record.value : new Uint8Array(record.value), ) ); const files = Array.isArray(value.files) ? value.files : []; let totalSize = 0; for (const entry of files) { const size = /** @type {{blob?: {size?: unknown}}} */ (entry)?.blob ?.size; if (typeof size === 'number') totalSize += size; } rows.push({ name, url: name === apexName ? `https://${siteDomain}` : `https://${name}.${siteDomain}`, fileCount: files.length, totalSize, deployedAt: typeof value.createdAt === 'string' ? value.createdAt : null, cid: record.cid, servable: name === apexName || isValidSiteName(name), }); } if (!page.cursor) break; cursor = page.cursor; } return rows; }, async listFiles(name) { const did = await getDid(); if (!did) return null; // The served view of the site, base record folded in, so the list // matches what the hostname answers path for path. const manifest = await loadSite(actorStorage, did, name); if (!manifest) return null; return [...manifest.files.entries()] .map(([path, file]) => ({ path, size: file.size, contentType: file.contentType || file.mimeType, cid: file.cid, })) .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)); }, }; }