chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* The /account/api/sites and /account/api/installs routes: the deployed
* sites, and the apps installed from manifests, each behind the owner's
* account session. The install listing and its settings need no site
* domain; previewing, installing and renaming need the installer, which
* does.
*
* This module must stay free of node-only imports; it runs on Cloudflare
* Workers.
*/
import {
cborDecode,
cborEncodeDagCbor,
cidToString,
createCid,
toDisplayValue,
} from '@pdsjs/core/repo';
/**
* @param {Object} ctx
* @param {import('@pdsjs/core/ports').ExtensionHost} ctx.host
* @param {import('./ports.js').SiteBrowserPort|null} ctx.browser - Absent, the sites listing reports the feature off and the site routes refuse.
* @param {import('./ports.js').SiteInstallerPort|null} ctx.installer - Absent, previewing, installing and renaming report installs off.
* @returns {Record<string, import('@pdsjs/core/ports').ExtensionRoute>}
*/
export function createSiteAccountRoutes({ host, browser, installer }) {
const {
accountApi,
actorStorage,
createCommit,
hostname,
reapOrphanedBlobs,
} = host;
/**
* GET /account/api/sites - The deployed sites, each marked as an app
* install's frontend or a published app's artifact where it is one.
* @param {Request} request @param {URL} url
*/
async function handleApiSites(request, url) {
return accountApi(request, url, async (did) => {
if (!browser) return { enabled: false, sites: [] };
// A site can be an app install's frontend, or the artifact a local
// manifest publishes. The page shows installs in their own section
// and marks artifacts, so the roles need naming here.
const installedNames = new Set();
for (const record of (
await actorStorage.listRecords('dev.pdsjs.app.install', null, 100)
).records) {
const value = /** @type {{site?: unknown}} */ (
cborDecode(
record.value instanceof Uint8Array
? record.value
: new Uint8Array(record.value),
)
);
if (typeof value.site === 'string') installedNames.add(value.site);
}
/** @type {Map<string, string>} */
const publishedBy = new Map();
for (const record of (
await actorStorage.listRecords('dev.pdsjs.app.manifest', null, 100)
).records) {
const value = /** @type {{site?: {uri?: unknown}}} */ (
toDisplayValue(
cborDecode(
record.value instanceof Uint8Array
? record.value
: new Uint8Array(record.value),
),
)
);
if (typeof value.site?.uri === 'string') {
publishedBy.set(value.site.uri, record.uri);
}
}
const sites = (await browser.listSites()).map((site) => {
const manifestUri = publishedBy.get(
`at://${did}/dev.pdsjs.site.deploy/${site.name}`,
);
return {
...site,
installed: installedNames.has(site.name),
publishedApp: Boolean(manifestUri),
manifestUri: manifestUri || null,
};
});
return {
enabled: true,
domain: browser.domain,
apexName: browser.apexName,
// The PDS answering at the site apex shadows the apex site; the
// sites page warns when it detects this.
apexShadowed:
(hostname || '').split(':')[0].toLowerCase() ===
browser.domain.toLowerCase(),
sites,
};
});
}
/**
* GET /account/api/installs - Installed apps, from dev.pdsjs.app.install
* records: the site each serves as, the manifest it pins, and the
* settings schema snapshot with current values.
* @param {Request} request @param {URL} url
*/
async function handleApiInstalls(request, url) {
return accountApi(request, url, async () => {
const page = await actorStorage.listRecords(
'dev.pdsjs.app.install',
null,
100,
);
const installs = [];
for (const record of page.records) {
const value = /** @type {Record<string, unknown>} */ (
cborDecode(
record.value instanceof Uint8Array
? record.value
: new Uint8Array(record.value),
)
);
const iconRef = /** @type {{ref?: {$link?: string}}|undefined} */ (
value.icon
);
const iconCid =
iconRef?.ref instanceof Uint8Array
? cidToString(iconRef.ref)
: typeof iconRef?.ref?.$link === 'string'
? iconRef.ref.$link
: null;
installs.push({
uri: record.uri,
name: typeof value.name === 'string' ? value.name : null,
site: typeof value.site === 'string' ? value.site : null,
iconUrl: iconCid
? `/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(record.uri.split('/')[2])}&cid=${iconCid}`
: null,
manifest: toDisplayValue(value.manifest),
settings: Array.isArray(value.settings) ? value.settings : [],
config:
value.config && typeof value.config === 'object'
? value.config
: {},
createdAt:
typeof value.createdAt === 'string' ? value.createdAt : null,
});
}
return { installs };
});
}
/**
* POST /account/api/installs/preview - The install sheet's view of a
* manifest at-uri: name, author, icon, settings, what it will copy.
* @param {Request} request @param {URL} url
*/
async function handleApiInstallPreview(request, url) {
return accountApi(request, url, async (_did, body) => {
if (!installer) throw new Error('Installs are not enabled.');
const uri = typeof body?.uri === 'string' ? body.uri.trim() : '';
if (!uri) throw new Error('Name the app to preview.');
return installer.preview(uri);
});
}
/**
* POST /account/api/installs/install - Install or update an app from its
* manifest at-uri. The installer copies blobs with local hashing and
* writes every record in one commit.
* @param {Request} request @param {URL} url
*/
async function handleApiInstallRun(request, url) {
return accountApi(
request,
url,
async (_did, body) => {
if (!installer) throw new Error('Installs are not enabled.');
const uri = typeof body?.uri === 'string' ? body.uri.trim() : '';
if (!uri) throw new Error('Name the app to install.');
return installer.install(uri, {
site: typeof body?.site === 'string' ? body.site : undefined,
config:
body?.config && typeof body.config === 'object'
? /** @type {Record<string, unknown>} */ (body.config)
: undefined,
});
},
{ write: true },
);
}
/**
* POST /account/api/installs/rename - Move an install's site to another
* subdomain label. The site record moves byte for byte, so its CID and
* the manifest pin are untouched.
* @param {Request} request @param {URL} url
*/
async function handleApiInstallRename(request, url) {
return accountApi(
request,
url,
async (did, body) => {
if (!installer) throw new Error('Installs are not enabled.');
const uri = typeof body?.uri === 'string' ? body.uri : '';
const site = typeof body?.site === 'string' ? body.site.trim() : '';
if (!uri.startsWith(`at://${did}/dev.pdsjs.app.install/`)) {
throw new Error('Name the install to rename.');
}
if (!site) throw new Error('Name the new subdomain.');
return installer.rename(uri, site);
},
{ write: true },
);
}
/**
* GET /account/api/installs/updates - For each install, whether the
* developer's manifest moved past the pinned version. An unreachable
* developer is omitted rather than failing the check for the rest.
* @param {Request} request @param {URL} url
*/
async function handleApiInstallUpdates(request, url) {
return accountApi(request, url, async () => {
if (!installer) return { updates: {} };
const page = await actorStorage.listRecords(
'dev.pdsjs.app.install',
null,
100,
);
/** @type {Record<string, {changed: boolean, currentCid: string}>} */
const updates = {};
await Promise.all(
page.records.map(async (record) => {
const value =
/** @type {{manifest?: {uri?: string, cid?: string}}} */ (
toDisplayValue(
cborDecode(
record.value instanceof Uint8Array
? record.value
: new Uint8Array(record.value),
),
)
);
const uri = value.manifest?.uri;
const cid = value.manifest?.cid;
if (typeof uri !== 'string' || typeof cid !== 'string') return;
try {
updates[record.uri] = await installer.checkUpdate(uri, cid);
} catch {}
}),
);
return { updates };
});
}
/**
* POST /account/api/installs/config - Replace an install's chosen
* settings. Values are checked against the record's own schema snapshot:
* only declared keys, each of its declared type.
* @param {Request} request @param {URL} url
*/
async function handleApiInstallConfig(request, url) {
return accountApi(
request,
url,
async (did, body) => {
const uri = typeof body?.uri === 'string' ? body.uri : '';
const config = body?.config;
if (!uri.startsWith(`at://${did}/dev.pdsjs.app.install/`)) {
throw new Error('Name the install to configure.');
}
if (!config || typeof config !== 'object' || Array.isArray(config)) {
throw new Error('Send the chosen settings as an object.');
}
const stored = await actorStorage.getRecord(uri);
if (!stored) throw new Error('No such install.');
const value = /** @type {Record<string, unknown>} */ (
cborDecode(
stored.value instanceof Uint8Array
? stored.value
: new Uint8Array(stored.value),
)
);
const schema = Array.isArray(value.settings) ? value.settings : [];
for (const [key, chosen] of Object.entries(config)) {
const field =
/** @type {{key?: string, type?: string, options?: unknown[]}|undefined} */ (
schema.find((f) => /** @type {{key?: string}} */ (f)?.key === key)
);
if (!field) throw new Error(`No such setting: ${key}`);
if (field.type === 'boolean' && typeof chosen !== 'boolean') {
throw new Error(`Setting ${key} takes true or false.`);
}
if (field.type === 'string' && typeof chosen !== 'string') {
throw new Error(`Setting ${key} takes a string.`);
}
if (
field.type === 'select' &&
!(Array.isArray(field.options) && field.options.includes(chosen))
) {
throw new Error(`Setting ${key} is not one of its options.`);
}
}
const updated = { ...value, config };
const encoded = cborEncodeDagCbor(updated);
const cid = cidToString(await createCid(encoded));
const rkey = uri.split('/').pop() || '';
await actorStorage.putBlock(cid, encoded);
await actorStorage.putRecord(
uri,
cid,
'dev.pdsjs.app.install',
rkey,
encoded,
);
await createCommit(did, [{ action: 'update', uri, cid }]);
return {};
},
{ write: true },
);
}
/** @param {Request} request @param {URL} url */
async function handleApiSiteFiles(request, url) {
return accountApi(request, url, async () => {
const name = url.searchParams.get('name') || '';
if (!name) throw new Error('Name the site to list.');
if (!browser) throw new Error('Sites are not enabled.');
const files = await browser.listFiles(name);
if (!files) throw new Error('No such site.');
return { files };
});
}
/** @param {Request} request @param {URL} url */
async function handleApiSiteDelete(request, url) {
return accountApi(
request,
url,
async (did, body) => {
const name = typeof body?.name === 'string' ? body.name : '';
if (!name) throw new Error('Name the site to delete.');
if (!browser) throw new Error('Sites are not enabled.');
const uri = `at://${did}/dev.pdsjs.site.deploy/${name}`;
if (!(await actorStorage.getRecord(uri))) {
throw new Error('No such site.');
}
// The same sequence deleteRecord runs: unlink, delete, commit, then
// reap this site's now-orphaned blobs (guarded against a stale link
// table, so blobs other records reference survive).
await actorStorage.unlinkBlobsFromRecord(uri);
await actorStorage.deleteRecord(uri);
await createCommit(did, [{ action: 'delete', uri }]);
await reapOrphanedBlobs(did, Date.now() + 1000);
return {};
},
{ write: true },
);
}
return {
'/account/api/sites': {
method: 'GET',
handler: handleApiSites,
},
'/account/api/installs': {
method: 'GET',
handler: handleApiInstalls,
},
'/account/api/installs/config': {
method: 'POST',
handler: handleApiInstallConfig,
},
'/account/api/installs/preview': {
method: 'POST',
handler: handleApiInstallPreview,
},
'/account/api/installs/install': {
method: 'POST',
handler: handleApiInstallRun,
},
'/account/api/installs/updates': {
method: 'GET',
handler: handleApiInstallUpdates,
},
'/account/api/installs/rename': {
method: 'POST',
handler: handleApiInstallRename,
},
'/account/api/sites/files': {
method: 'GET',
handler: handleApiSiteFiles,
},
'/account/api/sites/delete': {
method: 'POST',
handler: handleApiSiteDelete,
},
};
}