this repo has no description sites.wisp.place/zzstoatzz.io/pds-message-poc
pds messaging

add detailed explanation in collapsible section

covers the full flow: session auth → service auth → PLC resolution →
signature verification → inbox rules. includes sequence diagram,
SQL examples, and references ngerakines' PDS-as-crypto-service framing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+147
+147
README.md
··· 115 115 - [bourbon protocol](https://blog.boscolo.co/3lzj5po423s2g) - invitation-based messaging 116 116 - [How Streamplace Works](https://stream.place/blog/how-streamplace-works-embedded-pds) - embedded PDS pattern 117 117 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 + </details> 264 + 118 265 ## references 119 266 120 267 - [jacob.gold's thread](https://bsky.app/profile/jacob.gold/post/3mbsbqsc3vc24)