chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The dev.pdsjs.git XRPC methods, driven through pds.fetch against fake
// storage: anonymous reads of the public repo, the account session for
// spaces and writes, bearer tokens checked against repo scopes.
import { cborEncodeDagCbor } from '@pdsjs/core/repo';
import { describe, expect, it } from 'vitest';
import { createAccessJwt } from '../../../packages/core/src/auth.js';
import {
apiGet,
BASE,
createActorStorage,
createPds as createBasePds,
DID,
SECRET,
sessionCookie,
} from '../../../test/helpers/account-api-harness.js';
import { createFakeSpaceStorage } from '../../../test/helpers/fake-space-storage.js';
import {
GIT_DEFS,
GIT_METHODS,
gitDefsLexicon,
gitMethodLexicons,
} from '../src/api-lexicon.js';
import { createGitHosting } from '../src/hosting.js';
import { createGitMethods } from '../src/methods.js';
const SPACE = `at://${DID}/space/com.example.team/main`;
/** The space a private repository named proj keeps. */
/** The space a private repository lives in, keyed by its name. */
const ownSpace = (/** @type {string} */ rkey) =>
`at://${DID}/space/dev.pdsjs.git.space/${rkey}`;
const SHA = 'a'.repeat(40);
const REPO_RECORD = {
$type: 'dev.pdsjs.git.repo',
name: 'myrepo',
defaultBranch: 'main',
refs: [{ name: 'refs/heads/main', sha: SHA }],
bundles: [],
createdAt: '2026-08-01T00:00:00.000Z',
};
function gitActorStorage() {
const encoded = cborEncodeDagCbor(REPO_RECORD);
return createActorStorage({
async getRecord(/** @type {string} */ uri) {
return uri === `at://${DID}/dev.pdsjs.git.repo/myrepo`
? { cid: 'bafyrepo', value: encoded }
: null;
},
async listRecords(/** @type {string} */ collection) {
if (collection !== 'dev.pdsjs.git.repo') {
return { records: [], cursor: null };
}
return {
records: [
{
uri: `at://${DID}/dev.pdsjs.git.repo/myrepo`,
cid: 'bafyrepo',
rkey: 'myrepo',
value: encoded,
},
],
cursor: null,
};
},
});
}
/** A browser port that records calls and answers canned views. */
function fakeBrowser() {
/** @type {Array<{method: string, args: unknown[]}>} */
const calls = [];
/** @param {string} method @param {unknown[]} args */
const note = (method, args) => calls.push({ method, args });
/** @param {string|undefined} ref */
const check = (ref) => {
if (ref === 'huge') {
throw Object.assign(new Error('repository huge has no object indexes'), {
code: 'RepositoryTooLarge',
});
}
if (ref && ref !== 'main') throw new Error(`no such ref: ${ref}`);
};
return {
calls,
async listTree(/** @type {unknown[]} */ ...args) {
note('listTree', args);
check(/** @type {string|undefined} */ (args[2]));
return {
ref: 'refs/heads/main',
sha: SHA,
path: args[3],
entries: [
{ name: 'src', type: 'dir', mode: '40000', sha: SHA, size: null },
],
};
},
async readFile(/** @type {unknown[]} */ ...args) {
note('readFile', args);
return {
ref: 'refs/heads/main',
path: args[3],
size: 3,
mode: '100644',
mediaType: null,
binary: false,
truncated: false,
text: 'hi\n',
};
},
async readBytes(/** @type {unknown[]} */ ...args) {
note('readBytes', args);
const name = String(args[3]);
return {
ref: 'refs/heads/main',
path: args[3],
size: 3,
mode: '100644',
mediaType: name.endsWith('.png') ? 'image/png' : null,
binary: name.endsWith('.bin') || name.endsWith('.png'),
bytes: new Uint8Array([104, 105, 10]),
};
},
async listFiles(/** @type {unknown[]} */ ...args) {
note('listFiles', args);
return {
ref: 'refs/heads/main',
files: [{ path: 'README.md', size: 3 }],
truncated: false,
};
},
async listCommits(/** @type {unknown[]} */ ...args) {
note('listCommits', args);
return { ref: 'refs/heads/main', commits: [], truncated: false };
},
async readCommit(/** @type {unknown[]} */ ...args) {
note('readCommit', args);
return {
sha: SHA,
parents: [],
author: 'A <a@example.com>',
authoredAt: 1,
committedAt: 1,
message: 'm',
files: [
{
path: 'logo.png',
status: 'modified',
binary: true,
mediaType: 'image/png',
oldImage: new Uint8Array([1]),
newImage: new Uint8Array([2]),
hunks: null,
additions: 0,
deletions: 0,
},
],
};
},
async lastCommit(/** @type {unknown[]} */ ...args) {
note('lastCommit', args);
return { sha: SHA, parents: [], author: 'A', message: 'm' };
},
};
}
/**
* @param {Object} [options]
* @param {Object} [options.actorStorage]
* @param {Object} [options.spaceStorage]
* @param {Object} [options.spaceAdmin]
* @param {ReturnType<typeof fakeBrowser>|null} [options.browser]
* @param {boolean} [options.readOnly]
* @param {(who: {did: string, handle: string, pds: string}) => unknown} [options.openCopy] - a browser over another account's copy, for the reads that name one
*/
function createPds({
actorStorage = gitActorStorage(),
spaceStorage,
spaceAdmin,
browser = fakeBrowser(),
readOnly = false,
openCopy,
} = {}) {
return createBasePds({
actorStorage,
spaceStorage,
readOnly,
extensions: [
(host) => ({
name: 'git',
collections: [
'dev.pdsjs.git.repo',
'dev.pdsjs.git.config',
'dev.pdsjs.git.checkRequest',
],
routes: createGitMethods({
host,
browser: /** @type {any} */ (browser),
openCopy: /** @type {any} */ (openCopy),
hosting: createGitHosting({
host,
httpEnabled: false,
spaceBrowser: host.spaceBrowser ?? null,
spaceAdmin: /** @type {any} */ (spaceAdmin ?? null),
}),
}),
}),
],
});
}
/** @param {string} method @param {Record<string, string>} [params] */
const path = (method, params = {}) =>
`/xrpc/dev.pdsjs.git.${method}?${new URLSearchParams(params)}`;
/**
* @param {ReturnType<typeof createPds>} pds
* @param {string} method
* @param {unknown} body
* @param {Record<string, string>} [headers]
*/
const call = (pds, method, body, headers = {}) =>
pds.fetch(
new Request(`${BASE}/xrpc/dev.pdsjs.git.${method}`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...headers },
body: typeof body === 'string' ? body : JSON.stringify(body),
}),
);
/** @param {string} scope */
const bearer = async (scope) => ({
authorization: `Bearer ${await createAccessJwt(DID, SECRET, 3600, scope)}`,
});
describe('git XRPC reads', () => {
it('lists the public repositories to anyone, with CORS', async () => {
const pds = createPds();
const res = await pds.fetch(new Request(BASE + path('listRepos')));
expect(res.status).toBe(200);
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*');
expect(res.headers.get('Cache-Control')).toBe('no-cache');
const body = await res.json();
expect(body.httpEnabled).toBe(false);
expect(body.repos.map((/** @type {{name: string}} */ r) => r.name)).toEqual(
['myrepo'],
);
expect(body.repos[0].cloneUrl).toBe('atproto://test.handle/myrepo');
});
it('summarizes pull requests, issues and activity only when asked', async () => {
const pds = createPds();
const plain = await (
await pds.fetch(new Request(BASE + path('listRepos')))
).json();
expect(plain.repos[0].openPulls).toBeUndefined();
expect(plain.repos[0].openIssues).toBeUndefined();
expect(plain.repos[0].activity).toBeUndefined();
const summarized = await (
await pds.fetch(
new Request(BASE + path('listRepos', { summary: 'true' })),
)
).json();
expect(summarized.repos[0].openPulls).toBe(0);
expect(summarized.repos[0].openIssues).toBe(0);
expect(Array.isArray(summarized.repos[0].activity)).toBe(true);
});
it('lists the repositories in spaces to the account session only', async () => {
const spaceStorage = createFakeSpaceStorage({
spaces: [{ uri: SPACE, deletedAt: null }],
records: {
[SPACE]: [
{
collection: 'dev.pdsjs.git.repo',
rkey: 'private',
cid: 'bafyprivate',
value: { name: 'private', refs: [], bundles: [] },
},
],
},
});
const pds = createPds({ spaceStorage });
const anonymous = await (
await pds.fetch(new Request(BASE + path('listRepos')))
).json();
expect(
anonymous.repos.map((/** @type {{name: string}} */ r) => r.name),
).toEqual(['myrepo']);
const owner = await apiGet(pds, path('listRepos'), await sessionCookie());
expect(owner.headers.get('Cache-Control')).toBe('no-store');
const names = (await owner.json()).repos.map(
(/** @type {{name: string, space: string|null}} */ r) => [
r.name,
r.space,
],
);
expect(names).toEqual([
['myrepo', null],
['private', SPACE],
]);
});
it('answers one repository, and names the missing ones', async () => {
const pds = createPds();
const found = await pds.fetch(
new Request(BASE + path('getRepo', { repo: 'myrepo' })),
);
expect(found.status).toBe(200);
expect((await found.json()).defaultBranch).toBe('main');
const missing = await pds.fetch(
new Request(BASE + path('getRepo', { repo: 'nope' })),
);
expect(missing.status).toBe(404);
expect((await missing.json()).error).toBe('RepoNotFound');
const unnamed = await pds.fetch(new Request(BASE + path('getRepo')));
expect(unnamed.status).toBe(400);
expect((await unnamed.json()).error).toBe('InvalidRequest');
});
it('reads a tree under the record cid as its ETag', async () => {
const browser = fakeBrowser();
const pds = createPds({ browser });
const url = BASE + path('getTree', { repo: 'myrepo', path: 'src' });
const res = await pds.fetch(new Request(url));
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe('"bafyrepo"');
expect(res.headers.get('Cache-Control')).toBe('no-cache');
expect((await res.json()).entries[0].name).toBe('src');
expect(browser.calls[0]).toEqual({
method: 'listTree',
args: [DID, 'myrepo', undefined, 'src', undefined],
});
const cached = await pds.fetch(
new Request(url, { headers: { 'if-none-match': '"bafyrepo"' } }),
);
expect(cached.status).toBe(304);
expect(browser.calls).toHaveLength(1);
});
it('answers immutably under the current cid and refuses a stale one', async () => {
const pds = createPds();
const current = await pds.fetch(
new Request(BASE + path('getTree', { repo: 'myrepo', cid: 'bafyrepo' })),
);
expect(current.status).toBe(200);
expect(current.headers.get('Cache-Control')).toBe(
'public, max-age=31536000, immutable',
);
const stale = await pds.fetch(
new Request(BASE + path('getTree', { repo: 'myrepo', cid: 'bafyold' })),
);
expect(stale.status).toBe(404);
expect((await stale.json()).error).toBe('RepoNotFound');
});
it('reads a space repository through the account session alone', async () => {
const browser = fakeBrowser();
const spaceStorage = createFakeSpaceStorage({
spaces: [{ uri: SPACE, deletedAt: null }],
records: {
[SPACE]: [
{
collection: 'dev.pdsjs.git.repo',
rkey: 'private',
cid: 'bafyprivate',
value: { name: 'private', refs: [], bundles: [] },
},
],
},
});
const pds = createPds({ browser, spaceStorage });
const query = path('getTree', { repo: 'private', space: SPACE });
const anonymous = await pds.fetch(new Request(BASE + query));
expect(anonymous.status).toBe(401);
expect((await anonymous.json()).error).toBe('AuthenticationRequired');
const owner = await apiGet(pds, query, await sessionCookie());
expect(owner.status).toBe(200);
expect(owner.headers.get('Cache-Control')).toBe('no-store');
expect(owner.headers.get('ETag')).toBeNull();
expect(browser.calls[0].args).toEqual([
DID,
'private',
undefined,
'',
SPACE,
]);
});
it('reads a space repository with a bearer token naming the account', async () => {
const browser = fakeBrowser();
const spaceStorage = createFakeSpaceStorage({
spaces: [{ uri: SPACE, deletedAt: null }],
records: {
[SPACE]: [
{
collection: 'dev.pdsjs.git.repo',
rkey: 'private',
cid: 'bafyprivate',
value: { name: 'private', refs: [], bundles: [] },
},
],
},
});
const pds = createPds({ browser, spaceStorage });
const query = path('getTree', { repo: 'private', space: SPACE });
const res = await pds.fetch(
new Request(BASE + query, { headers: await bearer('atproto') }),
);
expect(res.status).toBe(200);
expect(browser.calls[0].args).toEqual([
DID,
'private',
undefined,
'',
SPACE,
]);
});
it('refuses a space read to another account’s bearer token', async () => {
const browser = fakeBrowser();
const spaceStorage = createFakeSpaceStorage({
spaces: [{ uri: SPACE, deletedAt: null }],
records: {
[SPACE]: [
{
collection: 'dev.pdsjs.git.repo',
rkey: 'private',
cid: 'bafyprivate',
value: { name: 'private', refs: [], bundles: [] },
},
],
},
});
const pds = createPds({ browser, spaceStorage });
const query = path('getTree', { repo: 'private', space: SPACE });
const other = {
authorization: `Bearer ${await createAccessJwt(
'did:plc:stranger',
SECRET,
3600,
'atproto',
)}`,
};
const res = await pds.fetch(new Request(BASE + query, { headers: other }));
expect(res.status).toBe(401);
expect((await res.json()).error).toBe('AuthenticationRequired');
});
it('lists the spaces to a bearer token naming the account', async () => {
const spaceStorage = createFakeSpaceStorage({
spaces: [{ uri: SPACE, deletedAt: null }],
records: {
[SPACE]: [
{
collection: 'dev.pdsjs.git.repo',
rkey: 'private',
cid: 'bafyprivate',
value: { name: 'private', refs: [], bundles: [] },
},
],
},
});
const pds = createPds({ spaceStorage });
const owner = await pds.fetch(
new Request(BASE + path('listRepos'), {
headers: await bearer('atproto'),
}),
);
const names = (await owner.json()).repos.map(
(/** @type {{name: string, space: string|null}} */ r) => [
r.name,
r.space,
],
);
expect(names).toEqual([
['myrepo', null],
['private', SPACE],
]);
});
it('reports an unknown ref as NotFound', async () => {
const pds = createPds();
const res = await pds.fetch(
new Request(BASE + path('getTree', { repo: 'myrepo', ref: 'gone' })),
);
expect(res.status).toBe(404);
expect(await res.json()).toEqual({
error: 'NotFound',
message: 'no such ref: gone',
});
});
it('names a repository the server will not download whole', async () => {
const pds = createPds();
const res = await pds.fetch(
new Request(BASE + path('getTree', { repo: 'myrepo', ref: 'huge' })),
);
expect(res.status).toBe(400);
expect((await res.json()).error).toBe('RepositoryTooLarge');
});
it('lists every file at a ref', async () => {
const browser = fakeBrowser();
const pds = createPds({ browser });
const res = await pds.fetch(
new Request(BASE + path('listFiles', { repo: 'myrepo', ref: 'main' })),
);
expect(res.status).toBe(200);
expect((await res.json()).files).toEqual([{ path: 'README.md', size: 3 }]);
expect(browser.calls[0].args).toEqual([DID, 'myrepo', 'main', undefined]);
});
it('reads a file, and needs its path', async () => {
const pds = createPds();
const unnamed = await pds.fetch(
new Request(BASE + path('getFile', { repo: 'myrepo' })),
);
expect(unnamed.status).toBe(400);
expect((await unnamed.json()).message).toMatch(/path/);
const res = await pds.fetch(
new Request(BASE + path('getFile', { repo: 'myrepo', path: 'a.txt' })),
);
expect(res.status).toBe(200);
expect((await res.json()).text).toBe('hi\n');
});
it('serves raw bytes as text, an octet stream or an image, never as a page', async () => {
const pds = createPds();
const text = await pds.fetch(
new Request(BASE + path('getRaw', { repo: 'myrepo', path: 'x.html' })),
);
expect(text.status).toBe(200);
expect(text.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
expect(text.headers.get('X-Content-Type-Options')).toBe('nosniff');
expect(text.headers.get('Content-Security-Policy')).toContain('sandbox');
expect(text.headers.get('ETag')).toBe('"bafyrepo"');
expect(await text.text()).toBe('hi\n');
const binary = await pds.fetch(
new Request(BASE + path('getRaw', { repo: 'myrepo', path: 'x.bin' })),
);
expect(binary.headers.get('Content-Type')).toBe('application/octet-stream');
const image = await pds.fetch(
new Request(BASE + path('getRaw', { repo: 'myrepo', path: 'x.png' })),
);
expect(image.headers.get('Content-Type')).toBe('image/png');
expect(image.headers.get('Content-Security-Policy')).toContain('sandbox');
});
it('clamps the commit limit', async () => {
const browser = fakeBrowser();
const pds = createPds({ browser });
await pds.fetch(
new Request(
BASE + path('listCommits', { repo: 'myrepo', limit: '9999' }),
),
);
await pds.fetch(
new Request(BASE + path('listCommits', { repo: 'myrepo' })),
);
expect(browser.calls.map((c) => c.args[3])).toEqual([500, 50]);
});
it('answers a commit without the image bytes of its diff', async () => {
const pds = createPds();
const res = await pds.fetch(
new Request(BASE + path('getCommit', { repo: 'myrepo', sha: SHA })),
);
expect(res.status).toBe(200);
const { files } = await res.json();
expect(files[0]).not.toHaveProperty('oldImage');
expect(files[0]).not.toHaveProperty('newImage');
expect(files[0].mediaType).toBe('image/png');
});
it('answers the last commit of a path', async () => {
const browser = fakeBrowser();
const pds = createPds({ browser });
const res = await pds.fetch(
new Request(
BASE + path('getLastCommit', { repo: 'myrepo', path: 'README.md' }),
),
);
expect((await res.json()).commit.sha).toBe(SHA);
expect(browser.calls[0].args).toEqual([
DID,
'myrepo',
undefined,
'README.md',
undefined,
undefined,
]);
});
it('answers the checks without a validator, since they live elsewhere', async () => {
const pds = createPds();
const res = await pds.fetch(
new Request(BASE + path('listChecks', { repo: 'myrepo' })),
);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBeNull();
expect(res.headers.get('Cache-Control')).toBe('no-store');
expect(await res.json()).toEqual({ runner: null, checks: [] });
});
it('registers the bundle-chain reads only with a browser', async () => {
const pds = createPds({ browser: null });
const res = await pds.fetch(
new Request(BASE + path('getTree', { repo: 'myrepo' })),
);
expect(res.status).toBe(501);
const { methods } = await (
await pds.fetch(
new Request(`${BASE}/xrpc/community.lexicon.service.describe`),
)
).json();
const names = methods.map((/** @type {{value: string}} */ m) => m.value);
expect(names).toContain('dev.pdsjs.git.listRepos');
expect(names).toContain('dev.pdsjs.git.createRepo');
expect(names).not.toContain('dev.pdsjs.git.getTree');
});
});
describe('git XRPC procedures', () => {
it('needs the account session or a bearer token', async () => {
const pds = createPds();
const res = await call(pds, 'createRepo', { name: 'x' });
expect(res.status).toBe(401);
expect((await res.json()).error).toBe('AuthenticationRequired');
});
it('refuses a cross-origin request on the session', async () => {
const pds = createPds();
const res = await call(
pds,
'createRepo',
{ name: 'x' },
{ cookie: await sessionCookie(), origin: 'https://evil.example' },
);
expect(res.status).toBe(403);
expect((await res.json()).error).toBe('Forbidden');
});
it('creates a private repository on the session', async () => {
/** @type {string[]} */
const created = [];
const pds = createPds({
spaceAdmin: {
async createSpace(/** @type {{type: string, skey?: string}} */ opts) {
created.push(`space ${opts.type}/${opts.skey ?? ''}`);
return { uri: `at://${DID}/space/${opts.type}/${opts.skey}` };
},
async createRecord(
/** @type {string} */ space,
/** @type {string} */ collection,
/** @type {string} */ rkey,
) {
created.push(`${space} ${collection}/${rkey}`);
},
},
});
const res = await call(
pds,
'createRepo',
{ name: 'proj', private: true },
{ cookie: await sessionCookie() },
);
expect(res.status).toBe(200);
expect(res.headers.get('Cache-Control')).toBe('no-store');
expect(await res.json()).toEqual({ name: 'proj', space: ownSpace('proj') });
expect(created).toEqual([
'space dev.pdsjs.git.space/proj',
`${ownSpace('proj')} dev.pdsjs.git.repo/proj`,
]);
});
it('creates a private repository with the account’s bearer token', async () => {
/** @type {string[]} */
const writes = [];
const pds = createPds({
spaceAdmin: {
async createSpace(/** @type {{type: string, skey?: string}} */ opts) {
return { uri: `at://${DID}/space/${opts.type}/${opts.skey}` };
},
async createRecord(
/** @type {string} */ space,
/** @type {string} */ collection,
/** @type {string} */ rkey,
) {
writes.push(`${space} ${collection}/${rkey}`);
},
},
});
const res = await call(
pds,
'createRepo',
{ name: 'proj', private: true },
await bearer('atproto'),
);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ name: 'proj', space: ownSpace('proj') });
expect(writes).toEqual([`${ownSpace('proj')} dev.pdsjs.git.repo/proj`]);
});
it('ignores a space the body names: the repository keeps its own', async () => {
/** @type {string[]} */
const created = [];
const pds = createPds({
spaceAdmin: {
async createSpace(/** @type {{type: string, skey?: string}} */ opts) {
created.push(`${opts.type}/${opts.skey ?? ''}`);
return { uri: `at://${DID}/space/${opts.type}/${opts.skey}` };
},
async createRecord() {},
},
});
const res = await call(
pds,
'createRepo',
{ name: 'proj', private: true, space: SPACE, type: 'com.example.other' },
await bearer('atproto'),
);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ name: 'proj', space: ownSpace('proj') });
expect(created).toEqual(['dev.pdsjs.git.space/proj']);
});
it('checks a bearer token against the repo scopes it writes', async () => {
const pds = createPds();
const denied = await call(
pds,
'createRepo',
{ name: 'myrepo' },
await bearer('repo:com.example.other'),
);
expect(denied.status).toBe(403);
expect(await denied.json()).toEqual({
error: 'InvalidScope',
message: 'Missing required scope to create dev.pdsjs.git.repo',
});
const allowed = await call(
pds,
'createRepo',
{ name: 'myrepo' },
await bearer('repo:dev.pdsjs.git.repo'),
);
expect(allowed.status).toBe(400);
expect((await allowed.json()).error).toBe('RepoExists');
});
it('refuses every write on a read-only server', async () => {
const pds = createPds({ readOnly: true });
const res = await call(
pds,
'requestCheck',
{ name: 'myrepo', ref: 'refs/heads/main' },
{ cookie: await sessionCookie() },
);
expect(res.status).toBe(401);
expect((await res.json()).message).toBe('This PDS is read-only');
});
it('names the refusals the lexicons list', async () => {
const pds = createPds();
const noRunner = await call(
pds,
'requestCheck',
{ name: 'myrepo', ref: 'refs/heads/main' },
{ cookie: await sessionCookie() },
);
expect(noRunner.status).toBe(400);
expect((await noRunner.json()).error).toBe('NoRunner');
const missing = await call(
pds,
'updateSettings',
{ name: 'nope', description: 'x' },
await bearer('com.atproto.access'),
);
expect(missing.status).toBe(404);
expect((await missing.json()).error).toBe('RepoNotFound');
});
it('refuses a body that is not a JSON object', async () => {
const pds = createPds();
const cookie = await sessionCookie();
const broken = await call(pds, 'createRepo', '{not json', { cookie });
expect(broken.status).toBe(400);
const list = await call(pds, 'createRepo', ['x'], { cookie });
expect(list.status).toBe(400);
expect((await list.json()).message).toMatch(/JSON object/);
});
});
describe('git method lexicons', () => {
it('describe the methods the extension registers, and no others', async () => {
const pds = createPds();
const { methods } = await (
await pds.fetch(
new Request(`${BASE}/xrpc/community.lexicon.service.describe`),
)
).json();
const served = methods
.map((/** @type {{value: string}} */ m) => m.value)
.filter((/** @type {string} */ nsid) =>
nsid.startsWith('dev.pdsjs.git.'),
);
expect(served.sort()).toEqual([...GIT_METHODS].sort());
});
it('resolve every local ref within the defs', () => {
/** @param {unknown} node @param {string[]} into */
const refsOf = (node, into) => {
if (Array.isArray(node)) {
for (const item of node) refsOf(item, into);
} else if (node && typeof node === 'object') {
const record = /** @type {Record<string, unknown>} */ (node);
if (typeof record.ref === 'string') into.push(record.ref);
for (const value of Object.values(record)) refsOf(value, into);
}
return into;
};
const defs = Object.keys(gitDefsLexicon.defs);
for (const lexicon of [gitDefsLexicon, ...gitMethodLexicons]) {
for (const ref of refsOf(lexicon.defs, [])) {
const [nsid, def] = ref.split('#');
if (nsid === '' || nsid === GIT_DEFS) {
expect(defs, `${lexicon.id} refers to ${ref}`).toContain(def);
} else {
expect(nsid).toMatch(/^dev\.pdsjs\.git\./);
}
}
expect(lexicon.lexicon).toBe(1);
}
for (const lexicon of gitMethodLexicons) {
expect(['query', 'procedure']).toContain(lexicon.defs.main.type);
}
});
});
describe("a read of another account's copy", () => {
const BOB = 'did:plc:bob';
/**
* The world beyond this server: bob has a document and a copy, nobody else
* does. The copy is listed rather than fetched by key, because a copy is
* its holder's own record and carries their own key.
*/
function directory() {
const real = globalThis.fetch;
globalThis.fetch = async (/** @type {any} */ input) => {
const url = new URL(String(input));
if (decodeURIComponent(url.pathname) === `/${BOB}`) {
return Response.json({
alsoKnownAs: ['at://bob.test'],
service: [
{ id: '#atproto_pds', serviceEndpoint: 'https://bob.test' },
],
});
}
if (
url.host === 'bob.test' &&
url.pathname === '/xrpc/com.atproto.repo.listRecords' &&
url.searchParams.get('collection') === 'dev.pdsjs.git.repo'
) {
return Response.json({
records: [
{
uri: `at://${BOB}/dev.pdsjs.git.repo/3lbobskey`,
cid: 'bafybobcopy',
value: { name: 'myrepo', refs: [], bundles: [] },
},
],
});
}
return new Response('no', { status: 404 });
};
return () => (globalThis.fetch = real);
}
/** A browser over bob's copy, noting what it is asked. */
function copyBrowser() {
/** @type {Array<{method: string, args: unknown[]}>} */
const calls = [];
const browser = {
readCommit: async (/** @type {unknown[]} */ ...args) => {
calls.push({ method: 'readCommit', args });
return { sha: SHA, parents: [], message: 'in the copy', files: [] };
},
readRange: async (/** @type {unknown[]} */ ...args) => {
calls.push({ method: 'readRange', args });
return {
base: SHA,
sha: SHA,
parents: [],
message: 'across',
files: [],
};
},
listRange: async (/** @type {unknown[]} */ ...args) => {
calls.push({ method: 'listRange', args });
return { commits: [], truncated: false };
},
listTree: async (/** @type {unknown[]} */ ...args) => {
calls.push({ method: 'listTree', args });
return { ref: 'main', sha: SHA, path: '', entries: [] };
},
};
return { calls, browser };
}
it('reads the copy through the server, and says whose copy answered', async () => {
const restore = directory();
const copy = copyBrowser();
try {
const pds = createPds({ openCopy: () => copy.browser });
const res = await pds.fetch(
new Request(
BASE + path('getCommit', { repo: 'myrepo', sha: SHA, from: BOB }),
),
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.message).toBe('in the copy');
expect(body.copy).toEqual({
did: BOB,
handle: 'bob.test',
pds: 'https://bob.test',
});
// The copy's account is what the browser reads, not this one, and it
// is read at the key bob's own record carries.
expect(copy.calls[0].method).toBe('readCommit');
expect(copy.calls[0].args.slice(0, 3)).toEqual([BOB, '3lbobskey', SHA]);
// A copy's version is theirs, so nothing here validates it.
expect(res.headers.get('etag')).toBeNull();
expect(res.headers.get('cache-control')).toBe('no-store');
} finally {
restore();
}
});
it('reads a range as one diff, and lists the commits across it', async () => {
const restore = directory();
const copy = copyBrowser();
try {
const pds = createPds({ openCopy: () => copy.browser });
const base = 'b'.repeat(40);
const diff = await pds.fetch(
new Request(
BASE +
path('getCommit', {
repo: 'myrepo',
sha: `${base}..${SHA}`,
from: BOB,
}),
),
);
expect(diff.status).toBe(200);
expect((await diff.json()).message).toBe('across');
const log = await pds.fetch(
new Request(
BASE +
path('listCommits', {
repo: 'myrepo',
range: `${base}..${SHA}`,
from: BOB,
}),
),
);
expect(log.status).toBe(200);
expect(copy.calls.map((one) => one.method)).toEqual([
'readRange',
'listRange',
]);
expect(copy.calls[0].args.slice(0, 4)).toEqual([
BOB,
'3lbobskey',
base,
SHA,
]);
} finally {
restore();
}
});
it('answers NotFound for an account with no server, and refuses a copy in a space', async () => {
const restore = directory();
try {
const pds = createPds({ openCopy: () => copyBrowser().browser });
const nobody = await pds.fetch(
new Request(
BASE + path('getTree', { repo: 'myrepo', from: 'did:plc:nobody' }),
),
);
expect(nobody.status).toBe(404);
expect((await nobody.json()).error).toBe('NotFound');
const spaced = await pds.fetch(
new Request(
BASE +
path('getTree', {
repo: 'myrepo',
from: BOB,
space: 'at://did:plc:x/com.example.t/s',
}),
),
);
expect(spaced.status).toBe(400);
} finally {
restore();
}
});
it("reads this account's own copy where from names it", async () => {
const restore = directory();
const copy = copyBrowser();
try {
const pds = createPds({ openCopy: () => copy.browser });
const res = await pds.fetch(
new Request(BASE + path('getTree', { repo: 'myrepo', from: DID })),
);
expect(res.status).toBe(200);
expect(copy.calls).toEqual([]);
expect(res.headers.get('etag')).not.toBeNull();
} finally {
restore();
}
});
});