1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! # bevy_symbios_multiuser
//!
//! A decentralized, low-latency multiplayer plugin for the Bevy engine.
//! Combines [ATProto](https://atproto.com/) for federated identity with
//! WebRTC (via [Matchbox](https://github.com/johanhelsing/matchbox)) for
//! peer-to-peer data transfer.
//!
//! ## Architecture
//!
//! The crate provides a generic message bus via [`plugin::SymbiosMultiuserPlugin`]
//! that accepts any serializable domain type `T`. Messages are transported over
//! WebRTC data channels: one **reliable** channel for state mutations and one
//! **unreliable** channel for ephemeral presence data.
//!
//! Authentication flows through a **Sovereign Broker** pattern:
//!
//! 1. The host application drives an OAuth 2.0 + DPoP authorization-code
//! exchange (via [`proto_blue_oauth`]), yielding an
//! [`proto_blue_oauth::OAuthSession`]. The host wraps that session in an
//! [`auth::AtprotoSession`] alongside the resolved DID, handle, and PDS
//! base URL. This crate does **not** ship login UI or credentials helpers;
//! the legacy App-Password flow (`create_session` / `AtprotoCredentials`)
//! was removed in 0.3.
//!
//! For relay authentication with `auth_required = true`, the client then
//! calls [`auth::get_service_auth`] to obtain a *service auth token* — a
//! JWT signed by the user's `#atproto` key that third-party relays can
//! verify via DID document resolution. The OAuth access token is
//! DPoP-bound to a private key held only by the client and cannot be
//! handed off to the relay. Wrap the service token in a
//! [`signaller::TokenSourceRes`] resource; the plugin reads the current
//! token from this resource on every (re)connect attempt. Inserting an
//! [`auth::AtprotoSession`] resource is optional and is only needed if
//! the application wants the user's identity available to its own
//! systems — the plugin itself never reads it.
//! 2. The [`signaller::SymbiosSignallerBuilder`] passes this token to the
//! relay during the WebSocket handshake. On native targets, the token is
//! sent as an `Authorization: Bearer` header. On WASM targets, the token
//! is sent via the `Sec-WebSocket-Protocol` subprotocol trick (the
//! browser `WebSocket` API does not support custom headers).
//! 3. The relay (`relay` module, feature-gated) validates the JWT claims and — when
//! `auth_required` is enabled — resolves the issuer's DID document to
//! cryptographically verify the signature (ES256/P-256 or ES256K/secp256k1)
//! against the `#atproto` signing key. When [`RelayConfig::service_did`] is
//! set, the `aud` claim is also validated to prevent cross-service token
//! replay. The authenticated DID becomes the peer's session identity.
//! 4. Once signaling completes, data flows directly peer-to-peer over
//! WebRTC data channels.
//!
//! ## Quick Start
//!
//! ```rust,no_run
//! use bevy::prelude::*;
//! use bevy_symbios_multiuser::prelude::*;
//! use serde::{Serialize, Deserialize};
//!
//! #[derive(Serialize, Deserialize, Debug, Clone)]
//! enum GameMessage {
//! Move { x: f32, y: f32 },
//! Chat(String),
//! }
//!
//! fn main() {
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(SymbiosMultiuserPlugin::<GameMessage>::new(
//! "wss://relay.example.com/my_room",
//! ))
//! .add_systems(Update, (handle_incoming, send_movement))
//! .run();
//! }
//!
//! fn handle_incoming(mut queue: ResMut<NetworkQueue<GameMessage>>) {
//! for msg in queue.drain() {
//! info!("From {:?}: {:?}", msg.sender, msg.payload);
//! }
//! }
//!
//! fn send_movement(mut writer: MessageWriter<Broadcast<GameMessage>>) {
//! writer.write(Broadcast {
//! payload: GameMessage::Move { x: 1.0, y: 2.0 },
//! channel: ChannelKind::Unreliable,
//! });
//! }
//! ```
//!
//! ## Features
//!
//! - `client` (default) — ATProto authentication, custom signaller for
//! authenticated relay connections.
//! - `tls` (default) — Enables TLS (via `rustls`) for both `reqwest` HTTPS
//! and `async-tungstenite` WebSocket (`wss://`) connections.
//! - `relay` — Sovereign Broker relay server with DID-based JWT signature
//! verification (ES256 + ES256K), room-based peer isolation, atomic
//! connection limits, SSRF-hardened DID resolution, message size caps,
//! idle/handshake/write timeouts, HTTP-level Slowloris protection, server-side
//! pings (WASM keep-alive), per-sender token-bucket rate limiting,
//! per-target burst limiting, per-domain and global `did:web` fetch
//! concurrency limiting, request coalescing, negative DID caching, peer ID
//! length validation, unique target cap, and JWT audience validation
//! (`service_did`). Built on `axum`.
// The relay server depends on `axum`, `tokio::net::TcpListener`, and
// `reqwest`'s native DNS/TLS stack — none of which build on wasm. Without
// this target gate, `cargo build --target wasm32-unknown-unknown --all-features`
// (a common pattern in full-stack workspaces that unify client and server
// crates) would fail deep inside the relay's transitive dependencies.
// Everyone actually running a relay uses native targets anyway, so gating
// this off on wasm is free.
/// Re-exports for convenient use.