chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The /account/api/drive endpoints: session-gated, served by the drive
// extension over fake storage, write path blocked in read-only mode.
import { PersonalDataServer } from '@pdsjs/core';
import {
CID,
cborEncodeDagCbor,
cidToString,
createBlobCid,
createCid,
} from '@pdsjs/core/repo';
import { describe, expect, it } from 'vitest';
import { createDriveAccountRoutes } from '../src/account.js';
import { createDriveBrowser } from '../src/browser.js';
import { createDriveWriter } from '../src/writer.js';
const DID = 'did:plc:driveapitest';
const PASSWORD = 'account-api-password';
const PDS_HOST = 'pds.example.com';
const NOTES = new TextEncoder().encode('the notes');
const SHOT = new TextEncoder().encode('jpeg bytes here');
/** @param {{name: string, parent?: string}} folder */
function folderRecord(folder) {
return cborEncodeDagCbor({
$type: 'dev.pdsjs.drive.folder',
name: folder.name,
...(folder.parent ? { parent: folder.parent } : {}),
createdAt: '2026-08-14T00:00:00.000Z',
});
}
/**
* @param {{name: string, folder?: string, mimeType: string, data: Uint8Array}} file
*/
async function fileRecord(file) {
return cborEncodeDagCbor({
$type: 'dev.pdsjs.drive.file',
name: file.name,
...(file.folder ? { folder: file.folder } : {}),
parts: [
{
$type: 'blob',
ref: new CID(await createBlobCid(file.data)),
mimeType: 'application/octet-stream',
size: file.data.length,
},
],
mimeType: file.mimeType,
createdAt: '2026-08-14T00:00:00.000Z',
});
}
/**
* @param {{drive?: boolean, readOnly?: boolean, upload?: boolean, streaming?: boolean, spaces?: Array<{uri: string, spaceType: string, deletedAt?: string}>}} [opts]
*/
async function makePds(opts = {}) {
/** @type {Map<string, {uri: string, cid: string, value: Uint8Array}>} */
const records = new Map();
// One folder record, so a file can name it by key the way the lexicon says.
for (const folder of [{ rkey: '3photostid', name: 'photos' }]) {
const value = folderRecord(folder);
const uri = `at://${DID}/dev.pdsjs.drive.folder/${folder.rkey}`;
records.set(uri, { uri, cid: cidToString(await createCid(value)), value });
}
const specs = [
{
rkey: '3notestid',
name: 'notes.txt',
mimeType: 'text/plain',
data: NOTES,
},
{
rkey: '3shottid',
name: 'shot.jpg',
folder: '3photostid',
mimeType: 'image/jpeg',
data: SHOT,
},
];
for (const spec of specs) {
const value = await fileRecord(spec);
const uri = `at://${DID}/dev.pdsjs.drive.file/${spec.rkey}`;
records.set(uri, { uri, cid: cidToString(await createCid(value)), value });
}
const deleted = /** @type {string[]} */ ([]);
const unlinked = /** @type {string[]} */ ([]);
const created = /** @type {Array<{type: string, skey: string}>} */ ([]);
// The repo event log, which the change feed reads to build a delta.
/** @type {Array<{seq: number, evt: Uint8Array}>} */
const events = [];
const actorStorage = /** @type {any} */ ({
getDid: async () => DID,
getHandle: async () => 'user.example.com',
getAccountStatus: async () => 'active',
getRecord: async (/** @type {string} */ uri) => records.get(uri) || null,
listRecords: async (/** @type {string} */ collection) => ({
records: [...records.values()]
.filter((row) => row.uri.includes(`/${collection}/`))
.map((row) => ({ ...row })),
cursor: null,
}),
getLatestSeq: async () => events.length,
getEventsBefore: async (
/** @type {number} */ before,
/** @type {number} */ limit,
) =>
events
.filter((row) => row.seq < before)
.sort((left, right) => right.seq - left.seq)
.slice(0, limit),
deleteRecord: async (/** @type {string} */ uri) => {
deleted.push(uri);
records.delete(uri);
},
unlinkBlobsFromRecord: async (/** @type {string} */ uri) => {
unlinked.push(uri);
},
putBlock: async () => {},
putRecord: async (
/** @type {string} */ uri,
/** @type {string} */ cid,
/** @type {string} */ _collection,
/** @type {string} */ _rkey,
/** @type {Uint8Array} */ value,
) => {
records.set(uri, { uri, cid, value });
},
putBlob: async () => {},
linkBlobToRecord: async () => {},
});
/** @type {Map<string, Uint8Array>} */
const written = new Map();
const blobs = /** @type {any} */ ({
put: async (
/** @type {string} */ _did,
/** @type {string} */ cid,
/** @type {Uint8Array} */ data,
) => {
written.set(cid, data);
},
get: async (/** @type {string} */ _did, /** @type {string} */ cid) => {
for (const data of [NOTES, SHOT]) {
if (cidToString(await createBlobCid(data)) === cid) {
return { data, mimeType: 'application/octet-stream' };
}
}
const stored = written.get(cid);
return stored
? { data: stored, mimeType: 'application/octet-stream' }
: null;
},
...(opts.streaming
? {
createWriter: async () => {
/** @type {Uint8Array[]} */
const staged = [];
return {
write: async (/** @type {Uint8Array} */ chunk) => {
staged.push(chunk);
},
commit: async (/** @type {string} */ cid) => {
const size = staged.reduce((sum, c) => sum + c.length, 0);
const data = new Uint8Array(size);
let at = 0;
for (const c of staged) {
data.set(c, at);
at += c.length;
}
written.set(cid, data);
},
abort: async () => {},
};
},
}
: {}),
});
const pds = new PersonalDataServer({
actorStorage,
sharedStorage: /** @type {any} */ ({}),
blobs,
jwtSecret: 'account-api-secret',
hostname: PDS_HOST,
password: PASSWORD,
readOnly: opts.readOnly,
blobUploadLimit: 64,
extensions:
opts.drive === false
? []
: [
(host) => {
const ports = { actorStorage, blobs, getDid: host.getDid };
const browser = createDriveBrowser(ports);
const writer =
opts.upload === false
? null
: createDriveWriter({
...ports,
createCommit: host.createCommit,
chunkSize: host.blobUploadLimit,
});
return {
name: 'drive',
collections: ['dev.pdsjs.drive.file', 'dev.pdsjs.drive.folder'],
routes: createDriveAccountRoutes({
host,
browser,
writer,
spaceBrowser: opts.spaces
? /** @type {any} */ ({
listSpaces: async () =>
opts.spaces?.map((space) => ({
deletedAt: null,
...space,
})),
countRecords: async () => [],
})
: null,
spaceAdmin: opts.spaces
? /** @type {any} */ ({
createSpace: async (
/** @type {{type: string, skey: string}} */ made,
) => {
created.push(made);
return {
uri: `at://${DID}/space/${made.type}/${made.skey}`,
};
},
})
: null,
}),
features: { drive: true, driveUpload: Boolean(writer) },
};
},
],
});
// Commit and reap run their full course in integration; here the API
// contract is under test, not the repo machinery.
const commits = /** @type {Array<{action: string, uri: string}[]>} */ ([]);
pds.createCommit = /** @type {any} */ (
async (/** @type {string} */ _did, /** @type {any[]} */ writes) => {
commits.push(writes);
// The real commit appends to the event log; the change feed reads it.
events.push({
seq: events.length + 1,
evt: cborEncodeDagCbor({
ops: writes.map((write) => ({
action: write.action,
path: write.uri.split('/').slice(-2).join('/'),
})),
}),
});
}
);
let reaped = 0;
pds.reapOrphanedBlobs = /** @type {any} */ (
async () => {
reaped++;
return [];
}
);
return {
pds,
deleted,
unlinked,
commits,
records,
created,
reapCount: () => reaped,
};
}
/** @param {PersonalDataServer} pds */
async function signInCookie(pds) {
const response = await pds.fetch(
new Request(`https://${PDS_HOST}/account/sign-in`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
username: DID,
password: PASSWORD,
}).toString(),
}),
);
const header = response.headers.get('set-cookie') || '';
const value = decodeURIComponent(
header.match(/pdsjs_account=([^;]*)/)?.[1] ?? '',
);
expect(value).not.toBe('');
return `pdsjs_account=${encodeURIComponent(value)}`;
}
/**
* @param {PersonalDataServer} pds
* @param {string} path
* @param {{cookie?: string, method?: string, body?: unknown}} [opts]
*/
async function apiRaw(pds, path, opts = {}) {
return pds.fetch(
new Request(`https://${PDS_HOST}${path}`, {
method: opts.method || 'GET',
headers: {
...(opts.cookie ? { Cookie: opts.cookie } : {}),
...(opts.body !== undefined
? { 'Content-Type': 'application/json' }
: {}),
},
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
}),
);
}
/**
* @param {PersonalDataServer} pds
* @param {string} path
* @param {{cookie?: string, method?: string, body?: unknown}} [opts]
*/
async function api(pds, path, opts = {}) {
const response = await apiRaw(pds, path, opts);
return { status: response.status, body: await response.json() };
}
describe('/account/api/drive/files', () => {
it('requires a session', async () => {
const { pds } = await makePds();
const { status } = await api(pds, '/account/api/drive/files');
expect(status).toBe(401);
});
it('lists the top level through the browser port', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await api(pds, '/account/api/drive/files', {
cookie,
});
expect(status).toBe(200);
expect(body.enabled).toBe(true);
expect(body.folder).toBe('');
expect(body.files.map((/** @type {any} */ f) => f.name)).toEqual([
'notes.txt',
]);
expect(body.folders).toEqual([
{
rkey: '3photostid',
name: 'photos',
files: 1,
size: SHOT.length,
createdAt: '2026-08-14T00:00:00.000Z',
},
]);
expect(body.totalFiles).toBe(2);
});
it('lists a folder by its record key', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { body } = await api(
pds,
'/account/api/drive/files?folder=3photostid',
{ cookie },
);
expect(body.folder).toBe('3photostid');
expect(body.path).toBe('photos');
expect(body.ancestry).toEqual([{ rkey: '3photostid', name: 'photos' }]);
expect(body.files.map((/** @type {any} */ f) => f.name)).toEqual([
'shot.jpg',
]);
expect(body.folders).toEqual([]);
});
it('refuses a folder that is not there', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await api(
pds,
'/account/api/drive/files?folder=nosuchfolder',
{ cookie },
);
expect(status).toBe(400);
expect(body.error).toMatch(/not valid/);
});
it('has no routes without the extension', async () => {
const { pds } = await makePds({ drive: false });
const cookie = await signInCookie(pds);
const response = await apiRaw(pds, '/account/api/drive/files', {
cookie,
});
expect(response.status).toBe(404);
});
});
describe('/account/api/drive/file', () => {
it('serves the bytes as an attachment', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const response = await apiRaw(
pds,
'/account/api/drive/file?rkey=3notestid',
{ cookie },
);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('text/plain');
expect(response.headers.get('content-disposition')).toContain(
'filename="notes.txt"',
);
expect(response.headers.get('x-content-type-options')).toBe('nosniff');
expect(new Uint8Array(await response.arrayBuffer())).toEqual(NOTES);
});
it('rejects a missing rkey and an unknown file', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const unnamed = await api(pds, '/account/api/drive/file', { cookie });
expect(unnamed.status).toBe(400);
const missing = await api(pds, '/account/api/drive/file?rkey=nope', {
cookie,
});
expect(missing.status).toBe(400);
expect(missing.body.error).toMatch(/No such file/);
});
});
describe('/account/api/drive/upload', () => {
/**
* @param {PersonalDataServer} pds
* @param {string} path
* @param {{cookie?: string, body?: BodyInit, type?: string, origin?: string}} opts
*/
async function send(pds, path, opts) {
const response = await pds.fetch(
new Request(`https://${PDS_HOST}${path}`, {
method: 'POST',
headers: {
...(opts.cookie ? { Cookie: opts.cookie } : {}),
'Content-Type': opts.type || 'text/plain',
...(opts.origin ? { Origin: opts.origin } : {}),
},
body: opts.body,
}),
);
return { status: response.status, body: await response.json() };
}
it('stores the body as a file and commits it', async () => {
const { pds, commits } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await send(
pds,
'/account/api/drive/upload?name=hello.txt',
{ cookie, body: 'hello there' },
);
expect(status).toBe(200);
expect(body).toMatchObject({
name: 'hello.txt',
folder: '',
mimeType: 'text/plain',
size: 11,
partCount: 1,
});
expect(commits).toEqual([
[{ action: 'create', uri: body.uri, cid: body.cid }],
]);
const listing = await api(pds, '/account/api/drive/files', { cookie });
expect(listing.body.files.map((/** @type {any} */ f) => f.name)).toContain(
'hello.txt',
);
});
it('splits a body past the chunk size into several parts', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
// The harness sets a 64-byte chunk, so this needs three blobs.
const { body } = await send(pds, '/account/api/drive/upload?name=big.bin', {
cookie,
body: 'x'.repeat(150),
type: 'application/octet-stream',
});
expect(body.partCount).toBe(3);
expect(body.size).toBe(150);
const download = await apiRaw(
pds,
`/account/api/drive/file?rkey=${body.rkey}`,
{ cookie },
);
expect(await download.text()).toBe('x'.repeat(150));
});
it('puts the file in the folder the query names', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { body } = await send(
pds,
'/account/api/drive/upload?name=note.txt&folder=3photostid',
{ cookie, body: 'notes' },
);
expect(body.folder).toBe('3photostid');
});
it('requires a session', async () => {
const { pds } = await makePds();
const { status } = await send(pds, '/account/api/drive/upload?name=a.txt', {
body: 'x',
});
expect(status).toBe(401);
});
it('refuses a cross-origin post', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status } = await send(pds, '/account/api/drive/upload?name=a.txt', {
cookie,
body: 'x',
origin: 'https://evil.example',
});
expect(status).toBe(403);
});
it('rejects a name that is not one path segment', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await send(
pds,
'/account/api/drive/upload?name=a%2Fb.txt',
{ cookie, body: 'x' },
);
expect(status).toBe(400);
expect(body.error).toMatch(/file name/);
});
it('is blocked in read-only mode', async () => {
const { pds } = await makePds({ readOnly: true });
const cookie = await signInCookie(pds);
const { status } = await send(pds, '/account/api/drive/upload?name=a.txt', {
cookie,
body: 'x',
});
expect(status).toBe(403);
});
it('reports the feature off without a writer', async () => {
const { pds } = await makePds({ upload: false });
const cookie = await signInCookie(pds);
const { status, body } = await send(
pds,
'/account/api/drive/upload?name=a.txt',
{ cookie, body: 'x' },
);
expect(status).toBe(400);
expect(body.error).toMatch(/not enabled/);
});
});
describe('/account/api/drive/delete', () => {
it('trashes the file, leaving the bytes where they are', async () => {
const { pds, deleted, unlinked, reapCount } = await makePds();
const cookie = await signInCookie(pds);
const { status } = await api(pds, '/account/api/drive/delete', {
cookie,
method: 'POST',
body: { rkey: '3notestid' },
});
expect(status).toBe(200);
// Nothing removed, nothing unlinked, no sweep: the bytes have to survive
// for a restore to give them back.
expect(deleted).toEqual([]);
expect(unlinked).toEqual([]);
expect(reapCount()).toBe(0);
const listing = await api(pds, '/account/api/drive/files', { cookie });
expect(
listing.body.files.map((/** @type {any} */ f) => f.name),
).not.toContain('notes.txt');
const trash = await api(pds, '/account/api/drive/trash', { cookie });
expect(trash.body.items.map((/** @type {any} */ i) => i.name)).toEqual([
'notes.txt',
]);
});
it('restores what it trashed', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
await api(pds, '/account/api/drive/delete', {
cookie,
method: 'POST',
body: { rkey: '3notestid' },
});
const { status, body } = await api(pds, '/account/api/drive/restore', {
cookie,
method: 'POST',
body: { rkey: '3notestid' },
});
expect(status).toBe(200);
expect(body.restored).toBe(1);
const listing = await api(pds, '/account/api/drive/files', { cookie });
expect(listing.body.files.map((/** @type {any} */ f) => f.name)).toContain(
'notes.txt',
);
});
it('frees the bytes only when the trash is emptied', async () => {
const { pds, deleted, unlinked, reapCount } = await makePds();
const cookie = await signInCookie(pds);
const uri = `at://${DID}/dev.pdsjs.drive.file/3notestid`;
await api(pds, '/account/api/drive/delete', {
cookie,
method: 'POST',
body: { rkey: '3notestid' },
});
const { status, body } = await api(pds, '/account/api/drive/purge', {
cookie,
method: 'POST',
body: { rkey: '3notestid' },
});
expect(status).toBe(200);
expect(body.purged).toBe(1);
expect(unlinked).toEqual([uri]);
expect(deleted).toEqual([uri]);
expect(reapCount()).toBe(1);
});
it('rejects an unknown file and a missing rkey', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const missing = await api(pds, '/account/api/drive/delete', {
cookie,
method: 'POST',
body: { rkey: 'nope' },
});
expect(missing.status).toBe(400);
expect(missing.body.error).toMatch(/No such file/);
const unnamed = await api(pds, '/account/api/drive/delete', {
cookie,
method: 'POST',
body: {},
});
expect(unnamed.status).toBe(400);
});
it('is blocked in read-only mode while listing still works', async () => {
const { pds, deleted } = await makePds({ readOnly: true });
const cookie = await signInCookie(pds);
const list = await api(pds, '/account/api/drive/files', { cookie });
expect(list.status).toBe(200);
const removal = await api(pds, '/account/api/drive/delete', {
cookie,
method: 'POST',
body: { rkey: '3notestid' },
});
expect(removal.status).toBe(403);
expect(deleted).toHaveLength(0);
});
});
describe('/account/api/drive/file ranges', () => {
it('advertises ranges and serves one', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const whole = await apiRaw(pds, '/account/api/drive/file?rkey=3notestid', {
cookie,
});
expect(whole.headers.get('accept-ranges')).toBe('bytes');
const response = await pds.fetch(
new Request(`https://${PDS_HOST}/account/api/drive/file?rkey=3notestid`, {
headers: { Cookie: cookie, Range: 'bytes=4-7' },
}),
);
expect(response.status).toBe(206);
expect(response.headers.get('content-range')).toBe(
`bytes 4-7/${NOTES.length}`,
);
expect(await response.text()).toBe(
new TextDecoder().decode(NOTES.subarray(4, 8)),
);
});
it('serves a suffix range', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const response = await pds.fetch(
new Request(`https://${PDS_HOST}/account/api/drive/file?rkey=3notestid`, {
headers: { Cookie: cookie, Range: 'bytes=-3' },
}),
);
expect(response.status).toBe(206);
expect(await response.text()).toBe(
new TextDecoder().decode(NOTES.subarray(NOTES.length - 3)),
);
});
it('answers 416 for a range past the end', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const response = await pds.fetch(
new Request(`https://${PDS_HOST}/account/api/drive/file?rkey=3notestid`, {
headers: { Cookie: cookie, Range: 'bytes=999-1200' },
}),
);
expect(response.status).toBe(416);
expect(response.headers.get('content-range')).toBe(
`bytes */${NOTES.length}`,
);
});
});
describe('/account/api/drive/folder', () => {
it('makes, renames and removes a folder', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const made = await api(pds, '/account/api/drive/folder', {
cookie,
method: 'POST',
body: { name: 'plans' },
});
expect(made.status).toBe(200);
expect(made.body.name).toBe('plans');
const renamed = await api(pds, '/account/api/drive/folder/rename', {
cookie,
method: 'POST',
body: { rkey: made.body.rkey, name: 'roadmap' },
});
expect(renamed.status).toBe(200);
expect(renamed.body.name).toBe('roadmap');
const listing = await api(
pds,
`/account/api/drive/files?folder=${made.body.rkey}`,
{ cookie },
);
expect(listing.body.path).toBe('roadmap');
const removed = await api(pds, '/account/api/drive/folder/delete', {
cookie,
method: 'POST',
body: { rkey: made.body.rkey },
});
expect(removed.status).toBe(200);
});
it('rejects an unnamed folder and one that is not there', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const unnamed = await api(pds, '/account/api/drive/folder', {
cookie,
method: 'POST',
body: {},
});
expect(unnamed.status).toBe(400);
const missing = await api(pds, '/account/api/drive/folder/delete', {
cookie,
method: 'POST',
body: { rkey: 'nosuch' },
});
expect(missing.status).toBe(400);
});
it('is blocked in read-only mode', async () => {
const { pds } = await makePds({ readOnly: true });
const cookie = await signInCookie(pds);
const { status } = await api(pds, '/account/api/drive/folder', {
cookie,
method: 'POST',
body: { name: 'plans' },
});
expect(status).toBe(403);
});
});
describe('/account/api/drive/changes', () => {
it('tells a caller with no anchor to enumerate', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await api(pds, '/account/api/drive/changes', {
cookie,
});
expect(status).toBe(200);
expect(body.reenumerate).toBe(true);
expect(typeof body.anchor).toBe('number');
});
it('tells a caller watching a space to enumerate', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { body } = await api(
pds,
'/account/api/drive/changes?since=0&space=at%3A%2F%2Fx%2Fspace%2Fa%2Fb',
{ cookie },
);
// A space keeps its own commit log, which the repo sequence does not cover.
expect(body.reenumerate).toBe(true);
expect(body.anchor).toBe(null);
});
it('rejects an anchor that is not a number', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status } = await api(pds, '/account/api/drive/changes?since=abc', {
cookie,
});
expect(status).toBe(400);
});
});
describe('the change feed as a client would walk it', () => {
it('reports what changed since an anchor, and what went away', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const first = await api(pds, '/account/api/drive/changes', { cookie });
const anchor = first.body.anchor;
const made = await api(pds, '/account/api/drive/folder', {
cookie,
method: 'POST',
body: { name: 'plans' },
});
const removal = await api(pds, '/account/api/drive/delete', {
cookie,
method: 'POST',
body: { rkey: '3notestid' },
});
expect(removal.status).toBe(200);
const delta = await api(pds, `/account/api/drive/changes?since=${anchor}`, {
cookie,
});
expect(delta.body.reenumerate).toBe(false);
expect(delta.body.changed).toContain(
`dev.pdsjs.drive.folder/${made.body.rkey}`,
);
expect(delta.body.deleted).toContain('dev.pdsjs.drive.file/3notestid');
expect(delta.body.anchor).toBeGreaterThan(anchor);
});
it('reports nothing when nothing has happened', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const first = await api(pds, '/account/api/drive/changes', { cookie });
const again = await api(
pds,
`/account/api/drive/changes?since=${first.body.anchor}`,
{ cookie },
);
expect(again.body.changed).toEqual([]);
expect(again.body.deleted).toEqual([]);
});
});
describe('which spaces a drive is offered', () => {
const uriFor = (/** @type {string} */ type, /** @type {string} */ key) =>
`at://${DID}/space/${type}/${key}`;
it("offers only spaces made for a drive, never another app's", async () => {
const { pds } = await makePds({
spaces: [
// An app's own space holds that app's data model, and its members
// read what is in it. A file put there is squatting and is shared.
{
uri: uriFor('app.bulleted.space', 'self'),
spaceType: 'app.bulleted.space',
},
{
uri: uriFor('sh.tangled.repo', '3msw4lgrmisi4'),
spaceType: 'sh.tangled.repo',
},
{
uri: uriFor('dev.pdsjs.drive.space', 'photos'),
spaceType: 'dev.pdsjs.drive.space',
},
],
});
const cookie = await signInCookie(pds);
const { body } = await api(pds, '/account/api/drive/files', { cookie });
expect(body.spaces).toEqual([
{
uri: uriFor('dev.pdsjs.drive.space', 'photos'),
type: 'dev.pdsjs.drive.space',
key: 'photos',
name: 'photos',
},
]);
});
it('names a drive by the key its owner chose', async () => {
const { pds } = await makePds({
spaces: [
{
uri: uriFor('dev.pdsjs.drive.space', 'photos'),
spaceType: 'dev.pdsjs.drive.space',
},
{
uri: uriFor('dev.pdsjs.drive.space', 'work'),
spaceType: 'dev.pdsjs.drive.space',
},
],
});
const cookie = await signInCookie(pds);
const { body } = await api(pds, '/account/api/drive/files', { cookie });
// Every drive space shares one type, so the key is what tells them apart.
expect(body.spaces.map((/** @type {any} */ s) => s.name)).toEqual([
'photos',
'work',
]);
});
it('leaves out a deleted drive', async () => {
const { pds } = await makePds({
spaces: [
{
uri: uriFor('dev.pdsjs.drive.space', 'gone'),
spaceType: 'dev.pdsjs.drive.space',
deletedAt: '2026-08-15T00:00:00.000Z',
},
],
});
const cookie = await signInCookie(pds);
const { body } = await api(pds, '/account/api/drive/files', { cookie });
expect(body.spaces).toEqual([]);
});
});
describe('/account/api/drive/space', () => {
it('makes a space of the drive type, not of any other', async () => {
const { pds, created } = await makePds({ spaces: [] });
const cookie = await signInCookie(pds);
const { status, body } = await api(pds, '/account/api/drive/space', {
cookie,
method: 'POST',
body: { name: 'photos' },
});
expect(status).toBe(200);
expect(created).toEqual([
{ type: 'dev.pdsjs.drive.space', skey: 'photos' },
]);
expect(body).toMatchObject({ key: 'photos', name: 'photos' });
});
it('refuses a name that would not read as one', async () => {
const { pds } = await makePds({ spaces: [] });
const cookie = await signInCookie(pds);
for (const name of ['', 'Photos', 'my photos', 'a/b', '-lead']) {
const { status } = await api(pds, '/account/api/drive/space', {
cookie,
method: 'POST',
body: { name },
});
expect(status).toBe(400);
}
});
it('reports the feature off where spaces are not enabled', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await api(pds, '/account/api/drive/space', {
cookie,
method: 'POST',
body: { name: 'photos' },
});
expect(status).toBe(400);
expect(body.error).toMatch(/not enabled/);
const listing = await api(pds, '/account/api/drive/files', { cookie });
expect(listing.body.canMakeSpace).toBe(false);
});
it('is blocked in read-only mode', async () => {
const { pds } = await makePds({ spaces: [], readOnly: true });
const cookie = await signInCookie(pds);
const { status } = await api(pds, '/account/api/drive/space', {
cookie,
method: 'POST',
body: { name: 'photos' },
});
expect(status).toBe(403);
});
});
describe('/account/api/drive/upload sessions', () => {
/**
* @param {PersonalDataServer} pds
* @param {string} id
* @param {string} cookie
* @param {string} text
*/
async function sendPart(pds, id, cookie, text) {
const response = await pds.fetch(
new Request(
`https://${PDS_HOST}/account/api/drive/upload/part?id=${id}`,
{
method: 'POST',
headers: {
Cookie: cookie,
'Content-Type': 'application/octet-stream',
},
body: text,
},
),
);
return { status: response.status, body: await response.json() };
}
it('takes a file across several requests and lands it as one blob', async () => {
const { pds, records } = await makePds({ streaming: true });
const cookie = await signInCookie(pds);
const started = await api(pds, '/account/api/drive/upload/start', {
cookie,
method: 'POST',
body: { name: 'clip.mp4', mimeType: 'video/mp4' },
});
expect(started.status).toBe(200);
const { id } = started.body;
const first = await sendPart(pds, id, cookie, 'the first part ');
expect(first.status).toBe(200);
expect(first.body.received).toBe(15);
const second = await sendPart(pds, id, cookie, 'and the second');
expect(second.body.received).toBe(29);
const finished = await api(pds, '/account/api/drive/upload/finish', {
cookie,
method: 'POST',
body: { id },
});
expect(finished.status).toBe(200);
expect(finished.body).toMatchObject({
name: 'clip.mp4',
mimeType: 'video/mp4',
size: 29,
partCount: 1,
});
expect(records.has(finished.body.uri)).toBe(true);
});
it('advertises the part size only when the port streams', async () => {
const streaming = await makePds({ streaming: true });
const cookie = await signInCookie(streaming.pds);
const listing = await api(
streaming.pds,
'/account/api/drive/files?folder=',
{
cookie,
},
);
expect(listing.body.uploadPartSize).toBeGreaterThan(0);
const buffered = await makePds();
const other = await signInCookie(buffered.pds);
const plain = await api(buffered.pds, '/account/api/drive/files?folder=', {
cookie: other,
});
expect(plain.body.uploadPartSize).toBe(null);
});
it('reports the feature off without a streaming blob port', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await api(pds, '/account/api/drive/upload/start', {
cookie,
method: 'POST',
body: { name: 'a.bin' },
});
expect(status).toBe(400);
expect(body.error).toMatch(/in parts/);
});
it('abort forgets the session', async () => {
const { pds } = await makePds({ streaming: true });
const cookie = await signInCookie(pds);
const { body: started } = await api(
pds,
'/account/api/drive/upload/start',
{ cookie, method: 'POST', body: { name: 'gone.bin' } },
);
await sendPart(pds, started.id, cookie, 'some bytes');
const aborted = await api(pds, '/account/api/drive/upload/abort', {
cookie,
method: 'POST',
body: { id: started.id },
});
expect(aborted.body).toEqual({ aborted: true });
const finished = await api(pds, '/account/api/drive/upload/finish', {
cookie,
method: 'POST',
body: { id: started.id },
});
expect(finished.status).toBe(400);
expect(finished.body.error).toMatch(/No such upload/);
});
});