~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
292 lines · 9.5 KB Raw History
// @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,
];