chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* 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),
};
}),
},
},
};
};
}