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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
126 lines · 4.3 KB Raw History
/**
 * The npm registry as a server extension: the platform passes the factory
 * to core's `extensions` option and core mounts the registry under /npm/
 * on its own fetch, and the packages listing the account page reads under
 * /account/api. The registry reads through the server and publishes
 * straight to the ports, the way createNpmRegistry is built by hand.
 */

import { cborDecode, toDisplayValue } from '@pdsjs/core/repo';
import { createNpmRegistry } from './http.js';
import { NPM_PACKAGE_COLLECTION } from './lexicon.js';

/**
 * @typedef {Object} PackageSummary
 * @property {string} name
 * @property {string} uri
 * @property {Array<{tag: string, version: string}>} distTags
 * @property {Array<{version: string, size: number, publishedAt: string|null}>} versions
 * @property {number} size - every tarball and manifest, in bytes
 * @property {string|null} createdAt
 * @property {string|null} updatedAt
 */

/**
 * One record summarized for the packages page. The registry stores sizes on
 * the blob references, so the summary reads the record alone.
 * @param {{uri: string, value: Uint8Array|ArrayBuffer}} record
 * @returns {PackageSummary}
 */
function summarizePackage(record) {
  const value =
    /** @type {{name?: unknown, distTags?: unknown, versions?: unknown, createdAt?: unknown, updatedAt?: unknown}} */ (
      toDisplayValue(
        cborDecode(
          record.value instanceof Uint8Array
            ? record.value
            : new Uint8Array(record.value),
        ),
      )
    );
  const name =
    typeof value.name === 'string'
      ? value.name
      : record.uri.split('/').pop() || '';
  const distTags = /** @type {Array<{tag: string, version: string}>} */ (
    /** @type {Array<{tag?: unknown, version?: unknown}>} */ (
      Array.isArray(value.distTags) ? value.distTags : []
    ).filter((t) => typeof t.tag === 'string' && typeof t.version === 'string')
  );
  const versions =
    /** @type {Array<{version: string, publishedAt?: unknown, tarball?: {size?: unknown}, manifest?: {size?: unknown}}>} */ (
      /** @type {Array<{version?: unknown}>} */ (
        Array.isArray(value.versions) ? value.versions : []
      ).filter((v) => typeof v.version === 'string')
    );
  let size = 0;
  for (const entry of versions) {
    if (typeof entry.tarball?.size === 'number') size += entry.tarball.size;
    if (typeof entry.manifest?.size === 'number') size += entry.manifest.size;
  }
  return {
    name,
    uri: record.uri,
    distTags,
    versions: versions.map((entry) => ({
      version: entry.version,
      size: typeof entry.tarball?.size === 'number' ? entry.tarball.size : 0,
      publishedAt:
        typeof entry.publishedAt === 'string' ? entry.publishedAt : null,
    })),
    size,
    createdAt: typeof value.createdAt === 'string' ? value.createdAt : null,
    updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : null,
  };
}

/**
 * @param {Object} [options]
 * @param {number} [options.maxTarballSize] - largest accepted tarball, in bytes
 * @param {string} [options.collection] - record collection, for lexicon forks
 * @returns {import('@pdsjs/core/ports').ExtensionFactory}
 */
export function createNpmExtension({
  maxTarballSize,
  collection = NPM_PACKAGE_COLLECTION,
} = {}) {
  return (host) => {
    const registry = createNpmRegistry({
      xrpc: host.xrpc,
      getDid: host.getDid,
      actorStorage: host.actorStorage,
      blobs: host.blobs,
      createCommit: host.createCommit,
      hasher: host.hasher,
      maxTarballSize,
      collection,
    });
    return {
      name: 'npm',
      collections: [collection],
      prefixes: ['/npm'],
      handle: (request) => registry.handle(request),
      features: { npm: true },
      stats: (count) => ({ npmPackages: count(collection) }),
      routes: {
        // The packages hosted in this account, for the account page.
        '/account/api/npm/packages': {
          method: 'GET',
          handler: (request, url) =>
            host.accountApi(request, url, async () => {
              const page = await host.actorStorage.listRecords(
                collection,
                null,
                100,
              );
              return {
                enabled: true,
                registryUrl: `${url.protocol}//${url.host}/npm/`,
                packages: page.records.map(summarizePackage),
              };
            }),
        },
      },
    };
  };
}