#!/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} */ 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}>} */ 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}} actor * @param {string} method * @param {Record} 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, did: string}} actor * @param {string} collection * @param {Record} 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}`);