chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* End-to-end test of the npm registry with the stock npm client: packages
* are published with `npm publish`, read back with `npm view`, installed
* with `npm install`, and the tarball integrity survives the round trip.
*/
import { execFile } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { createServer } from '@pdsjs/node';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const execFileAsync = promisify(execFile);
const PORT = 2486;
const BASE = `http://127.0.0.1:${PORT}`;
const REGISTRY = `${BASE}/npm/`;
const PASSWORD = 'npm-test-password';
const HANDLE = 'npm.test';
/** @param {number} length */
function randomBase32(length) {
const chars = 'abcdefghijklmnopqrstuvwxyz234567';
return Array.from(randomBytes(length))
.map((b) => chars[b % 32])
.join('');
}
const DID = `did:plc:${randomBase32(24)}`;
/** @type {Awaited<ReturnType<typeof createServer>>|null} */
let server = null;
/** @type {string} */
let workDir = '';
/** @type {string} */
let npmrc = '';
// npm reads `npm_config_*` environment variables as a layer above the file
// `--userconfig` names. `pnpm run` exports npm_config_registry. Without this
// filter every publish goes to registry.npmjs.org and fails with ENEEDAUTH.
const NPM_ENV = Object.fromEntries(
Object.entries(process.env).filter(([key]) => !/^npm_config_/i.test(key)),
);
/**
* @param {string} cwd
* @param {string[]} args
*/
async function runNpm(cwd, args) {
const { stdout } = await execFileAsync(
'npm',
[...args, '--userconfig', npmrc, '--no-audit', '--no-fund'],
{ cwd, encoding: 'utf8', env: NPM_ENV },
);
return stdout;
}
/**
* @param {string} dir
* @param {{name: string, version: string, main?: string}} manifest
* @param {string} body - module source
*/
function writePackage(dir, manifest, body) {
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({ main: 'index.js', ...manifest }, null, 2),
);
writeFileSync(join(dir, 'index.js'), body);
}
beforeAll(async () => {
workDir = mkdtempSync(join(tmpdir(), 'pdsjs-npm-'));
server = await createServer({
port: PORT,
dbPath: join(workDir, 'pds.db'),
blobsDir: join(workDir, 'blobs'),
jwtSecret: 'test-secret-for-npm',
hostname: `127.0.0.1:${PORT}`,
password: PASSWORD,
experimental: { npm: true },
});
await server.listen();
const init = await fetch(`${BASE}/init?did=${DID}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
did: DID,
privateKey: randomBytes(32).toString('hex'),
handle: HANDLE,
password: PASSWORD,
}),
});
expect(init.ok).toBe(true);
// What `npm login` would store: registry plus the token, which for this
// registry is the password itself.
npmrc = join(workDir, 'npmrc');
writeFileSync(
npmrc,
[
`registry=${REGISTRY}`,
`//127.0.0.1:${PORT}/npm/:_authToken=${PASSWORD}`,
'',
].join('\n'),
);
}, 60_000);
afterAll(async () => {
if (server) await server.close();
rmSync(workDir, { recursive: true, force: true });
});
describe('npm registry e2e', () => {
it('logs in the way npm adduser does', async () => {
const res = await fetch(`${REGISTRY}-/user/org.couchdb.user:${HANDLE}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: HANDLE, password: PASSWORD }),
});
expect(res.status).toBe(201);
const body = await res.json();
expect(body.token).toBe(PASSWORD);
const bad = await fetch(`${REGISTRY}-/user/org.couchdb.user:${HANDLE}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: HANDLE, password: 'wrong' }),
});
expect(bad.status).toBe(401);
});
it('answers whoami for the token', async () => {
const res = await fetch(`${REGISTRY}-/whoami`, {
headers: { Authorization: `Bearer ${PASSWORD}` },
});
expect(res.ok).toBe(true);
expect((await res.json()).username).toBe(HANDLE);
});
it('publishes a package with the npm CLI', async () => {
const dir = join(workDir, 'demo-pkg');
writePackage(
dir,
{ name: 'demo-pkg', version: '1.0.0' },
"module.exports = 'served from an atproto repo';\n",
);
const out = await runNpm(dir, ['publish']);
expect(out).toContain('demo-pkg@1.0.0');
});
it('refuses a republish of the same version', async () => {
const dir = join(workDir, 'demo-pkg');
await expect(runNpm(dir, ['publish'])).rejects.toThrow(
/cannot publish over/i,
);
});
it('answers npm view from the record', async () => {
const version = await runNpm(workDir, ['view', 'demo-pkg', 'version']);
expect(version.trim()).toBe('1.0.0');
});
it('installs the package with the npm CLI and runs it', async () => {
const consumer = join(workDir, 'consumer');
writePackage(
consumer,
{ name: 'consumer', version: '0.0.0' },
"console.log(require('demo-pkg'));\n",
);
await runNpm(consumer, ['install', 'demo-pkg']);
const { stdout } = await execFileAsync('node', ['index.js'], {
cwd: consumer,
encoding: 'utf8',
});
expect(stdout.trim()).toBe('served from an atproto repo');
});
it('publishes and installs a scoped package', async () => {
const dir = join(workDir, 'scoped-pkg');
writePackage(
dir,
{ name: '@chad/util', version: '2.0.0' },
'module.exports = { scoped: true };\n',
);
await runNpm(dir, ['publish', '--access', 'public']);
const consumer = join(workDir, 'scoped-consumer');
writePackage(
consumer,
{ name: 'scoped-consumer', version: '0.0.0' },
"console.log(JSON.stringify(require('@chad/util')));\n",
);
await runNpm(consumer, ['install', '@chad/util']);
const { stdout } = await execFileAsync('node', ['index.js'], {
cwd: consumer,
encoding: 'utf8',
});
expect(JSON.parse(stdout)).toEqual({ scoped: true });
});
it('serves dist-tags and moves them with the npm CLI', async () => {
const dir = join(workDir, 'demo-pkg');
writePackage(
dir,
{ name: 'demo-pkg', version: '1.1.0' },
"module.exports = 'next';\n",
);
await runNpm(dir, ['publish', '--tag', 'next']);
const tags = await runNpm(workDir, ['dist-tag', 'ls', 'demo-pkg']);
expect(tags).toContain('latest: 1.0.0');
expect(tags).toContain('next: 1.1.0');
await runNpm(workDir, ['dist-tag', 'add', 'demo-pkg@1.1.0', 'stable']);
const after = await runNpm(workDir, ['dist-tag', 'ls', 'demo-pkg']);
expect(after).toContain('stable: 1.1.0');
});
it('holds every version in the package record', async () => {
const params = new URLSearchParams({
repo: DID,
collection: 'dev.pdsjs.npm.package',
rkey: 'demo-pkg',
});
const res = await fetch(
`${BASE}/xrpc/com.atproto.repo.getRecord?${params}`,
);
expect(res.ok).toBe(true);
const body = await res.json();
expect(body.value.name).toBe('demo-pkg');
// Two versions, each holding a tarball and a manifest blob against
// orphan cleanup.
expect(body.value.versions).toHaveLength(2);
for (const version of body.value.versions) {
expect(version.integrity).toMatch(/^sha512-/);
expect(version.tarball.ref.$link).toMatch(/^b/);
expect(version.manifest.ref.$link).toMatch(/^b/);
}
const scoped = new URLSearchParams({
repo: DID,
collection: 'dev.pdsjs.npm.package',
rkey: 'chad:util',
});
const scopedRes = await fetch(
`${BASE}/xrpc/com.atproto.repo.getRecord?${scoped}`,
);
expect(scopedRes.ok).toBe(true);
expect((await scopedRes.json()).value.name).toBe('@chad/util');
});
it('serves the packument with a strong ETag', async () => {
const first = await fetch(`${REGISTRY}demo-pkg`);
expect(first.ok).toBe(true);
const etag = first.headers.get('etag');
expect(etag).toBeTruthy();
const packument = await first.json();
expect(Object.keys(packument.versions).sort()).toEqual(['1.0.0', '1.1.0']);
expect(packument.versions['1.0.0'].dist.tarball).toBe(
`${BASE}/npm/demo-pkg/-/demo-pkg-1.0.0.tgz`,
);
const cached = await fetch(`${REGISTRY}demo-pkg`, {
headers: { 'If-None-Match': /** @type {string} */ (etag) },
});
expect(cached.status).toBe(304);
});
it('refuses an unauthenticated publish', async () => {
const res = await fetch(`${REGISTRY}sneaky`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'sneaky', versions: {} }),
});
expect(res.status).toBe(401);
});
it('answers 404 for an unknown package', async () => {
const res = await fetch(`${REGISTRY}absent`);
expect(res.status).toBe(404);
});
it('serves a scoped packument at the encoded URL npm sends', async () => {
const res = await fetch(`${REGISTRY}@chad%2futil`);
expect(res.ok).toBe(true);
const packument = await res.json();
expect(packument.name).toBe('@chad/util');
expect(packument.versions['2.0.0'].dist.tarball).toBe(
`${BASE}/npm/@chad/util/-/util-2.0.0.tgz`,
);
});
it('answers ping and refuses what it does not serve', async () => {
expect((await fetch(`${REGISTRY}-/ping`)).status).toBe(200);
expect((await fetch(`${REGISTRY}UPPER`)).status).toBe(400);
expect((await fetch(`${REGISTRY}-/v1/search?text=x`)).status).toBe(404);
expect((await fetch(`${REGISTRY}demo-pkg/-/other-1.0.0.tgz`)).status).toBe(
404,
);
expect(
(await fetch(`${REGISTRY}demo-pkg/-/demo-pkg-9.9.9.tgz`)).status,
).toBe(404);
const unpublish = await fetch(`${REGISTRY}demo-pkg`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${PASSWORD}` },
});
expect(unpublish.status).toBe(403);
});
it('removes a dist-tag over the wire', async () => {
const res = await fetch(`${REGISTRY}-/package/demo-pkg/dist-tags/stable`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${PASSWORD}` },
});
expect(res.ok).toBe(true);
const tags = await fetch(`${REGISTRY}-/package/demo-pkg/dist-tags`);
const body = await tags.json();
expect(body.stable).toBeUndefined();
expect(body.latest).toBe('1.0.0');
const missing = await fetch(
`${REGISTRY}-/package/demo-pkg/dist-tags/gone`,
{ method: 'DELETE', headers: { Authorization: `Bearer ${PASSWORD}` } },
);
expect(missing.status).toBe(404);
});
it('summarizes hosted packages for the account page', async () => {
const signIn = await fetch(`${BASE}/account/sign-in`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ username: DID, password: PASSWORD }),
redirect: 'manual',
});
const cookie = (signIn.headers.get('set-cookie') || '').split(';')[0];
expect(cookie).toContain('pdsjs_account=');
const overview = await fetch(`${BASE}/account/api/overview`, {
headers: { Cookie: cookie },
});
const account = await overview.json();
expect(account.features.npm).toBe(true);
expect(account.stats.npmPackages).toBeGreaterThanOrEqual(2);
const res = await fetch(`${BASE}/account/api/npm/packages`, {
headers: { Cookie: cookie },
});
expect(res.ok).toBe(true);
const body = await res.json();
expect(body.enabled).toBe(true);
expect(body.registryUrl).toBe(`${BASE}/npm/`);
const demo = body.packages.find(
(/** @type {{name: string}} */ p) => p.name === 'demo-pkg',
);
expect(
demo.versions.map((/** @type {{version: string}} */ v) => v.version),
).toEqual(['1.0.0', '1.1.0']);
expect(demo.distTags).toContainEqual({ tag: 'latest', version: '1.0.0' });
expect(demo.size).toBeGreaterThan(0);
const scoped = body.packages.find(
(/** @type {{name: string}} */ p) => p.name === '@chad/util',
);
expect(scoped.versions).toHaveLength(1);
const anonymous = await fetch(`${BASE}/account/api/npm/packages`);
expect(anonymous.status).toBe(401);
});
it('refuses a publish whose body lies about the tarball', async () => {
const manifest = {
name: 'liar',
version: '1.0.0',
dist: { integrity: `sha512-${'A'.repeat(86)}==` },
};
const res = await fetch(`${REGISTRY}liar`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${PASSWORD}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'liar',
versions: { '1.0.0': manifest },
'dist-tags': { latest: '1.0.0' },
_attachments: {
'liar-1.0.0.tgz': {
data: Buffer.from('not the declared bytes').toString('base64'),
},
},
}),
});
expect(res.status).toBe(400);
expect((await res.json()).error).toMatch(/hashes to/);
});
});