chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The storage views an extension receives: record calls check the
// collection they name against what the extension declared; everything
// else reaches the server's own port unchanged.
import { describe, expect, it, vi } from 'vitest';
import {
anonymousRequest,
collectionOfUri,
createCollectionScope,
scopeActorStorage,
scopeCommits,
scopeSpaceBrowser,
scopeSpaceStorage,
} from '../packages/core/src/extension-scope.js';
const DID = 'did:plc:scopetest';
const SPACE = `at://${DID}/space/com.example.team/main`;
/** @param {string[]|undefined} declared */
const scopeOf = (declared) =>
createCollectionScope(
() => 'demo',
() => declared,
);
describe('collectionOfUri', () => {
it('reads the segment before the record key, public or in a space', () => {
expect(collectionOfUri(`at://${DID}/app.example.post/3k`)).toBe(
'app.example.post',
);
expect(collectionOfUri(`${SPACE}/${DID}/app.example.post/3k`)).toBe(
'app.example.post',
);
expect(collectionOfUri('at://nothing')).toBe('');
});
});
describe('createCollectionScope', () => {
it('allows the declared collections and nothing else', () => {
const scope = scopeOf(['app.example.post']);
expect(scope.allows('app.example.post')).toBe(true);
expect(scope.allows('app.example.like')).toBe(false);
expect(() => scope.assert('app.example.like')).toThrow(
'Extension demo may not touch app.example.like: it declares app.example.post',
);
expect(() => scope.assertAll()).toThrow(/every collection/);
});
it('allows everything for a wildcard and nothing when undeclared', () => {
expect(scopeOf(['*']).allows('anything.at.all')).toBe(true);
expect(() => scopeOf(['*']).assertAll()).not.toThrow();
expect(() => scopeOf(undefined).assert('app.example.post')).toThrow(
/declares no collections/,
);
});
it('reads the declaration on every check', () => {
/** @type {string[]|undefined} */
let declared;
const scope = createCollectionScope(
() => 'late',
() => declared,
);
expect(scope.allows('a.b')).toBe(false);
declared = ['a.b'];
expect(scope.allows('a.b')).toBe(true);
});
});
describe('scopeActorStorage', () => {
function fakeStorage() {
return {
getRecord: vi.fn(async () => null),
listRecords: vi.fn(async () => ({ records: [], cursor: null })),
listAllRecords: vi.fn(async () => []),
putRecord: vi.fn(async () => {}),
deleteRecord: vi.fn(async () => {}),
linkBlobToRecord: vi.fn(async () => {}),
unlinkBlobsFromRecord: vi.fn(async () => {}),
putBlock: vi.fn(async () => {}),
// Bound to the storage, so a port written as a class still works.
getHandle: vi.fn(
/** @this {unknown} */ async function () {
return this === storage ? 'bound.handle' : 'unbound';
},
),
someField: 42,
};
}
const storage = fakeStorage();
const scoped = scopeActorStorage(
/** @type {any} */ (storage),
scopeOf(['app.example.post']),
);
it('passes record calls in the declared collection through', async () => {
await scoped.getRecord(`at://${DID}/app.example.post/1`);
await scoped.listRecords('app.example.post', null, 10);
await scoped.putRecord(
`at://${DID}/app.example.post/1`,
'bafy',
'app.example.post',
'1',
new Uint8Array(),
);
await scoped.deleteRecord(`at://${DID}/app.example.post/1`);
await scoped.linkBlobToRecord('bafyblob', `at://${DID}/app.example.post/1`);
await scoped.unlinkBlobsFromRecord(`at://${DID}/app.example.post/1`);
expect(storage.getRecord).toHaveBeenCalledTimes(1);
expect(storage.listRecords).toHaveBeenCalledTimes(1);
expect(storage.putRecord).toHaveBeenCalledTimes(1);
expect(storage.deleteRecord).toHaveBeenCalledTimes(1);
expect(storage.linkBlobToRecord).toHaveBeenCalledTimes(1);
expect(storage.unlinkBlobsFromRecord).toHaveBeenCalledTimes(1);
});
it('refuses record calls outside it before reaching the port', async () => {
const other = `at://${DID}/app.example.like/1`;
await expect(scoped.getRecord(other)).rejects.toThrow(/may not touch/);
await expect(
scoped.listRecords('app.example.like', null, 10),
).rejects.toThrow(/may not touch/);
await expect(
scoped.putRecord(
other,
'bafy',
'app.example.like',
'1',
new Uint8Array(),
),
).rejects.toThrow(/may not touch/);
await expect(scoped.deleteRecord(other)).rejects.toThrow(/may not touch/);
await expect(scoped.linkBlobToRecord('bafyblob', other)).rejects.toThrow(
/may not touch/,
);
await expect(scoped.unlinkBlobsFromRecord(other)).rejects.toThrow(
/may not touch/,
);
await expect(scoped.listAllRecords()).rejects.toThrow(/every collection/);
expect(storage.getRecord).toHaveBeenCalledTimes(1);
expect(storage.listAllRecords).not.toHaveBeenCalled();
});
it('passes everything that names no collection through, bound', async () => {
await scoped.putBlock('bafy', new Uint8Array());
expect(storage.putBlock).toHaveBeenCalledTimes(1);
expect(await scoped.getHandle()).toBe('bound.handle');
expect(/** @type {any} */ (scoped).someField).toBe(42);
});
});
describe('scopeSpaceStorage', () => {
const storage = {
getSpaceRecord: vi.fn(async () => null),
getSpaceRecordCid: vi.fn(async () => null),
listSpaceRecords: vi.fn(async () => ({ records: [], cursor: null })),
listAllSpaceRecords: vi.fn(async () => []),
commitSpaceWrite: vi.fn(async () => {}),
getSpace: vi.fn(async () => null),
};
const scoped = scopeSpaceStorage(
/** @type {any} */ (storage),
scopeOf(['app.example.post']),
);
it('checks the collection a record call or commit names', async () => {
await scoped.getSpaceRecord(SPACE, 'app.example.post', '1');
await scoped.listSpaceRecords(SPACE, {
collection: 'app.example.post',
limit: 5,
});
await scoped.commitSpaceWrite(SPACE, {
writes: [
/** @type {any} */ ({
action: 'create',
collection: 'app.example.post',
rkey: '1',
}),
],
setHash: new Uint8Array(),
rev: '3l',
indexedAt: 'now',
});
await scoped.getSpace(SPACE);
expect(storage.getSpaceRecord).toHaveBeenCalledTimes(1);
expect(storage.commitSpaceWrite).toHaveBeenCalledTimes(1);
expect(storage.getSpace).toHaveBeenCalledTimes(1);
await expect(
scoped.getSpaceRecordCid(SPACE, 'app.example.like', '1'),
).rejects.toThrow(/may not touch/);
await expect(scoped.listSpaceRecords(SPACE, { limit: 5 })).rejects.toThrow(
/every collection/,
);
await expect(scoped.listAllSpaceRecords(SPACE)).rejects.toThrow(
/every collection/,
);
await expect(
scoped.commitSpaceWrite(SPACE, {
writes: [
/** @type {any} */ ({
action: 'create',
collection: 'app.example.post',
rkey: '1',
}),
/** @type {any} */ ({
action: 'create',
collection: 'app.example.like',
rkey: '2',
}),
],
setHash: new Uint8Array(),
rev: '3l',
indexedAt: 'now',
}),
).rejects.toThrow(/may not touch app.example.like/);
expect(storage.commitSpaceWrite).toHaveBeenCalledTimes(1);
});
});
describe('scopeCommits', () => {
it('refuses a commit with any op outside the scope, before it starts', async () => {
const createCommit = vi.fn(async () => ({ cid: 'bafy', rev: '3l' }));
const commit = scopeCommits(createCommit, scopeOf(['app.example.post']));
await expect(
commit(DID, [
{ action: 'create', uri: `at://${DID}/app.example.post/1` },
]),
).resolves.toEqual({ cid: 'bafy', rev: '3l' });
await expect(
commit(DID, [
{ action: 'create', uri: `at://${DID}/app.example.post/2` },
{ action: 'delete', uri: `at://${DID}/app.example.like/1` },
]),
).rejects.toThrow(/may not touch app.example.like/);
expect(createCommit).toHaveBeenCalledTimes(1);
});
});
describe('anonymousRequest', () => {
it('removes the credential headers and keeps the rest', async () => {
const request = new Request('https://pds.internal/xrpc/x', {
method: 'POST',
headers: {
authorization: 'Bearer secret',
cookie: 'pdsjs_account=secret',
dpop: 'proof',
'content-type': 'application/json',
},
body: '{"a":1}',
});
const anonymous = anonymousRequest(request);
expect(anonymous.headers.has('authorization')).toBe(false);
expect(anonymous.headers.has('cookie')).toBe(false);
expect(anonymous.headers.has('dpop')).toBe(false);
expect(anonymous.headers.get('content-type')).toBe('application/json');
expect(anonymous.method).toBe('POST');
expect(await anonymous.text()).toBe('{"a":1}');
});
it('returns a request that carries none unchanged', () => {
const request = new Request('https://pds.internal/xrpc/x');
expect(anonymousRequest(request)).toBe(request);
});
});
describe('scopeSpaceBrowser', () => {
const browser = {
listSpaces: vi.fn(async () => []),
countRecords: vi.fn(async () => []),
countMembers: vi.fn(async () => 0),
listMembers: vi.fn(async () => []),
listRecords: vi.fn(async () => ({ records: [], truncated: false })),
listAllRecords: vi.fn(async () => []),
};
const scoped = scopeSpaceBrowser(browser, scopeOf(['app.example.post']));
it('checks a collection listing and needs every collection for the whole view', async () => {
await scoped.listSpaces();
await scoped.listMembers(SPACE);
await scoped.listRecords(SPACE, 'app.example.post', 10);
expect(browser.listRecords).toHaveBeenCalledTimes(1);
await expect(
scoped.listRecords(SPACE, 'app.example.like', 10),
).rejects.toThrow(/may not touch/);
await expect(scoped.listAllRecords()).rejects.toThrow(/every collection/);
expect(browser.listAllRecords).not.toHaveBeenCalled();
});
});