/** * PDS client - wraps XRPC calls to deployed pds.js instances * * alice is on pds-message-demo, bob is on pds-message-demo-2 * this demonstrates real PDS-to-PDS messaging across different servers */ const PDS_PASSWORD = 'pds-message-demo-2026'; const CREDENTIALS = { alice: { handle: 'alice.pds-message-demo.nate-8fe.workers.dev', did: 'did:plc:cmadossymmii3izkabdbp5en', pdsUrl: 'https://pds-message-demo.nate-8fe.workers.dev' }, bob: { handle: 'bob.pds-message-demo-2.nate-8fe.workers.dev', did: 'did:plc:deeom7pq4ynuigyr2p562vxz', pdsUrl: 'https://pds-message-demo-2.nate-8fe.workers.dev' } }; // resolve DID to PDS URL (for cross-PDS messaging) async function resolvePdsUrl(did) { // check local cache first for (const creds of Object.values(CREDENTIALS)) { if (creds.did === did) return creds.pdsUrl; } // fallback: resolve via plc.directory const res = await fetch(`https://plc.directory/${did}`); if (!res.ok) throw new Error(`Failed to resolve DID: ${did}`); const doc = await res.json(); return doc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint; } export class PDSClient { constructor(name, creds) { this.name = name; this.did = creds.did; this.handle = creds.handle; this.pdsUrl = creds.pdsUrl; this.inbox = []; this.pending = new Map(); this.accepted = new Set(); this.blocked = new Set(); this.accessToken = null; } async init() { const res = await fetch(`${this.pdsUrl}/xrpc/com.atproto.server.createSession`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: this.handle, password: PDS_PASSWORD }) }); if (!res.ok) { throw new Error(`Failed to create session for ${this.name}: ${await res.text()}`); } const session = await res.json(); this.accessToken = session.accessJwt; await this.syncState(); } async syncState() { const inboxRes = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.list`, { headers: { Authorization: `Bearer ${this.accessToken}` } }); if (inboxRes.ok) { const data = await inboxRes.json(); this.inbox = data.messages.map((m) => ({ from: m.fromDid, text: m.text, time: new Date(m.createdAt) })); } const reqRes = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.listRequests`, { headers: { Authorization: `Bearer ${this.accessToken}` } }); if (reqRes.ok) { const data = await reqRes.json(); this.pending = new Map( data.requests.map((r) => [ r.fromDid, { text: r.text, time: new Date(r.createdAt) } ]) ); } const stateRes = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.getState`, { headers: { Authorization: `Bearer ${this.accessToken}` } }); if (stateRes.ok) { const data = await stateRes.json(); this.accepted = new Set(data.accepted); this.blocked = new Set(data.blocked); } } async getServiceAuth(audienceDid, lxm) { const params = new URLSearchParams({ aud: audienceDid }); if (lxm) params.set('lxm', lxm); const res = await fetch(`${this.pdsUrl}/xrpc/com.atproto.server.getServiceAuth?${params}`, { headers: { Authorization: `Bearer ${this.accessToken}` } }); if (!res.ok) { throw new Error(`Failed to get service auth: ${await res.text()}`); } const { token } = await res.json(); return token; } async sendMessage(recipientDid, text) { // get service auth JWT from OUR PDS const jwt = await this.getServiceAuth(recipientDid, 'xyz.fake.inbox.send'); // resolve recipient's PDS and send the message THERE (cross-PDS!) const recipientPdsUrl = await resolvePdsUrl(recipientDid); const res = await fetch(`${recipientPdsUrl}/xrpc/xyz.fake.inbox.send`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}` }, body: JSON.stringify({ text }) }); const result = await res.json(); // parse JWT for display const parts = jwt.split('.'); const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); if (result.error) { return [false, result.error, payload]; } const statusMap = { delivered: 'delivered', pending: 'pending-acceptance', request_created: 'request-created', blocked: 'blocked', spam: 'labeled-spam', rate_limited: 'rate-limited' }; const reason = statusMap[result.status] || result.status; const ok = result.status === 'delivered'; return [ok, reason, payload]; } async acceptRequest(senderDid) { const res = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.accept`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.accessToken}` }, body: JSON.stringify({ did: senderDid }) }); if (res.ok) { await this.syncState(); this.accepted.add(senderDid); return true; } return false; } async rejectRequest(senderDid) { const res = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.reject`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.accessToken}` }, body: JSON.stringify({ did: senderDid }) }); if (res.ok) { await this.syncState(); this.blocked.add(senderDid); return true; } return false; } async unblockSender(senderDid) { const res = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.unblock`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.accessToken}` }, body: JSON.stringify({ did: senderDid }) }); if (res.ok) { await this.syncState(); this.blocked.delete(senderDid); return true; } return false; } } const LABELER_URL = 'https://spam-labeler.nate-8fe.workers.dev'; export class LabelerClient { constructor() { // cache of spam-labeled DIDs (synced with remote) this.spamDids = new Set(); } async addLabel(did, label) { if (label !== 'spam') return; try { const res = await fetch(`${LABELER_URL}/xrpc/xyz.fake.labeler.addSpam`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ did }) }); if (res.ok) { this.spamDids.add(did); } } catch (e) { console.error('labeler addLabel failed:', e); } } async removeLabel(did, label) { if (label !== 'spam') return; try { const res = await fetch(`${LABELER_URL}/xrpc/xyz.fake.labeler.removeSpam`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ did }) }); if (res.ok) { this.spamDids.delete(did); } } catch (e) { console.error('labeler removeLabel failed:', e); } } hasLabel(did, label) { if (label !== 'spam') return false; return this.spamDids.has(did); } async sync() { try { const res = await fetch(`${LABELER_URL}/xrpc/xyz.fake.labeler.listSpam`); if (res.ok) { const { dids } = await res.json(); this.spamDids = new Set(dids); } } catch (e) { console.error('labeler sync failed:', e); } } } export async function createClients() { const clients = {}; for (const [name, creds] of Object.entries(CREDENTIALS)) { clients[name] = new PDSClient(name, creds); await clients[name].init(); } return clients; }