chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @pdsjs/sites - lexicon schemas for the app platform
//
// Record validation fails open for unresolvable collections, so these gate
// nothing at write time. They document the shapes, drive the CLI, and can be
// bundled into a resolver or published later. @pdsjs/sites has no dependency
// on a lexicon library, so the type is structural.
/**
* A deployed static site. The record key is the site name: a lowercase DNS
* label that binds the site to `<rkey>.<siteDomain>`; the deployment's
* configured apex name binds to the apex itself.
*/
export const devPdsjsSiteDeploy = {
lexicon: 1,
id: 'dev.pdsjs.site.deploy',
defs: {
main: {
type: 'record',
description:
'A static site served from this repo. The record key is the site name, a lowercase DNS label.',
key: 'any',
record: {
type: 'object',
required: ['files', 'createdAt'],
properties: {
files: {
type: 'array',
description:
'Site contents. Paths are unique, relative, slash-separated, with no leading slash.',
items: { type: 'ref', ref: '#file' },
},
fallback: {
type: 'string',
description:
'Path served with status 200 for any request path with no match (single-page-app routing). Must resolve within files, possibly via base.',
},
base: {
type: 'ref',
ref: 'com.atproto.repo.strongRef',
description:
"Another dev.pdsjs.site.deploy record in this repo whose files merge beneath this record's; this record wins on path conflicts. One hop only: a base may not itself declare a base.",
},
createdAt: { type: 'string', format: 'datetime' },
},
},
},
file: {
type: 'object',
required: ['path', 'blob'],
properties: {
path: { type: 'string', maxLength: 1024 },
blob: { type: 'blob', maxSize: 5242880 },
contentType: {
type: 'string',
description:
'Overrides the stored blob mime type when serving this path.',
},
cacheControl: {
type: 'string',
description:
'Overrides the default Cache-Control header for this path.',
},
originTemplate: {
type: 'boolean',
description:
'Serve this file with the token __PDSJS_ORIGIN__ replaced by the origin the request arrived on, for a file whose content names the site it is served from, such as an OAuth client registration.',
},
},
},
},
};
/**
* An installable app, published in its developer's repo. Installing copies
* the referenced site's blobs into the installer's own repo, so the app
* keeps working if the developer's repo disappears.
*/
export const devPdsjsAppManifest = {
lexicon: 1,
id: 'dev.pdsjs.app.manifest',
defs: {
main: {
type: 'record',
key: 'any',
record: {
type: 'object',
required: ['name', 'createdAt'],
properties: {
name: { type: 'string', maxLength: 64 },
description: { type: 'string', maxLength: 1024 },
icon: { type: 'blob', accept: ['image/*'], maxSize: 262144 },
site: {
type: 'ref',
ref: 'com.atproto.repo.strongRef',
description:
"The dev.pdsjs.site.deploy record in this repo holding the app's frontend. Its record key is an artifact address with no naming meaning for installs.",
},
suggestedSite: {
type: 'string',
description:
'Default subdomain label offered at install. The installer never uses the pinned record key for naming.',
},
scopes: {
type: 'array',
items: { type: 'string' },
description:
'OAuth scopes the app requests at install, in the host PDS scope grammar.',
},
primitives: {
type: 'array',
items: {
type: 'string',
enum: ['serve', 'grant', 'query', 'react', 'schedule'],
},
},
external: {
type: 'boolean',
description:
"True when the app talks to services other than the user's own PDS; surfaced prominently on the install sheet.",
},
queries: {
type: 'unknown',
description:
'Query definitions the app reads from its host: an array of {name, collection, where?, sort?}. The installer writes each as a dev.pdsjs.query.def record.',
},
settings: {
type: 'unknown',
description:
'Settings the app exposes: an array of {key, label, type: "boolean"|"string"|"select", default?, options?}. Declared data, never code; the install record snapshots this schema with the chosen values.',
},
createdAt: { type: 'string', format: 'datetime' },
},
},
},
},
};
/**
* An installed app, written to the installing user's repo. The manifest ref
* pins the reviewed version by CID; an updated manifest requires re-consent.
*/
export const devPdsjsAppInstall = {
lexicon: 1,
id: 'dev.pdsjs.app.install',
defs: {
main: {
type: 'record',
key: 'tid',
record: {
type: 'object',
required: ['manifest', 'createdAt'],
properties: {
name: {
type: 'string',
maxLength: 64,
description:
"The manifest's display name at install, snapshotted like the settings schema.",
},
manifest: {
type: 'ref',
ref: 'com.atproto.repo.strongRef',
description:
"The developer's dev.pdsjs.app.manifest record, pinned by CID.",
},
grantedScopes: { type: 'array', items: { type: 'string' } },
site: {
type: 'string',
description:
"Local site name: the dev.pdsjs.site.deploy rkey holding this install's copied frontend.",
},
config: {
type: 'unknown',
description:
'Chosen values for the settings the manifest declares.',
},
settings: {
type: 'unknown',
description:
"Snapshot of the manifest's settings schema at install, so a settings editor needs no cross-server read.",
},
icon: {
type: 'blob',
accept: ['image/*'],
maxSize: 262144,
description:
"The manifest's icon, copied into this repo at install.",
},
createdAt: { type: 'string', format: 'datetime' },
},
},
},
},
};
/**
* A palette an account publishes for the pages it serves. The record key
* names the theme, so an account may hold several and a reader may pick
* between them.
*
* The colours are the semantic names a page paints with, not the elements
* they paint: background, foreground, key, destructive and so on. A page
* that knows a name it is not given falls back to its own.
*/
export const devPdsjsAppTheme = {
lexicon: 1,
id: 'dev.pdsjs.app.theme',
defs: {
main: {
type: 'record',
key: 'any',
record: {
type: 'object',
required: ['name', 'scheme', 'colors'],
properties: {
name: {
type: 'string',
maxLength: 64,
description: 'What to call this palette in a theme picker.',
},
scheme: {
type: 'string',
knownValues: ['light', 'dark'],
description:
'Which way round the palette runs. A page tells the browser, so scrollbars and form controls match.',
},
colors: {
type: 'unknown',
description:
'Colour per semantic name. A name the page does not know is ignored, and a name left out keeps the page default.',
},
createdAt: { type: 'string', format: 'datetime' },
},
},
},
},
};
/**
* A named live view over one collection of this repo. The record key names
* the query; dev.pdsjs.query.run serves its rows.
*/
export const devPdsjsQueryDef = {
lexicon: 1,
id: 'dev.pdsjs.query.def',
defs: {
main: {
type: 'record',
key: 'any',
record: {
type: 'object',
required: ['collection', 'createdAt'],
properties: {
collection: {
type: 'string',
format: 'nsid',
description: 'The collection this query reads.',
},
where: {
type: 'unknown',
description:
'Field path to required value, equality only. A record matches when every named field equals its value.',
},
sort: {
type: 'object',
properties: {
field: { type: 'string', description: 'Field path to sort by.' },
direction: { type: 'string', enum: ['asc', 'desc'] },
},
},
createdAt: { type: 'string', format: 'datetime' },
},
},
},
},
};
/** The collection a site manifest lives in. */
export const SITE_COLLECTION = 'dev.pdsjs.site.deploy';
/** The collection query definitions live in. */
export const QUERY_COLLECTION = 'dev.pdsjs.query.def';
/** The collection published palettes live in. */
export const THEME_COLLECTION = 'dev.pdsjs.app.theme';
export const siteLexicons = [
devPdsjsSiteDeploy,
devPdsjsAppManifest,
devPdsjsAppInstall,
devPdsjsAppTheme,
devPdsjsQueryDef,
];