podcast manager
at main 419 lines 13 kB view raw
1#+PROPERTY: COOKIE_DATA recursive 2#+STARTUP: overview 3 4* concepts [0/5] 5 6the skypod architecture is broken into pieces: 7 8** p2p with realms 9 10In order to sync device and playback state, we need to have devices communicate with each 11other, which requires a signaling server, for peer discovery at the very least. 12 13*** realm 14 15A realm is a collection of known identities, verified with signed JWTs, where those 16identities can communicate outside of the server's purview. 17 18A realm is not publicly routable; to gain access, one must have the realm id and an 19invitation to the realm from an already existing member; new realms are created on 20demand with a random realmid supplied by the client. 21 22**** realm server 23 24A realm at the signalling sever is only a collection of known identity public keys, and 25the currently connected sockets. 26 27It acts mostly as a smart router; socket connection, authentication, and directed and 28realm-wide broadcast. 29 30**** realm client 31 32The realm client manages a full-mesh p2p network with other connected peers in the realm, 33and exposes the combined set of incoming messages as a stream that the client can do 34whatever with. 35 36***** realm encryption 37 38In order to keep private data off of the server, the realm client takes on the additional 39task of maintaining a shared encryption key for the realm, which can be used to encrypt 40data going over broadcasts. 41 42****** TODO key exchange protocol 43 44*** identity 45 46Identity in the realm system is just an id and keypair. 47 48The private key stays local to the installed device, and is used to send signed tokens 49over the wire, either to the realm server to manage authentication, or over other channels 50to other members in the realm. 51 52The /public/ key is stored by all members of the realm, and the server, in order to 53perform signature validation (which is also authentication). 54 55**** browser private key storage 56 57There is no good way to store private keys in a browser, but there are less bad ways. 58 59- private keys are ~CryptoKey~ objects, with ~{ exportable: false }~ 60- WebCrypto native ~CryptoKey~ are structured clonable, which means they can get saved to indexeddb 61 62At the end of the day this is a podcast app. 63 64***** TODO are there other ways to do this? 65 66Could we use webauthn, or some way to use a yubikey to sign something? 67 68*** connection flow 69 70#+begin_src mermaid :file docs/readme/connection-flow.png 71 sequenceDiagram 72 participant Server as Server 73 actor Client as Client 74 participant PeerA as Existing Peer A 75 participant PeerB as Existing Peer B 76 77 Client<<->>Server: WebSocket Connection 78 79 Note over Client, Server: Authentication Phase (3 second timeout) 80 alt Registration (new realm) 81 Client->>Server: JWT: preauth.register {pubkey} 82 Server->>Server: validate JWT signature 83 Server->>Server: ensureRegisteredRealm(realmid, identid, pubkey) 84 else Authentication (existing realm) 85 Client->>Server: JWT: preauth.authn {} 86 Server->>Server: get publickey for issuer 87 Server->>Server: validate JWT signature 88 else Invitation Exchange 89 Client->>Server: JWT: preauth.exchange {inviteJwt, pubkey} 90 Server->>Server: get publickey for issuer 91 Server->>Server: validate invitation JWT signature & nonce 92 Server->>Server: admitToRealm(realmid, identid, pubkey) 93 Server->>Server: identity admitted 94 end 95 96 Note over PeerB, Server: Authenticated! Exchange Peer Identities 97 par do 98 Server->>Client: preauth.authn response {peers[], identities{}} 99 and 100 Server--)PeerA: realm.rtc.peer-joined {identid, pubkey} 101 and 102 Server--)PeerB: realm.rtc.peer-joined {identid, pubkey} 103 end 104 105 Note over Client, PeerB: WebRTC Connections (initiated by Client) 106 par do per connected peer 107 Client->>+Server: realm.rtc.signal {signed_jwt, localid, remoteid} 108 Server->>PeerA: realm.rtc.signal {signed_jwt, localid, remoteid} 109 PeerA->>PeerA: verify JWT signature (pubkey from `peer-joined`) 110 PeerA->>Server: realm.rtc.signal {signed_answer_jwt, localid, remoteid} 111 Server->>-Client: realm.rtc.signal {signed_answer_jwt, localid, remoteid} 112 Client->>Client: verify JWT signature (pubkey from `authn` response) 113 114 Note over Client, PeerA: Direct P2P Connection Established 115 Client->>PeerA: Direct WebRTC Connection 116 loop every 30s 117 PeerA<<->>Client: realm.rtc.ping/pong 118 PeerA<<->>Client: application messages 119 end 120 end 121 122 Note over PeerB, Server: Message Flow (Operational) 123 Client->>PeerA: Application Data (p2p) 124 Client->>PeerB: Application Data (p2p) 125 Client->>Server: realm.broadcast {payload, recipients} 126 Server->>PeerB: realm.broadcast {payload} 127 128 Note over PeerB, Server: Connection Persistence & Error Handling 129 alt Socket Error/Close 130 Client->>Client: Connection destroyed 131 Server->>PeerA: realm.rtc.peer-left {identid} 132 Server->>PeerB: realm.rtc.peer-left {identid} 133 PeerA->>PeerA: Disconnect peer 134 PeerB->>PeerB: Disconnect peer 135 else Peer Leaves 136 PeerA->>Server: Connection closes 137 Server->>Client: realm.rtc.peer-left {peerA_identid} 138 Server->>PeerB: realm.rtc.peer-left {peerA_identid} 139 Client->>Client: Disconnect from PeerA 140 end 141#+end_src 142 143#+RESULTS: 144[[file:docs/readme/connection-flow.png]] 145 146*** message format 147 148A message consists of: 149 150- ~typ~ :: the message type, see below 151- ~msg~ :: a message key, used for discrimination on the payload 152- ~seq?~ :: an (optional) sequence number, which allows for request/reply semantics 153- ~dat?~ :: a payload, the schema for which depends on ~typ~ and ~msg~ (possibly omitted) 154 155See ~#common/protocol~ for the messages that have been defined. 156 157**** ~typ~ 158 159- ~evt~ :: events - one-way notifications 160- ~req~ :: requests - request-response pattern with sequence matching 161- ~res~ :: responses - correlated responses to requests by ~seq~ 162- ~err~ :: errors - structured errors with HTTP-style status codes 163 164**** examples 165 166#+begin_example 167{ 168 "typ": "req", 169 "msg": "preauth.register", 170 "seq": 1234, 171 "dat": { 172 "pubkey": { ... } 173 } 174} 175#+end_example 176 177#+begin_example 178{ 179 "typ": "req", 180 "msg": "preauth.authn", 181 "seq": 1234 182} 183#+end_example 184 185#+begin_example 186{ 187 "typ": "res", 188 "msg": "preauth.authn", 189 "seq": 1234, 190 "dat": { 191 "identities": { ... }, 192 "peers": { ... } 193 } 194} 195#+end_example 196 197*** message authentication 198 199A client authenticates to the system and to peers by exchanging signed JWTs as the first 200messages in the messaging protocol; for authenticating to the realm, and for exchanging 201webrtc signaling messages. 202 203**** TODO webrtc encryption? 204 205The websocket is encrypted by virtue of ~wss~ and the server's cert, but I'm not sure if 206the traffic over webrtc is encrypted; if it's not, we should figure that out. 207 208** feed proxy server 209 210Due to ~CORS~, we'll need to help clients fetch the contents of feeds by running a caching 211proxy server for various HTTP requests. 212 213- help bypass ~CORS~ restrictions, so clients can access the content of the response 214- cache feeds, especially with regards to running transformations 215- perform transformations on responses: 216 - text feeds: reader mode, detect reading time 217 - podcast feeds: extract episode metadata, audio analysis for silence skips, etc 218 - all feeds: extract title tags, etc. 219 220*** TODO open question: is the client able to not use the proxy? 221 222I'm not sure yet if we want the PWA to be able to pull feeds directly when the server 223isn't present. It would be much easier to keep it around, but 224 225** feed management 226 227With a solid p2p WebRTC connection, we can use something like ~dexie~ or ~rxdb~ to get a 228synced document database that we use to manage feeds. 229 230* flow 231 232- user goes to https://skypod.accidental.cc 233 - pwa runs, prompts to do full install for storage and offline 234 - pwa is installed, sets up caches 235 236- first run 237 - identity is generated (id + keypair per device) 238 - do you want to sync to an existing install? 239 - if yes, go to invitee flow 240 - otherwise, new realm is generated and registered 241 - pubkey and id get stored in the realm, to make future sync easier 242 243- subsequent runs 244 - identity already exists, so we just go about our day 245 246- invitee flow 247 - already generated identity 248 - qr code pops 249 - scanned by inviter, see inviter flow 250 - done button after 251 - camera pops, scan inviter's QR codes 252 - sends invitation+registration token to server 253 - added to the realm 254 - go to subsequent runs 255 256* WebRTC Full-Mesh Implementation Plan 257 258** Overview 259 260Implement a full-mesh WebRTC system where every client in a realm establishes direct 261peer-to-peer connections with all other clients. The existing WebSocket infrastructure 262serves as the signaling channel, leveraging the newly refactored broadcast protocol. 263 264** Architecture Components 265 266*** Protocol Extensions (src/common/protocol/messages-rtc.js) 267 268Create new WebRTC message schemas: 269- rtc.offer - SDP offer with connectionId 270- rtc.answer - SDP answer with connectionId 271- rtc.ice-candidate - ICE candidate exchange 272- rtc.peer-state - Connection state updates 273- rtc.request-connection - Initiate connection with polite flag 274- rtc.peer-joined - Server message when peer joins (includes member list) 275- rtc.peer-left - Server message when peer leaves 276 277*** Server-Side Updates 278 279**** broadcastToRealm Function Enhancement 280 281Update the function signature to: 282- Take complete messages instead of payloads 283- Add skipSelf flag (default true) 284- For rtc.peer-joined, set skipSelf=false to include sender 285 286**** Handler Updates (handler-realm.js) 287 288- On client join: Broadcast rtc.peer-joined to ALL members (including self) 289- On client leave: Broadcast rtc.peer-left to remaining members 290- Existing broadcast mechanism handles WebRTC signaling perfectly 291 292*** WebRTC Utilities (src/common/webrtc.js) 293 294Core utilities for WebRTC: 295- RTC_CONFIG with STUN servers 296- DATA_CHANNEL_CONFIG for reliable messaging 297- PerfectNegotiation class for glare-free negotiation 298- ConnectionHealthMonitor for ping/pong health checks 299 300*** Client WebRTC Manager (src/client/webrtc-manager.js) 301 302Main orchestrator that: 303- Manages all peer connections 304- Handles incoming RTC messages 305- Routes signaling between peers 306- Emits events for UI updates 307- Provides public API for sending messages 308 309*** Client Peer Connection (src/client/peer-connection.js) 310 311Individual peer connection handler: 312- RTCPeerConnection lifecycle management 313- Perfect negotiation implementation 314- Data channel setup and messaging 315- Health monitoring with ping/pong 316- Automatic reconnection with exponential backoff 317- Connection state tracking 318 319*** UI Components 320 321**** PeerList Component 322- Shows all realm members 323- Connection status indicators 324- Real-time state updates 325 326**** MessageInterface Component 327- Send messages via WebRTC or server broadcast 328- Display incoming messages 329- Mode selection (P2P vs server relay) 330 331** Connection Flow 332 333*** Initial Join 3341. Client authenticates via WebSocket 3352. Server sends realm.status 3363. Server broadcasts rtc.peer-joined to ALL members 3374. Client sees own join message with member list 3385. Client initializes WebRTCManager 3396. Client connects to all existing members 340 341*** Peer-to-Peer Connection 3421. Initiator creates RTCPeerConnection (polite=true) 3432. Creates data channel, triggering negotiation 3443. Sends offer via realm.broadcast to target peer 3454. Target creates RTCPeerConnection (polite=false) 3465. Exchanges answer and ICE candidates 3476. Data channel opens, health monitoring starts 348 349*** Reconnection 3501. Health monitor detects issues or connection drops 3512. Exponential backoff timer starts 3523. New connection attempt with fresh connectionId 3534. ICE restart or full renegotiation 354 355** Key Design Decisions 356 357*** Perfect Negotiation Pattern 358Prevents glare when both peers try to connect simultaneously by using 359polite/impolite roles. 360 361*** Health Monitoring 362Proactive ping/pong messages detect connection issues before browser APIs, 363enabling faster recovery. 364 365*** Connection ID Tracking 366Each connection attempt has unique ID to ensure offer/answer pairs match 367during concurrent connections. 368 369*** Leveraging Existing Infrastructure 370WebRTC signaling is just another payload type in the existing broadcast 371system - no new server complexity. 372 373** Implementation Order 374 375*** Phase 1: Core Infrastructure 3761. Create protocol message schemas 3772. Update broadcastToRealm function 3783. Add peer join/leave broadcasts 3794. Create WebRTC utilities module 380 381*** Phase 2: Client Connection Management 3821. Implement WebRTCManager 3832. Create PeerConnection class 3843. Add perfect negotiation 3854. Implement health monitoring 386 387*** Phase 3: UI Integration 3881. Update main app to initialize WebRTC 3892. Create PeerList component 3903. Add MessageInterface with dual modes 3914. Style connection indicators 392 393*** Phase 4: Robustness 3941. Add reconnection logic 3952. Implement ICE restart 3963. Handle edge cases 3974. Add comprehensive error handling 398 399** Testing Strategy 400 401*** Unit Tests 402- Perfect negotiation scenarios 403- Health monitoring logic 404- Message routing 405 406*** Integration Tests 407- Full connection flow with mocks 408- Signaling message flow 409- State management 410 411*** E2E Tests 412- Real browser testing 413- Network condition simulation 414- Multi-peer scenarios 415 416*** Load Tests 417- Mesh scalability limits 418- Message throughput 419- Connection stability