Something went wrong. Try again.
An AT Protocol Personal Data Server written in JavaScript pdsjs.dev
pds atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167#!/usr/bin/env node/** * Local-dev seed for the space browser: a space alice hosts, a second member * writing into it from a PDS of his own, records in two collections, and one * record naming a blob. * * The second writer is what makes this worth seeding. A space is one dataset * spread over the repos of everyone who writes to it, and reading somebody * else's repo is the whole delegation-and-credential exchange, which nothing * short of two servers exercises. * * Both accounts must already exist; `just dev-spaces` runs the recipes that * make them and then starts the UI. * * Usage: node scripts/dev-spaces-demo.mjs * Env: PDS_DEV_URL (default http://localhost:2471) * PDS_DEV_BOB_URL (default http://localhost:2472) * PDS_DEV_PASSWORD (default test-password) */
const ALICE = process.env.PDS_DEV_URL || 'http://localhost:2471';const BOB = process.env.PDS_DEV_BOB_URL || 'http://localhost:2472';const PASSWORD = process.env.PDS_DEV_PASSWORD || 'test-password';
const SPACE_TYPE = 'dev.pdsjs.demo.board';const SKEY = 'general';const NOTES = 'dev.pdsjs.demo.note';const PINS = 'dev.pdsjs.demo.pin';
/** * @param {string} base * @param {string} method * @param {object} [init] * @returns {Promise<any>} */async function xrpc(base, method, init = {}) { const response = await fetch(`${base}/xrpc/${method}`, init); const text = await response.text(); if (!response.ok) { throw new Error(`${method} ${response.status}: ${text.slice(0, 300)}`); } return text ? JSON.parse(text) : {};}
/** * The account one dev PDS serves, with a session on it. * @param {string} base * @returns {Promise<{base: string, did: string, headers: Record<string, string>}>} */async function account(base) { const did = (await (await fetch(`${base}/.well-known/atproto-did`)).text()) .trim() .replace(/^"|"$/g, ''); if (!did.startsWith('did:')) { throw new Error(`${base} serves no account yet.`); } const session = await xrpc(base, 'com.atproto.server.createSession', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ identifier: did, password: PASSWORD }), }); return { base, did, headers: { authorization: `Bearer ${session.accessJwt}`, 'content-type': 'application/json', }, };}
/** * @param {{base: string, headers: Record<string, string>}} actor * @param {string} method * @param {Record<string, unknown>} input */function post(actor, method, input) { return xrpc(actor.base, method, { method: 'POST', headers: actor.headers, body: JSON.stringify(input), });}
const alice = await account(ALICE);const bob = await account(BOB);
// ---- the space ----
const space = `at://${alice.did}/space/${SPACE_TYPE}/${SKEY}`;try { await post(alice, 'com.atproto.simplespace.createSpace', { type: SPACE_TYPE, skey: SKEY, }); console.error(`created ${space}`);} catch (error) { // A re-run finds the space already there, which is not a failure. if (!String(error.message).includes('SpaceAlreadyExists')) throw error; console.error(`keep ${space}`);}
await post(alice, 'com.atproto.simplespace.addMember', { space, did: bob.did,});console.error(`member ${bob.did}`);
// ---- a blob, and the record that names it ----
// A 1x1 PNG, so the browser has an image to display rather than a download.const pngBytes = Uint8Array.from( atob( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', ), (character) => character.charCodeAt(0),);const upload = await xrpc(ALICE, 'com.atproto.repo.uploadBlob', { method: 'POST', headers: { ...alice.headers, 'content-type': 'image/png' }, body: pngBytes,});
// ---- records, from both members ----
/** * @param {{base: string, headers: Record<string, string>, did: string}} actor * @param {string} collection * @param {Record<string, unknown>} record */async function write(actor, collection, record) { const { uri } = await post(actor, 'com.atproto.space.createRecord', { space, repo: actor.did, collection, record: { $type: collection, createdAt: new Date().toISOString(), ...record, }, }); console.error(`wrote ${uri}`);}
await write(alice, NOTES, { text: 'The board is open. Anyone on the member list can write here.',});await write(alice, NOTES, { text: 'Records live in each writer’s own repo, on their own PDS.',});await write(alice, PINS, { label: 'Read me first', order: 1 });await write(alice, PINS, { label: 'With an attachment', order: 2, image: upload.blob,});
await write(bob, NOTES, { text: 'Bob writing from a second server. Opening this repo takes a credential.',});await write(bob, PINS, { label: "Bob's pin", order: 3 });
console.error('');console.error(`space: ${space}`);console.error(`alice: ${alice.did}`);console.error(`bob: ${bob.did}`);