chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @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<string|null>} 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));
},
};
}