+83
-224
README.md
+83
-224
README.md
···
6
6
7
7
**live demo**: [sites.wisp.place/zzstoatzz.io/pds-message-poc](https://sites.wisp.place/zzstoatzz.io/pds-message-poc)
8
8
9
-
## architecture
9
+
## what it does
10
+
11
+
- two separate PDSes exchange messages via custom XRPC endpoints (`xyz.fake.inbox.*`)
12
+
- first contact requires acceptance, like DM requests
13
+
- sender's PDS signs messages with service auth JWTs
14
+
- recipient's PDS verifies by resolving sender's DID via plc.directory
15
+
- separate labeler service can mark senders as spam
16
+
17
+
## run locally
10
18
11
-
this demo uses a real PDS deployment:
19
+
```bash
20
+
git submodule update --init
21
+
bun install
22
+
bun dev
23
+
```
24
+
25
+
<details>
26
+
<summary>architecture</summary>
27
+
28
+
this demo uses **two separate PDS deployments** for real cross-server messaging:
12
29
13
-
- **pds.js fork** deployed to Cloudflare Workers at `pds-message-demo.nate-8fe.workers.dev`
14
-
- **real DIDs** registered with [plc.directory](https://plc.directory)
15
-
- **real service auth** - JWTs signed server-side via `com.atproto.server.getServiceAuth`
16
-
- **real signature verification** - recipient PDS resolves sender DID via PLC to get public key
30
+
- **alice's PDS**: `pds-message-demo.nate-8fe.workers.dev`
31
+
- **bob's PDS**: `pds-message-demo-2.nate-8fe.workers.dev`
32
+
- **spam labeler**: `did:plc:x6io7svnbth4pikg2e63vvkx`
17
33
18
34
```
19
-
┌─────────────────┐ ┌─────────────────┐
20
-
│ Browser │ │ PDS Worker │
21
-
│ (demo UI) │ │ (Cloudflare) │
22
-
├─────────────────┤ ├─────────────────┤
23
-
│ │ 1. createSession(bob) │ │
24
-
│ │ ────────────────────────────>│ bob's DO │
25
-
│ │ ← accessJwt │ │
26
-
│ │ │ │
27
-
│ │ 2. getServiceAuth(aud=alice)│ │
28
-
│ │ ────────────────────────────>│ signs JWT │
29
-
│ │ ← service JWT │ server-side │
30
-
│ │ │ │
31
-
│ │ 3. inbox.send + JWT │ │
32
-
│ │ ────────────────────────────>│ alice's DO: │
33
-
│ │ │ - resolve DID │
34
-
│ │ │ - verify sig │
35
-
│ │ │ - check spam │
36
-
│ │ │ - deliver/queue│
37
-
│ │ ← {status: ...} │ │
38
-
└─────────────────┘ └─────────────────┘
39
-
│
40
-
▼
41
-
┌───────────────┐
42
-
│ plc.directory │
43
-
│ (DID → pubkey)│
44
-
└───────────────┘
35
+
browser bob's PDS alice's PDS
36
+
│ │ │
37
+
│ 1. createSession(bob) │ │
38
+
│ ──────────────────────────>│ │
39
+
│ <- accessJwt │ │
40
+
│ │ │
41
+
│ 2. getServiceAuth │ │
42
+
│ (aud=alice's DID) │ │
43
+
│ ──────────────────────────>│ │
44
+
│ <- service JWT │ (signs w/ bob's key) │
45
+
│ │ │
46
+
│ 3. xyz.fake.inbox.send │ │
47
+
│ + service JWT ─────────────────────────────────────>│
48
+
│ │ │
49
+
│ │ 4. resolve bob's DID │
50
+
│ │ via plc.directory │
51
+
│ │ 5. verify JWT │
52
+
│ │ 6. check spam label │
53
+
│ │ 7. deliver/queue │
54
+
│ <- {status: ...} │ │
45
55
```
46
56
47
-
## run locally
57
+
alice's PDS verifies bob's identity by resolving his DID via plc.directory - no way to forge the sender.
58
+
59
+
</details>
60
+
61
+
<details>
62
+
<summary>deploy</summary>
63
+
64
+
each PDS worker requires two secrets set via wrangler:
48
65
49
66
```bash
50
-
git submodule update --init
51
-
npm install
52
-
npm run dev
67
+
wrangler secret put PDS_PASSWORD --config pds-alice.toml
68
+
wrangler secret put JWT_SECRET --config pds-alice.toml
69
+
wrangler secret put PDS_PASSWORD --config pds-bob.toml
70
+
wrangler secret put JWT_SECRET --config pds-bob.toml
71
+
```
72
+
73
+
then deploy:
74
+
75
+
```bash
76
+
wrangler deploy --config pds-alice.toml
77
+
wrangler deploy --config pds-bob.toml
53
78
```
54
79
55
-
## usage
80
+
</details>
81
+
82
+
<details>
83
+
<summary>usage</summary>
56
84
57
85
- type a message, select sender → recipient
58
86
- **send** - initiates message (first message creates a request)
···
60
88
- **reject** - recipient rejects request and blocks sender
61
89
- **spam** - labeler marks sender as spam (rejected by all PDSes)
62
90
63
-
## invitation flow
91
+
**invitation flow**: first contact requires acceptance (like DM requests). bob sends to alice → request created → alice accepts → message delivered. subsequent messages from bob deliver immediately.
64
92
65
-
first contact requires acceptance (like DM requests):
93
+
</details>
66
94
67
-
1. bob sends message to alice → creates **request** (message held)
68
-
2. alice sees request in her "requests" section
69
-
3. alice clicks **accept** → original message delivered, bob now accepted
70
-
4. subsequent messages from bob deliver immediately (subject to rate limits)
71
-
72
-
alternatively:
73
-
- alice clicks **reject** → request deleted, bob blocked permanently
74
-
75
-
## what's real
95
+
<details>
96
+
<summary>what's real</summary>
76
97
77
98
| component | implementation |
78
99
|-----------|----------------|
79
-
| PDS | [pds.js](https://tangled.org/chadtmiller.com/pds.js) fork on Cloudflare Workers |
100
+
| PDSes | two [pds.js](https://tangled.org/chadtmiller.com/pds.js) deployments on Cloudflare Workers |
80
101
| DIDs | real `did:plc` registered with [plc.directory](https://plc.directory) |
81
102
| service auth | server-side JWT signing via `com.atproto.server.getServiceAuth` |
82
103
| signature verification | PLC resolution → public key → ES256 verify |
83
-
| invitation flow | persistent in Durable Object SQLite |
84
-
| block list | persistent per-user |
85
-
86
-
## what's demonstrated
87
-
88
-
| feature | implementation | ATProto pattern |
89
-
|---------|----------------|-----------------|
90
-
| service auth | JWT with iss/aud/exp/lxm | [com.atproto.server.getServiceAuth](https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/server/getServiceAuth.json) |
91
-
| invitation flow | pending/accepted sets | similar to `chat.bsky.convo` request status |
92
-
| reputation | labeler with spam labels | [com.atproto.label](https://github.com/bluesky-social/atproto/tree/main/lexicons/com/atproto/label) |
93
-
| block list | per-user set | existing pattern |
94
-
| rate limiting | per-sender, time-windowed | existing pattern |
95
-
96
-
## what's simplified
104
+
| labeler | real ATProto labeler with secp256k1 signing |
105
+
| cross-PDS messaging | bob's PDS signs, alice's PDS verifies via DID resolution |
97
106
98
-
| component | current | path to production |
99
-
|-----------|---------|-------------------|
100
-
| labeler | in-memory (browser) | [ozone](https://github.com/bluesky-social/atproto/tree/main/packages/ozone) |
101
-
| accounts | 3 demo users (alice/bob/charlie) | real account creation |
102
-
| encryption | none (messages in plaintext) | E2EE layer |
107
+
</details>
103
108
104
-
## pds.js modifications
109
+
<details>
110
+
<summary>pds.js modifications</summary>
105
111
106
112
our fork adds:
107
-
- `xyz.fake.inbox.*` XRPC endpoints (send, list, listRequests, accept, reject)
113
+
- `xyz.fake.inbox.*` XRPC endpoints (send, list, listRequests, accept, reject, unblock, getState)
108
114
- inbox tables in SQLite schema
109
115
- PLC resolution for DID → public key during JWT verification
110
116
- `com.atproto.server.getServiceAuth` for server-side JWT signing
117
+
- spam labeler check before message delivery
111
118
112
-
## prior art
119
+
</details>
120
+
121
+
<details>
122
+
<summary>references</summary>
113
123
124
+
- [jacob.gold's thread](https://bsky.app/profile/jacob.gold/post/3mbsbqsc3vc24)
125
+
- [pds.js](https://tangled.org/chadtmiller.com/pds.js) - cloudflare workers PDS
114
126
- [AT Protocol and SMTP](https://ngerakines.leaflet.pub/3lxxk3oahzc2f) - ngerakines on PDS as crypto service
115
127
- [bourbon protocol](https://blog.boscolo.co/3lzj5po423s2g) - invitation-based messaging
116
128
- [How Streamplace Works](https://stream.place/blog/how-streamplace-works-embedded-pds) - embedded PDS pattern
117
129
118
-
<details>
119
-
<summary><strong>how it actually works</strong></summary>
120
-
121
-
### the setup
122
-
123
-
three users exist on the same PDS deployment:
124
-
125
-
```
126
-
alice.pds-message-demo.nate-8fe.workers.dev → did:plc:cmadossymmii3izkabdbp5en
127
-
bob.pds-message-demo.nate-8fe.workers.dev → did:plc:deeom7pq4ynuigyr2p562vxz
128
-
charlie.pds-message-demo.nate-8fe.workers.dev → did:plc:c6qmjdpyg6uoqnb6uoxt5omb
129
-
```
130
-
131
-
each DID is registered with [plc.directory](https://plc.directory), which maps DIDs to public keys. this is real - you can verify them:
132
-
133
-
```bash
134
-
curl https://plc.directory/did:plc:deeom7pq4ynuigyr2p562vxz
135
-
```
136
-
137
-
### what happens when bob sends a message to alice
138
-
139
-
```
140
-
┌─────────────┐ ┌──────────────────────────┐ ┌─────────────┐
141
-
│ browser │ │ cloudflare worker │ │ plc.directory│
142
-
│ (demo UI) │ │ (pds.js) │ │ │
143
-
└──────┬──────┘ └────────────┬─────────────┘ └──────┬──────┘
144
-
│ │ │
145
-
│ 1. createSession(bob) │ │
146
-
│ ───────────────────────────>│ │
147
-
│ ← accessJwt │ │
148
-
│ │ │
149
-
│ 2. getServiceAuth │ │
150
-
│ (aud=alice's DID) │ │
151
-
│ ───────────────────────────>│ │
152
-
│ │ bob's Durable Object: │
153
-
│ │ - looks up bob's private key│
154
-
│ │ - signs JWT with ES256 │
155
-
│ ← service JWT │ - iss=bob, aud=alice │
156
-
│ (signed by bob) │ │
157
-
│ │ │
158
-
│ 3. xyz.fake.inbox.send │ │
159
-
│ + service JWT │ │
160
-
│ ───────────────────────────>│ │
161
-
│ │ routed to alice's DO: │
162
-
│ │ │
163
-
│ │ 4. resolve sender DID │
164
-
│ │ ────────────────────────────>│
165
-
│ │ ← bob's public key │
166
-
│ │ │
167
-
│ │ 5. verify JWT signature │
168
-
│ │ (ES256 with bob's pubkey)│
169
-
│ │ │
170
-
│ │ 6. check inbox rules: │
171
-
│ │ - is bob blocked? no │
172
-
│ │ - is bob accepted? no │
173
-
│ │ → create request │
174
-
│ │ │
175
-
│ ← {status: "request_created"} │
176
-
│ │ │
177
-
```
178
-
179
-
### the key insight: PDS as cryptographic service
180
-
181
-
following ngerakines' framing, the PDS acts as a "cryptographic service" - it holds private keys and signs on behalf of users. the browser never sees private keys.
182
-
183
-
```
184
-
browser knows: PDS knows:
185
-
- handle - handle
186
-
- DID - DID
187
-
- session token - session token
188
-
- private signing key ← this is the important part
189
-
```
190
-
191
-
when bob wants to prove his identity to alice, he asks his PDS to sign a JWT. alice's PDS verifies it by:
192
-
1. parsing the `iss` claim to get bob's DID
193
-
2. resolving bob's DID via plc.directory to get his public key
194
-
3. verifying the signature
195
-
196
-
this is exactly how service auth works in AT Protocol - we just applied it to messaging.
197
-
198
-
### durable objects as isolated PDSes
199
-
200
-
[pds.js](https://tangled.org/chadtmiller.com/pds.js) uses Cloudflare Durable Objects, where each user gets their own isolated DO with its own SQLite database:
201
-
202
-
```
203
-
cloudflare worker
204
-
├── alice's DO
205
-
│ └── SQLite: inbox_messages, inbox_accepted, inbox_blocked, inbox_requests
206
-
├── bob's DO
207
-
│ └── SQLite: inbox_messages, inbox_accepted, inbox_blocked, inbox_requests
208
-
└── charlie's DO
209
-
└── SQLite: inbox_messages, inbox_accepted, inbox_blocked, inbox_requests
210
-
```
211
-
212
-
when a message arrives at `xyz.fake.inbox.send`, the worker routes it to the recipient's DO based on the `aud` claim in the JWT. each DO is effectively an independent PDS with its own state.
213
-
214
-
### the invitation flow
215
-
216
-
first contact creates a request (like DM requests on most platforms):
217
-
218
-
```sql
219
-
-- alice's DO when bob sends first message
220
-
INSERT INTO inbox_requests (fromDid, text, createdAt) VALUES ('did:plc:bob...', 'hey alice', '...')
221
-
```
222
-
223
-
when alice accepts:
224
-
225
-
```sql
226
-
-- move to accepted list
227
-
INSERT INTO inbox_accepted (did) VALUES ('did:plc:bob...')
228
-
-- deliver held message
229
-
INSERT INTO inbox_messages (fromDid, text, createdAt) SELECT fromDid, text, createdAt FROM inbox_requests WHERE fromDid = 'did:plc:bob...'
230
-
-- remove request
231
-
DELETE FROM inbox_requests WHERE fromDid = 'did:plc:bob...'
232
-
```
233
-
234
-
subsequent messages from bob deliver immediately (skipping the request stage).
235
-
236
-
### XRPC endpoints we added
237
-
238
-
```
239
-
xyz.fake.inbox.send POST send message (requires service auth JWT)
240
-
xyz.fake.inbox.list GET list inbox messages (requires session auth)
241
-
xyz.fake.inbox.listRequests GET list pending requests (requires session auth)
242
-
xyz.fake.inbox.accept POST accept a request (requires session auth)
243
-
xyz.fake.inbox.reject POST reject and block (requires session auth)
244
-
```
245
-
246
-
plus the standard AT Protocol endpoint:
247
-
248
-
```
249
-
com.atproto.server.getServiceAuth GET get a signed JWT for service-to-service auth
250
-
```
251
-
252
-
### why this matters
253
-
254
-
the demo shows that PDS-to-PDS messaging is possible with existing AT Protocol primitives:
255
-
256
-
1. **identity** - DIDs provide portable, cryptographically-verifiable identity
257
-
2. **service auth** - JWTs let services prove who's making requests
258
-
3. **PLC resolution** - public key discovery without trusting the sender's PDS
259
-
4. **durable objects** - each user's inbox is isolated and persistent
260
-
261
-
no new cryptography needed. no blockchain. just the existing AT Protocol stack applied to a new use case.
262
-
263
130
</details>
264
-
265
-
## references
266
-
267
-
- [jacob.gold's thread](https://bsky.app/profile/jacob.gold/post/3mbsbqsc3vc24)
268
-
- [pds.js](https://tangled.org/chadtmiller.com/pds.js) - cloudflare workers PDS
269
-
- [official PDS](https://github.com/bluesky-social/atproto/tree/main/packages/pds)
270
-
- [service auth](https://github.com/bluesky-social/atproto/blob/main/packages/xrpc-server/src/auth.ts)
271
-
- [AT Protocol specs](https://atproto.com/specs/atp)
+15
pds-alice.toml
+15
pds-alice.toml
···
1
+
name = "pds-message-demo"
2
+
main = "vendor/pds.js/src/pds.js"
3
+
compatibility_date = "2024-01-01"
4
+
5
+
[[durable_objects.bindings]]
6
+
name = "PDS"
7
+
class_name = "PersonalDataServer"
8
+
9
+
[[migrations]]
10
+
tag = "v1"
11
+
new_sqlite_classes = [ "PersonalDataServer" ]
12
+
13
+
[[r2_buckets]]
14
+
binding = "BLOBS"
15
+
bucket_name = "pds-blobs"
+15
pds-bob.toml
+15
pds-bob.toml
···
1
+
name = "pds-message-demo-2"
2
+
main = "vendor/pds.js/src/pds.js"
3
+
compatibility_date = "2024-01-01"
4
+
5
+
[[durable_objects.bindings]]
6
+
name = "PDS"
7
+
class_name = "PersonalDataServer"
8
+
9
+
[[migrations]]
10
+
tag = "v1"
11
+
new_sqlite_classes = [ "PersonalDataServer" ]
12
+
13
+
[[r2_buckets]]
14
+
binding = "BLOBS"
15
+
bucket_name = "pds-message-demo-2-blobs"
+36
-18
src/lib/client.js
+36
-18
src/lib/client.js
···
1
1
/**
2
-
* PDS client - wraps XRPC calls to the deployed pds.js instance
2
+
* PDS client - wraps XRPC calls to deployed pds.js instances
3
+
*
4
+
* alice is on pds-message-demo, bob is on pds-message-demo-2
5
+
* this demonstrates real PDS-to-PDS messaging across different servers
3
6
*/
4
7
5
-
const PDS_URL = 'https://pds-message-demo.nate-8fe.workers.dev';
6
8
const PDS_PASSWORD = 'pds-message-demo-2026';
7
9
8
10
const CREDENTIALS = {
9
11
alice: {
10
12
handle: 'alice.pds-message-demo.nate-8fe.workers.dev',
11
-
did: 'did:plc:cmadossymmii3izkabdbp5en'
13
+
did: 'did:plc:cmadossymmii3izkabdbp5en',
14
+
pdsUrl: 'https://pds-message-demo.nate-8fe.workers.dev'
12
15
},
13
16
bob: {
14
-
handle: 'bob.pds-message-demo.nate-8fe.workers.dev',
15
-
did: 'did:plc:deeom7pq4ynuigyr2p562vxz'
16
-
},
17
-
charlie: {
18
-
handle: 'charlie.pds-message-demo.nate-8fe.workers.dev',
19
-
did: 'did:plc:c6qmjdpyg6uoqnb6uoxt5omb'
17
+
handle: 'bob.pds-message-demo-2.nate-8fe.workers.dev',
18
+
did: 'did:plc:deeom7pq4ynuigyr2p562vxz',
19
+
pdsUrl: 'https://pds-message-demo-2.nate-8fe.workers.dev'
20
20
}
21
21
};
22
22
23
+
// resolve DID to PDS URL (for cross-PDS messaging)
24
+
async function resolvePdsUrl(did) {
25
+
// check local cache first
26
+
for (const creds of Object.values(CREDENTIALS)) {
27
+
if (creds.did === did) return creds.pdsUrl;
28
+
}
29
+
// fallback: resolve via plc.directory
30
+
const res = await fetch(`https://plc.directory/${did}`);
31
+
if (!res.ok) throw new Error(`Failed to resolve DID: ${did}`);
32
+
const doc = await res.json();
33
+
return doc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint;
34
+
}
35
+
23
36
export class PDSClient {
24
37
constructor(name, creds) {
25
38
this.name = name;
26
39
this.did = creds.did;
27
40
this.handle = creds.handle;
41
+
this.pdsUrl = creds.pdsUrl;
28
42
29
43
this.inbox = [];
30
44
this.pending = new Map();
···
35
49
}
36
50
37
51
async init() {
38
-
const res = await fetch(`${PDS_URL}/xrpc/com.atproto.server.createSession`, {
52
+
const res = await fetch(`${this.pdsUrl}/xrpc/com.atproto.server.createSession`, {
39
53
method: 'POST',
40
54
headers: { 'Content-Type': 'application/json' },
41
55
body: JSON.stringify({
···
54
68
}
55
69
56
70
async syncState() {
57
-
const inboxRes = await fetch(`${PDS_URL}/xrpc/xyz.fake.inbox.list`, {
71
+
const inboxRes = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.list`, {
58
72
headers: { Authorization: `Bearer ${this.accessToken}` }
59
73
});
60
74
if (inboxRes.ok) {
···
66
80
}));
67
81
}
68
82
69
-
const reqRes = await fetch(`${PDS_URL}/xrpc/xyz.fake.inbox.listRequests`, {
83
+
const reqRes = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.listRequests`, {
70
84
headers: { Authorization: `Bearer ${this.accessToken}` }
71
85
});
72
86
if (reqRes.ok) {
···
79
93
);
80
94
}
81
95
82
-
const stateRes = await fetch(`${PDS_URL}/xrpc/xyz.fake.inbox.getState`, {
96
+
const stateRes = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.getState`, {
83
97
headers: { Authorization: `Bearer ${this.accessToken}` }
84
98
});
85
99
if (stateRes.ok) {
···
93
107
const params = new URLSearchParams({ aud: audienceDid });
94
108
if (lxm) params.set('lxm', lxm);
95
109
96
-
const res = await fetch(`${PDS_URL}/xrpc/com.atproto.server.getServiceAuth?${params}`, {
110
+
const res = await fetch(`${this.pdsUrl}/xrpc/com.atproto.server.getServiceAuth?${params}`, {
97
111
headers: { Authorization: `Bearer ${this.accessToken}` }
98
112
});
99
113
···
106
120
}
107
121
108
122
async sendMessage(recipientDid, text) {
123
+
// get service auth JWT from OUR PDS
109
124
const jwt = await this.getServiceAuth(recipientDid, 'xyz.fake.inbox.send');
110
125
111
-
const res = await fetch(`${PDS_URL}/xrpc/xyz.fake.inbox.send`, {
126
+
// resolve recipient's PDS and send the message THERE (cross-PDS!)
127
+
const recipientPdsUrl = await resolvePdsUrl(recipientDid);
128
+
129
+
const res = await fetch(`${recipientPdsUrl}/xrpc/xyz.fake.inbox.send`, {
112
130
method: 'POST',
113
131
headers: {
114
132
'Content-Type': 'application/json',
···
143
161
}
144
162
145
163
async acceptRequest(senderDid) {
146
-
const res = await fetch(`${PDS_URL}/xrpc/xyz.fake.inbox.accept`, {
164
+
const res = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.accept`, {
147
165
method: 'POST',
148
166
headers: {
149
167
'Content-Type': 'application/json',
···
161
179
}
162
180
163
181
async rejectRequest(senderDid) {
164
-
const res = await fetch(`${PDS_URL}/xrpc/xyz.fake.inbox.reject`, {
182
+
const res = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.reject`, {
165
183
method: 'POST',
166
184
headers: {
167
185
'Content-Type': 'application/json',
···
179
197
}
180
198
181
199
async unblockSender(senderDid) {
182
-
const res = await fetch(`${PDS_URL}/xrpc/xyz.fake.inbox.unblock`, {
200
+
const res = await fetch(`${this.pdsUrl}/xrpc/xyz.fake.inbox.unblock`, {
183
201
method: 'POST',
184
202
headers: {
185
203
'Content-Type': 'application/json',
+2
src/lib/components/PdsPanel.svelte
+2
src/lib/components/PdsPanel.svelte
+24
-6
src/routes/+page.svelte
+24
-6
src/routes/+page.svelte
···
177
177
<select id="sender" bind:value={senderHandle}>
178
178
<option value="alice">alice</option>
179
179
<option value="bob">bob</option>
180
-
<option value="charlie">charlie</option>
181
180
</select>
182
181
</div>
183
182
<button class="swap" onclick={swap} aria-label="swap sender and recipient">⇄</button>
···
186
185
<select id="recipient" bind:value={recipientHandle}>
187
186
<option value="alice">alice</option>
188
187
<option value="bob">bob</option>
189
-
<option value="charlie">charlie</option>
190
188
</select>
191
189
</div>
192
190
</div>
···
250
248
251
249
<section class="infrastructure">
252
250
<h3>infrastructure</h3>
253
-
<p class="infra-desc">this demo is backed by real ATProto services</p>
251
+
<p class="infra-desc">this demo uses two separate PDSes for real cross-server messaging</p>
254
252
<div class="infra-grid">
255
253
<a href="https://pds-message-demo.nate-8fe.workers.dev/xrpc/com.atproto.server.describeServer" target="_blank" class="infra-card">
256
254
<div class="infra-icon">
···
262
260
</svg>
263
261
</div>
264
262
<div class="infra-content">
265
-
<div class="infra-label">personal data server</div>
263
+
<div class="infra-label">alice's pds</div>
266
264
<div class="infra-value">pds-message-demo.nate-8fe.workers.dev</div>
267
265
<div class="infra-detail">cloudflare worker + durable objects</div>
268
266
</div>
269
267
</a>
270
268
269
+
<a href="https://pds-message-demo-2.nate-8fe.workers.dev/xrpc/com.atproto.server.describeServer" target="_blank" class="infra-card">
270
+
<div class="infra-icon">
271
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
272
+
<rect x="2" y="3" width="20" height="18" rx="2"/>
273
+
<line x1="2" y1="9" x2="22" y2="9"/>
274
+
<circle cx="6" cy="6" r="1" fill="currentColor"/>
275
+
<circle cx="10" cy="6" r="1" fill="currentColor"/>
276
+
</svg>
277
+
</div>
278
+
<div class="infra-content">
279
+
<div class="infra-label">bob's pds</div>
280
+
<div class="infra-value">pds-message-demo-2.nate-8fe.workers.dev</div>
281
+
<div class="infra-detail">cloudflare worker + durable objects</div>
282
+
</div>
283
+
</a>
284
+
271
285
<a href="https://plc.directory/did:plc:x6io7svnbth4pikg2e63vvkx" target="_blank" class="infra-card">
272
286
<div class="infra-icon">
273
287
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
···
323
337
324
338
.container {
325
339
display: grid;
326
-
grid-template-columns: 1fr 2fr 1fr;
340
+
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr);
327
341
gap: 1rem;
328
342
max-width: 1000px;
329
343
margin: 0 auto;
···
334
348
background: #111;
335
349
border: 1px solid #222;
336
350
padding: 1rem;
351
+
overflow: hidden;
352
+
min-width: 0;
337
353
}
338
354
.center h2 {
339
355
font-size: 11px;
···
497
513
color: #444;
498
514
font-size: 12px;
499
515
align-self: stretch;
516
+
overflow: hidden;
517
+
min-width: 0;
500
518
}
501
519
502
520
.state-summary {
···
548
566
}
549
567
.infra-grid {
550
568
display: grid;
551
-
grid-template-columns: repeat(3, 1fr);
569
+
grid-template-columns: repeat(2, 1fr);
552
570
gap: 1rem;
553
571
}
554
572
.infra-card {