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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
130 lines · 4.4 KB Raw History
/**
 * The appview's ExtensionHost, over a remote account and the local index.
 *
 * Everything `createGitExtension` consumes is here: record reads land on
 * the PDS over the wire, blob reads stream from it, discovery answers from
 * the jetstream index, and the write guard arrives once the extension has
 * been built — the same late binding core's host does, because the guard is
 * the extension's own creation.
 */

import { createIndexedDiscovery } from '@pdsjs/appview/discovery';
import { createGitExtension } from '@pdsjs/git/extension';

/** The most a JSON request body may weigh, matching core's own bound. */
const JSON_BODY_LIMIT = 1_000_000;

/**
 * @param {Request} request
 * @returns {Promise<unknown>} the parsed body, or a 413 Response when it
 *   is too large; the caller tells the two apart
 */
async function readJsonBounded(request) {
  const text = await request.text();
  if (new TextEncoder().encode(text).byteLength > JSON_BODY_LIMIT) {
    return Response.json(
      {
        error: 'PayloadTooLarge',
        message: `Request body exceeds ${JSON_BODY_LIMIT} bytes`,
      },
      { status: 413 },
    );
  }
  return JSON.parse(text);
}

/**
 * @param {Object} options
 * @param {import('./remote.js').RemoteAccount} options.account - the PDS
 *   reached over the wire
 * @param {import('@pdsjs/appview/store').AppViewStore} options.store - the
 *   record index discovery reads from
 * @param {string} [options.hostname] - this host's own name, for client
 *   strings
 * @param {string} [options.plcUrl] - the directory DIDs resolve against
 * @param {number} [options.blobUploadLimit] - the PDS's blob limit, which
 *   sizes merge uploads
 * @param {(request: Request) => Promise<string|null>} [options.sessionDid] -
 *   the account session a request carries, for the owner-only procedures
 * @returns {{host: unknown, extension: import('@pdsjs/core/ports').PdsExtension}}
 */
export function createGitAppViewHost(options) {
  const { account, store } = options;

  /** @type {import('@pdsjs/core/ports').PdsExtension|null} */
  let mounted = null;

  /**
   * The record storage git writes through, over the wire. The members the
   * git package never calls are the ones a remote host cannot serve: the
   * PDS keeps its own blocks and event log.
   */
  const actorStorage = {
    getRecord: (/** @type {string} */ uri) => account.getRecord(uri),
    listRecords: (
      /** @type {string} */ collection,
      /** @type {string|null} */ cursor,
      /** @type {number} */ limit,
    ) => account.listRecords(collection, cursor, limit),
    // The PDS re-encodes and commits what putRecord hands it; a separate
    // block write has no remote meaning.
    putBlock: async () => {},
    putRecord: async (
      /** @type {string} */ uri,
      /** @type {string} */ _cid,
      /** @type {string} */ collection,
      /** @type {string} */ rkey,
      /** @type {Uint8Array} */ value,
    ) => account.putRecord(uri, collection, rkey, value, null),
    // Blobs the record names are linked by the PDS's own write path.
    linkBlobToRecord: async () => {},
    getBlob: async () => null,
    putBlob: async () => {},
    getHandle: () => account.getHandle(),
  };

  const blobs = {
    get: (/** @type {string} */ did, /** @type {string} */ cid) =>
      account.getBlob(did, cid),
    getStream: (
      /** @type {string} */ did,
      /** @type {string} */ cid,
      /** @type {{start: number, end: number}|undefined} */ range,
    ) => account.getBlobStream(did, cid, range),
    put: async (
      /** @type {string} */ _did,
      /** @type {Uint8Array} */ data,
      /** @type {string} */ mimeType,
    ) => account.uploadBlob(data, mimeType),
    delete: async () => {},
  };

  const host = {
    actorStorage,
    blobs,
    get writeGuard() {
      return mounted?.writeGuard;
    },
    getDid: account.getDid,
    xrpc: account.xrpc,
    hostname: options.hostname,
    plcUrl: options.plcUrl,
    blobUploadLimit: options.blobUploadLimit ?? 5_242_880,
    readOnly: false,
    readAccountSession: async (/** @type {Request} */ request) =>
      options.sessionDid ? options.sessionDid(request) : null,
    readJsonBounded,
  };

  mounted = createGitExtension({
    http: true,
    browse: true,
    discovery: createIndexedDiscovery(store),
  })(
    /** @type {import('@pdsjs/core/ports').ExtensionHost} */ (
      /** @type {unknown} */ (host)
    ),
  );

  return { host, extension: mounted };
}