···1515JWT_SECRET=your-super-secret-jwt-key-please-change-me
1616PDS_HOSTNAME=localhost:3000 # The public-facing hostname of the PDS
1717PLC_URL=plc.directory
1818+APPVIEW_URL=https://api.bsky.app
···2626tracing = "0.1.43"
2727tracing-subscriber = "0.3.22"
2828uuid = { version = "1.19.0", features = ["v4", "fast-rng"] }
2929+3030+[dev-dependencies]
3131+testcontainers = "0.26.0"
3232+testcontainers-modules = { version = "0.14.0", features = ["postgres"] }
+48-75
TODO.md
···11-# Implementation TODOs
11+# PDS Implementation TODOs
2233-Lewis' special big boy todofile
33+Lewis' corrected big boy todofile
4455-## 1. Server Infrastructure & Health
55+## 1. Server Infrastructure & Proxying
66- [x] Health Check
77 - [x] Implement `GET /health` endpoint (returns "OK").
88- [x] Server Description
99 - [x] Implement `com.atproto.server.describeServer` (returns available user domains).
1010+- [x] XRPC Proxying
1111+ - [x] Implement strict forwarding for all `app.bsky.*` and `chat.bsky.*` requests to an appview.
1212+ - [x] Forward Auth headers correctly.
1313+ - [x] Handle AppView errors/timeouts gracefully.
10141115## 2. Authentication & Account Management (`com.atproto.server`)
1216- [x] Account Creation
1317 - [x] Implement `com.atproto.server.createAccount`.
1418 - [x] Validate handle format (reject invalid characters).
1515- - [x] Create DID for new user.
1616- - [x] Initialize user repository.
1919+ - [x] Create DID for new user (PLC directory).
2020+ - [x] Initialize user repository (Root commit).
1721 - [x] Return access JWT and DID.
1818- - [x] MST stuff I think...
1919-2222+ - [ ] Create DID for new user (did:web).
2023- [x] Session Management
2124 - [x] Implement `com.atproto.server.createSession` (Login).
2222- - [x] Validate identifier (handle/email) and password.
2323- - [x] Return access JWT, refresh JWT, and DID.
2425 - [x] Implement `com.atproto.server.getSession`.
2525- - [x] Verify JWT validity.
2626 - [x] Implement `com.atproto.server.refreshSession`.
2727 - [x] Implement `com.atproto.server.deleteSession` (Logout).
2828- - [x] Invalidate current session/token.
29283029## 3. Repository Operations (`com.atproto.repo`)
3130- [ ] Record CRUD
3231 - [ ] Implement `com.atproto.repo.createRecord`.
3333- - [ ] Generate `rkey` if not provided.
3434- - [ ] Validate schema against Lexicon.
3535- - [ ] Handle `swapCommit` for optimistic locking.
3232+ - [ ] Validate schema against Lexicon (just structure, not complex logic).
3333+ - [ ] Generate `rkey` (TID) if not provided.
3434+ - [ ] Handle MST (Merkle Search Tree) insertion.
3535+ - [ ] **Trigger Firehose Event**.
3636 - [ ] Implement `com.atproto.repo.putRecord`.
3737- - [ ] Handle create vs update logic.
3838- - [ ] Validate `repo` matches authenticated user.
3939- - [ ] Validate record schema (e.g., missing required fields).
4037 - [ ] Implement `com.atproto.repo.getRecord`.
4141- - [ ] Handle missing params (400 Bad Request).
4242- - [ ] Handle non-existent record (404 Not Found).
4338 - [ ] Implement `com.atproto.repo.deleteRecord`.
4439 - [ ] Implement `com.atproto.repo.listRecords`.
4545- - [ ] Support pagination (`limit`, `cursor`).
4040+ - [ ] Implement `com.atproto.repo.describeRepo`.
4641- [ ] Blob Management
4742 - [ ] Implement `com.atproto.repo.uploadBlob`.
4848- - [ ] Enforce authentication.
4949- - [ ] Validate MIME types (reject unsupported).
5050- - [ ] Return blob reference (`$link`).
5151-- [ ] Repo Meta
5252- - [ ] Implement `com.atproto.repo.describeRepo`.
4343+ - [ ] Store blob (S3).
4444+ - [ ] return `blob` ref (CID + MimeType).
53455454-## 4. Actor & Profile (`app.bsky.actor`)
5555-- [ ] Profile Management
5656- - [ ] Implement `app.bsky.actor.getProfile`.
5757- - [ ] Resolve handle to DID.
5858- - [ ] Return profile record data.
5959-- [ ] Discovery
6060- - [ ] Implement `app.bsky.actor.searchActors`.
4646+## 4. Sync & Federation (`com.atproto.sync`)
4747+- [ ] The Firehose (WebSocket)
4848+ - [ ] Implement `com.atproto.sync.subscribeRepos`.
4949+ - [ ] Broadcast real-time commit events.
5050+ - [ ] Handle cursor replay (backfill).
5151+- [ ] Bulk Export
5252+ - [ ] Implement `com.atproto.sync.getRepo` (Return full CAR file of repo).
5353+ - [ ] Implement `com.atproto.sync.getBlocks` (Return specific blocks via CIDs).
5454+ - [ ] Implement `com.atproto.sync.getLatestCommit`.
5555+ - [ ] Implement `com.atproto.sync.getRecord` (Sync version, distinct from repo.getRecord).
5656+- [ ] Blob Sync
5757+ - [ ] Implement `com.atproto.sync.getBlob`.
5858+ - [ ] Implement `com.atproto.sync.listBlobs`.
5959+- [ ] Crawler Interaction
6060+ - [ ] Implement `com.atproto.sync.requestCrawl` (Notify relays to index us).
61616262-## 5. Feed & Timeline (`app.bsky.feed`)
6363-- [ ] Feed Retrieval
6464- - [ ] Implement `app.bsky.feed.getTimeline`.
6565- - [ ] Implement `app.bsky.feed.getAuthorFeed`.
6666- - [ ] Filter by actor.
6767- - [ ] Respect mutes (if viewer is authenticated).
6868- - [ ] Implement `app.bsky.feed.getPostThread`.
6969- - [ ] Construct thread tree (parents, replies).
7070- - [ ] Handle deleted posts (return `notFoundPost` view).
7171-- [ ] Record Types
7272- - [ ] Support `app.bsky.feed.post` record type.
7373- - [ ] Support `app.bsky.feed.like` record type.
7474- - [ ] Support `app.bsky.embed.images` in posts.
7575-7676-## 6. Social Graph (`app.bsky.graph`)
7777-- [ ] Relationships
7878- - [ ] Implement `app.bsky.graph.getFollows`.
7979- - [ ] Implement `app.bsky.graph.getFollowers`.
8080- - [ ] Implement `app.bsky.graph.getMutes`.
8181- - [ ] Implement `app.bsky.graph.getBlocks`.
8282-- [ ] Record Types
8383- - [ ] Support `app.bsky.graph.follow` record type.
8484- - [ ] Support `app.bsky.graph.mute` record type.
8585-8686-## 7. Notifications (`app.bsky.notification`)
8787-- [ ] Notification Management
8888- - [ ] Implement `app.bsky.notification.listNotifications`.
8989- - [ ] Aggregate notifications (likes, follows, replies).
9090- - [ ] Implement `app.bsky.notification.getUnreadCount`.
9191- - [ ] Track read state.
9292- - [ ] Reset count on list/read.
9393-9494-## 8. Identity (`com.atproto.identity`)
6262+## 5. Identity (`com.atproto.identity`)
9563- [ ] Resolution
9696- - [ ] Implement `com.atproto.identity.resolveHandle`.
6464+ - [ ] Implement `com.atproto.identity.resolveHandle` (Can be internal or proxy to PLC).
6565+ - [ ] Implement `/.well-known/did.json` (Depends on supporting did:web).
97669898-## 9. Sync & Federation (`com.atproto.sync`)
9999-- [ ] Data Export
100100- - [ ] Implement `com.atproto.sync.getRepo` (Export CAR file).
101101- - [ ] Implement `com.atproto.sync.getBlocks`.
6767+## 6. Record Schema Validation
6868+- [ ] `app.bsky.feed.post`
6969+- [ ] `app.bsky.feed.like`
7070+- [ ] `app.bsky.feed.repost`
7171+- [ ] `app.bsky.graph.follow`
7272+- [ ] `app.bsky.graph.block`
7373+- [ ] `app.bsky.actor.profile`
7474+- [ ] Other app(view) validation too!!!
10275103103-## 10. General Requirements
7676+## 7. General Requirements
7777+- [ ] IPLD & MST
7878+ - [ ] Implement Merkle Search Tree (MST) logic for repo signing.
7979+ - [ ] Implement CAR (Content Addressable Archives) encoding/decoding.
10480- [ ] Validation
105105- - [ ] Ensure all endpoints validate input parameters.
106106- - [ ] Ensure proper error codes (400, 401, 404, 409).
107107-- [ ] Concurrency
108108- - [ ] Ensure thread safety for repo updates.
8181+ - [ ] DID PLC Operations (Sign rotation keys).
+23
justfile
···11+# Run all tests with correct threading models
22+test: test-proxy test-lifecycle test-others
33+44+# Proxy tests modify environment variables, so must run single-threaded
55+# TODO: figure out how to run in parallel
66+test-proxy:
77+ cargo test --test proxy -- --test-threads=1
88+99+# Lifecycle tests involve complex state mutations, run single-threaded to be safe
1010+# TODO: figure out how to run in parallel
1111+test-lifecycle:
1212+ cargo test --test lifecycle -- --test-threads=1
1313+1414+test-others:
1515+ cargo test --lib
1616+ cargo test --test actor
1717+ cargo test --test feed
1818+ cargo test --test graph
1919+ cargo test --test identity
2020+ cargo test --test notification
2121+ cargo test --test repo
2222+ cargo test --test server
2323+ cargo test --test sync
+1
src/api/mod.rs
···11pub mod server;
22pub mod repo;
33+pub mod proxy;
···88 let params = [
99 ("actor", AUTH_DID),
1010 ];
1111- let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL))
1111+ let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await))
1212 .query(¶ms)
1313 .bearer_auth(AUTH_TOKEN)
1414 .send()
···2525 ("q", "test"),
2626 ("limit", "10"),
2727 ];
2828- let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", BASE_URL))
2828+ let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", base_url().await))
2929 .query(¶ms)
3030 .bearer_auth(AUTH_TOKEN)
3131 .send()
+70-4
tests/common/mod.rs
···55use std::collections::HashMap;
66#[allow(unused_imports)]
77use std::time::Duration;
88+use std::sync::OnceLock;
99+use bspds::state::AppState;
1010+use sqlx::postgres::PgPoolOptions;
1111+use tokio::net::TcpListener;
1212+use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt};
1313+use testcontainers_modules::postgres::Postgres;
81499-pub const BASE_URL: &str = "http://127.0.0.1:3000";
1515+static SERVER_URL: OnceLock<String> = OnceLock::new();
1616+static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
1717+1018#[allow(dead_code)]
1119pub const AUTH_TOKEN: &str = "test-token";
1220#[allow(dead_code)]
···2028 Client::new()
2129}
22303131+pub async fn base_url() -> &'static str {
3232+ SERVER_URL.get_or_init(|| {
3333+ let (tx, rx) = std::sync::mpsc::channel();
3434+3535+ std::thread::spawn(move || {
3636+ if std::env::var("DOCKER_HOST").is_err() {
3737+ if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
3838+ let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
3939+ if podman_sock.exists() {
4040+ unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); }
4141+ }
4242+ }
4343+ }
4444+4545+ let rt = tokio::runtime::Runtime::new().unwrap();
4646+ rt.block_on(async move {
4747+ let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres");
4848+ let connection_string = format!(
4949+ "postgres://postgres:postgres@127.0.0.1:{}/postgres",
5050+ container.get_host_port_ipv4(5432).await.expect("Failed to get port")
5151+ );
5252+5353+ DB_CONTAINER.set(container).ok();
5454+5555+ let url = spawn_app(connection_string).await;
5656+ tx.send(url).unwrap();
5757+ std::future::pending::<()>().await;
5858+ });
5959+ });
6060+6161+ rx.recv().expect("Failed to start test server")
6262+ })
6363+}
6464+6565+async fn spawn_app(database_url: String) -> String {
6666+ let pool = PgPoolOptions::new()
6767+ .connect(&database_url)
6868+ .await
6969+ .expect("Failed to connect to Postgres. Make sure the database is running.");
7070+7171+ sqlx::migrate!("./migrations")
7272+ .run(&pool)
7373+ .await
7474+ .expect("Failed to run migrations");
7575+7676+ let state = AppState::new(pool);
7777+ let app = bspds::app(state);
7878+7979+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8080+ let addr = listener.local_addr().unwrap();
8181+8282+ tokio::spawn(async move {
8383+ axum::serve(listener, app).await.unwrap();
8484+ });
8585+8686+ format!("http://{}", addr)
8787+}
8888+2389#[allow(dead_code)]
2490pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value {
2525- let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
9191+ let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
2692 .header(header::CONTENT_TYPE, mime)
2793 .bearer_auth(AUTH_TOKEN)
2894 .body(data)
···59125 "record": record
60126 });
611276262- let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
128128+ let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
63129 .bearer_auth(AUTH_TOKEN)
64130 .json(&payload)
65131 .send()
···84150 "password": "password"
85151 });
861528787- let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
153153+ let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
88154 .json(&payload)
89155 .send()
90156 .await
+3-3
tests/feed.rs
···88async fn test_get_timeline() {
99 let client = client();
1010 let params = [("limit", "30")];
1111- let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", BASE_URL))
1111+ let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", base_url().await))
1212 .query(¶ms)
1313 .bearer_auth(AUTH_TOKEN)
1414 .send()
···2525 ("actor", AUTH_DID),
2626 ("limit", "30")
2727 ];
2828- let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
2828+ let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await))
2929 .query(¶ms)
3030 .bearer_auth(AUTH_TOKEN)
3131 .send()
···4242 params.insert("uri", "at://did:plc:other/app.bsky.feed.post/3k12345");
4343 params.insert("depth", "5");
44444545- let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
4545+ let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await))
4646 .query(¶ms)
4747 .bearer_auth(AUTH_TOKEN)
4848 .send()
+4-4
tests/graph.rs
···88 let params = [
99 ("actor", AUTH_DID),
1010 ];
1111- let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL))
1111+ let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await))
1212 .query(¶ms)
1313 .bearer_auth(AUTH_TOKEN)
1414 .send()
···2424 let params = [
2525 ("actor", AUTH_DID),
2626 ];
2727- let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", BASE_URL))
2727+ let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", base_url().await))
2828 .query(¶ms)
2929 .bearer_auth(AUTH_TOKEN)
3030 .send()
···4040 let params = [
4141 ("limit", "25"),
4242 ];
4343- let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", BASE_URL))
4343+ let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", base_url().await))
4444 .query(¶ms)
4545 .bearer_auth(AUTH_TOKEN)
4646 .send()
···5757 let params = [
5858 ("limit", "25"),
5959 ];
6060- let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", BASE_URL))
6060+ let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", base_url().await))
6161 .query(¶ms)
6262 .bearer_auth(AUTH_TOKEN)
6363 .send()
+1-1
tests/identity.rs
···88 let params = [
99 ("handle", "bsky.app"),
1010 ];
1111- let res = client.get(format!("{}/xrpc/com.atproto.identity.resolveHandle", BASE_URL))
1111+ let res = client.get(format!("{}/xrpc/com.atproto.identity.resolveHandle", base_url().await))
1212 .query(¶ms)
1313 .send()
1414 .await
+42-42
tests/lifecycle.rs
···3030 }
3131 });
32323333- let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
3333+ let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
3434 .bearer_auth(AUTH_TOKEN)
3535 .json(&create_payload)
3636 .send()
···4747 ("collection", collection),
4848 ("rkey", &rkey),
4949 ];
5050- let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
5050+ let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
5151 .query(¶ms)
5252 .send()
5353 .await
···7171 }
7272 });
73737474- let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
7474+ let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
7575 .bearer_auth(AUTH_TOKEN)
7676 .json(&update_payload)
7777 .send()
···8181 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record");
828283838484- let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
8484+ let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
8585 .query(¶ms)
8686 .send()
8787 .await
···9898 "rkey": rkey
9999 });
100100101101- let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
101101+ let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
102102 .bearer_auth(AUTH_TOKEN)
103103 .json(&delete_payload)
104104 .send()
···108108 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record");
109109110110111111- let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
111111+ let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
112112 .query(¶ms)
113113 .send()
114114 .await
···157157 }
158158 });
159159160160- let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
160160+ let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
161161 .bearer_auth(AUTH_TOKEN)
162162 .json(&create_payload)
163163 .send()
···172172 ("collection", collection),
173173 ("rkey", &rkey),
174174 ];
175175- let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
175175+ let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
176176 .query(¶ms)
177177 .send()
178178 .await
···202202 }
203203 });
204204205205- let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
205205+ let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
206206 .bearer_auth(AUTH_TOKEN)
207207 .json(&create_payload)
208208 .send()
···219219 let params_get_follows = [
220220 ("actor", AUTH_DID),
221221 ];
222222- let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL))
222222+ let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await))
223223 .query(¶ms_get_follows)
224224 .bearer_auth(AUTH_TOKEN)
225225 .send()
···243243 "rkey": rkey
244244 });
245245246246- let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
246246+ let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
247247 .bearer_auth(AUTH_TOKEN)
248248 .json(&delete_payload)
249249 .send()
···253253 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record");
254254255255256256- let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL))
256256+ let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await))
257257 .query(¶ms_get_follows)
258258 .bearer_auth(AUTH_TOKEN)
259259 .send()
···290290 }
291291 });
292292293293- let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
293293+ let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
294294 .bearer_auth(AUTH_TOKEN)
295295 .json(&payload)
296296 .send()
···308308 ("limit", "2"),
309309 ];
310310311311- let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL))
311311+ let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
312312 .query(¶ms_page1)
313313 .send()
314314 .await
···330330 ("cursor", cursor),
331331 ];
332332333333- let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL))
333333+ let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
334334 .query(¶ms_page2)
335335 .send()
336336 .await
···351351 "collection": collection,
352352 "rkey": rkey
353353 });
354354- client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
354354+ client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
355355 .bearer_auth(AUTH_TOKEN)
356356 .json(&delete_payload)
357357 .send()
···386386 let params = [
387387 ("uri", &root_uri),
388388 ];
389389- let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
389389+ let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await))
390390 .query(¶ms)
391391 .bearer_auth(AUTH_TOKEN)
392392 .send()
···410410411411412412 let collection = "app.bsky.feed.post";
413413- client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
413413+ client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
414414 .bearer_auth(AUTH_TOKEN)
415415 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey }))
416416 .send().await.expect("Failed to delete reply");
417417418418- client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
418418+ client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
419419 .bearer_auth(AUTH_TOKEN)
420420 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey }))
421421 .send().await.expect("Failed to delete root post");
···436436 "password": password
437437 });
438438439439- let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
439439+ let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
440440 .json(&create_account_payload)
441441 .send()
442442 .await
···455455 "password": password
456456 });
457457458458- let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
458458+ let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
459459 .json(&session_payload)
460460 .send()
461461 .await
···479479 }
480480 });
481481482482- let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
482482+ let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
483483 .bearer_auth(&session_jwt)
484484 .json(&profile_payload)
485485 .send()
···492492 let params_get_profile = [
493493 ("actor", &handle),
494494 ];
495495- let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL))
495495+ let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await))
496496 .query(¶ms_get_profile)
497497 .send()
498498 .await
···506506 assert_eq!(profile_body["displayName"], "E2E Test User");
507507508508509509- let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL))
509509+ let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await))
510510 .bearer_auth(&session_jwt)
511511 .send()
512512 .await
···515515 assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session");
516516517517518518- let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL))
518518+ let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await))
519519 .bearer_auth(&session_jwt)
520520 .send()
521521 .await
···536536 "email": email,
537537 "password": password
538538 });
539539- let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
539539+ let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
540540 .json(&create_account_payload)
541541 .send()
542542 .await
···557557 "description": "A user created by the e2e test suite."
558558 }
559559 });
560560- let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
560560+ let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
561561 .bearer_auth(&new_jwt)
562562 .json(&profile_payload)
563563 .send()
···581581 "record": record
582582 });
583583584584- let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
584584+ let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
585585 .bearer_auth(jwt)
586586 .json(&payload)
587587 .send()
···609609 "rkey": rkey
610610 });
611611612612- let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
612612+ let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
613613 .bearer_auth(jwt)
614614 .json(&payload)
615615 .send()
···640640 ).await;
641641 let post_ref = json!({ "uri": post_uri, "cid": post_cid });
642642643643- let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
643643+ let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await))
644644 .bearer_auth(&user_a_jwt)
645645 .send().await.expect("getUnreadCount 1 failed");
646646 let count_body_1: Value = count_res_1.json().await.expect("count 1 not json");
···677677678678 tokio::time::sleep(Duration::from_millis(500)).await;
679679680680- let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
680680+ let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await))
681681 .bearer_auth(&user_a_jwt)
682682 .send().await.expect("getUnreadCount 2 failed");
683683 let count_body_2: Value = count_res_2.json().await.expect("count 2 not json");
684684 assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions");
685685686686- let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL))
686686+ let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await))
687687 .bearer_auth(&user_a_jwt)
688688 .send().await.expect("listNotifications failed");
689689 let list_body: Value = list_res.json().await.expect("list not json");
···699699 assert!(has_like, "Notification list missing 'like'");
700700 assert!(has_reply, "Notification list missing 'reply'");
701701702702- let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
702702+ let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await))
703703 .bearer_auth(&user_a_jwt)
704704 .send().await.expect("getUnreadCount 3 failed");
705705 let count_body_3: Value = count_res_3.json().await.expect("count 3 not json");
···727727 ).await;
728728729729 let feed_params_1 = [("actor", &user_b_did)];
730730- let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
730730+ let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await))
731731 .query(&feed_params_1)
732732 .bearer_auth(&user_a_jwt)
733733 .send().await.expect("getAuthorFeed 1 failed");
···749749 let mute_rkey = mute_uri.split('/').last().unwrap();
750750751751 let feed_params_2 = [("actor", &user_b_did)];
752752- let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
752752+ let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await))
753753 .query(&feed_params_2)
754754 .bearer_auth(&user_a_jwt)
755755 .send().await.expect("getAuthorFeed 2 failed");
···765765 ).await;
766766767767 let feed_params_3 = [("actor", &user_b_did)];
768768- let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
768768+ let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await))
769769 .query(&feed_params_3)
770770 .bearer_auth(&user_a_jwt)
771771 .send().await.expect("getAuthorFeed 3 failed");
···783783784784 let (user_did, user_jwt) = setup_new_user("user-conflict").await;
785785786786- let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
786786+ let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
787787 .query(&[
788788 ("repo", &user_did),
789789 ("collection", &"app.bsky.actor.profile".to_string()),
···803803 },
804804 "swapCommit": cid_v1 // <-- Correctly point to v1
805805 });
806806- let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
806806+ let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
807807 .bearer_auth(&user_jwt)
808808 .json(&update_payload_v2)
809809 .send().await.expect("putRecord v2 failed");
···821821 },
822822 "swapCommit": cid_v1
823823 });
824824- let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
824824+ let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
825825 .bearer_auth(&user_jwt)
826826 .json(&update_payload_v3_stale)
827827 .send().await.expect("putRecord v3 (stale) failed");
···842842 },
843843 "swapCommit": cid_v2 // <-- Correct
844844 });
845845- let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
845845+ let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
846846 .bearer_auth(&user_jwt)
847847 .json(&update_payload_v3_good)
848848 .send().await.expect("putRecord v3 (good) failed");
···894894 }),
895895 ).await;
896896897897- let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
897897+ let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await))
898898 .query(&[("uri", &p1_uri)])
899899 .bearer_auth(&user_a_jwt)
900900 .send().await.expect("getThread 1 failed");
···914914 &p2_rkey,
915915 ).await;
916916917917- let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
917917+ let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await))
918918 .query(&[("uri", &p1_uri)])
919919 .bearer_auth(&user_a_jwt)
920920 .send().await.expect("getThread 2 failed");
+2-2
tests/notification.rs
···88 let params = [
99 ("limit", "30"),
1010 ];
1111- let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL))
1111+ let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await))
1212 .query(¶ms)
1313 .bearer_auth(AUTH_TOKEN)
1414 .send()
···2121#[tokio::test]
2222async fn test_get_unread_count() {
2323 let client = client();
2424- let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
2424+ let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await))
2525 .bearer_auth(AUTH_TOKEN)
2626 .send()
2727 .await
+96
tests/proxy.rs
···11+mod common;
22+33+use axum::{
44+ routing::any,
55+ Router,
66+ extract::Request,
77+ http::StatusCode,
88+};
99+use tokio::net::TcpListener;
1010+use reqwest::Client;
1111+use std::sync::Arc;
1212+1313+async fn spawn_mock_upstream() -> (String, tokio::sync::mpsc::Receiver<(String, String, Option<String>)>) {
1414+ let (tx, rx) = tokio::sync::mpsc::channel(10);
1515+ let tx = Arc::new(tx);
1616+1717+ let app = Router::new().fallback(any(move |req: Request| {
1818+ let tx = tx.clone();
1919+ async move {
2020+ let method = req.method().to_string();
2121+ let uri = req.uri().to_string();
2222+ let auth = req.headers().get("Authorization")
2323+ .and_then(|h| h.to_str().ok())
2424+ .map(|s| s.to_string());
2525+2626+ let _ = tx.send((method, uri, auth)).await;
2727+ (StatusCode::OK, "Mock Response")
2828+ }
2929+ }));
3030+3131+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
3232+ let addr = listener.local_addr().unwrap();
3333+3434+ tokio::spawn(async move {
3535+ axum::serve(listener, app).await.unwrap();
3636+ });
3737+3838+ (format!("http://{}", addr), rx)
3939+}
4040+4141+#[tokio::test]
4242+async fn test_proxy_via_header() {
4343+ let app_url = common::base_url().await;
4444+ let (upstream_url, mut rx) = spawn_mock_upstream().await;
4545+ let client = Client::new();
4646+4747+ let res = client.get(format!("{}/xrpc/com.example.test", app_url))
4848+ .header("atproto-proxy", &upstream_url)
4949+ .header("Authorization", "Bearer test-token")
5050+ .send()
5151+ .await
5252+ .unwrap();
5353+5454+ assert_eq!(res.status(), StatusCode::OK);
5555+5656+ let (method, uri, auth) = rx.recv().await.expect("Upstream should receive request");
5757+ assert_eq!(method, "GET");
5858+ assert_eq!(uri, "/xrpc/com.example.test");
5959+ assert_eq!(auth, Some("Bearer test-token".to_string()));
6060+}
6161+6262+#[tokio::test]
6363+async fn test_proxy_via_env_var() {
6464+ let (upstream_url, mut rx) = spawn_mock_upstream().await;
6565+6666+ unsafe { std::env::set_var("APPVIEW_URL", &upstream_url); }
6767+6868+ let app_url = common::base_url().await;
6969+ let client = Client::new();
7070+7171+ let res = client.get(format!("{}/xrpc/com.example.envtest", app_url))
7272+ .send()
7373+ .await
7474+ .unwrap();
7575+7676+ assert_eq!(res.status(), StatusCode::OK);
7777+7878+ let (method, uri, _) = rx.recv().await.expect("Upstream should receive request");
7979+ assert_eq!(method, "GET");
8080+ assert_eq!(uri, "/xrpc/com.example.envtest");
8181+}
8282+8383+#[tokio::test]
8484+async fn test_proxy_missing_config() {
8585+ unsafe { std::env::remove_var("APPVIEW_URL"); }
8686+8787+ let app_url = common::base_url().await;
8888+ let client = Client::new();
8989+9090+ let res = client.get(format!("{}/xrpc/com.example.fail", app_url))
9191+ .send()
9292+ .await
9393+ .unwrap();
9494+9595+ assert_eq!(res.status(), StatusCode::BAD_GATEWAY);
9696+}
+16-16
tests/repo.rs
···1515 ("rkey", "self"),
1616 ];
17171818- let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
1818+ let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
1919 .query(¶ms)
2020 .send()
2121 .await
···3636 ("rkey", "nonexistent"),
3737 ];
38383939- let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
3939+ let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
4040 .query(¶ms)
4141 .send()
4242 .await
···5151#[ignore]
5252async fn test_upload_blob_no_auth() {
5353 let client = client();
5454- let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
5454+ let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
5555 .header(header::CONTENT_TYPE, "text/plain")
5656 .body("no auth")
5757 .send()
···6868async fn test_upload_blob_success() {
6969 let client = client();
7070 let (token, _) = create_account_and_login(&client).await;
7171- let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
7171+ let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
7272 .header(header::CONTENT_TYPE, "text/plain")
7373 .bearer_auth(token)
7474 .body("This is our blob data")
···9292 "record": {}
9393 });
94949595- let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
9595+ let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
9696 .json(&payload)
9797 .send()
9898 .await
···120120 }
121121 });
122122123123- let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
123123+ let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
124124 .bearer_auth(token)
125125 .json(&payload)
126126 .send()
···142142 ("repo", "did:plc:12345"),
143143 ];
144144145145- let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
145145+ let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
146146 .query(¶ms)
147147 .send()
148148 .await
···156156#[ignore]
157157async fn test_upload_blob_bad_token() {
158158 let client = client();
159159- let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
159159+ let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
160160 .header(header::CONTENT_TYPE, "text/plain")
161161 .bearer_auth(BAD_AUTH_TOKEN)
162162 .body("This is our blob data")
···187187 }
188188 });
189189190190- let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
190190+ let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
191191 .bearer_auth(token)
192192 .json(&payload)
193193 .send()
···215215 }
216216 });
217217218218- let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
218218+ let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
219219 .bearer_auth(token)
220220 .json(&payload)
221221 .send()
···231231async fn test_upload_blob_unsupported_mime_type() {
232232 let client = client();
233233 let (token, _) = create_account_and_login(&client).await;
234234- let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
234234+ let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
235235 .header(header::CONTENT_TYPE, "application/xml")
236236 .bearer_auth(token)
237237 .body("<xml>not an image</xml>")
···252252 ("collection", "app.bsky.feed.post"),
253253 ("limit", "10"),
254254 ];
255255- let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL))
255255+ let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
256256 .query(¶ms)
257257 .send()
258258 .await
···270270 "collection": "app.bsky.feed.post",
271271 "rkey": "some_post_to_delete"
272272 });
273273- let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
273273+ let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
274274 .bearer_auth(token)
275275 .json(&payload)
276276 .send()
···287287 let params = [
288288 ("repo", did.as_str()),
289289 ];
290290- let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", BASE_URL))
290290+ let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", base_url().await))
291291 .query(¶ms)
292292 .send()
293293 .await
···310310 }
311311 });
312312313313- let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
313313+ let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
314314 .json(&payload)
315315 .bearer_auth(token) // Assuming auth is required
316316 .send()
···340340 }
341341 });
342342343343- let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
343343+ let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
344344 .json(&payload)
345345 .bearer_auth(token) // Assuming auth is required
346346 .send()
+11-11
tests/server.rs
···77#[tokio::test]
88async fn test_health() {
99 let client = client();
1010- let res = client.get(format!("{}/health", BASE_URL))
1010+ let res = client.get(format!("{}/health", base_url().await))
1111 .send()
1212 .await
1313 .expect("Failed to send request");
···1919#[tokio::test]
2020async fn test_describe_server() {
2121 let client = client();
2222- let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", BASE_URL))
2222+ let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", base_url().await))
2323 .send()
2424 .await
2525 .expect("Failed to send request");
···3939 "email": format!("{}@example.com", handle),
4040 "password": "password"
4141 });
4242- let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
4242+ let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
4343 .json(&payload)
4444 .send()
4545 .await;
···4949 "password": "password"
5050 });
51515252- let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
5252+ let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
5353 .json(&payload)
5454 .send()
5555 .await
···6767 "password": "password"
6868 });
69697070- let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
7070+ let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
7171 .json(&payload)
7272 .send()
7373 .await
···8686 "password": "password"
8787 });
88888989- let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
8989+ let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
9090 .json(&payload)
9191 .send()
9292 .await
···9898#[tokio::test]
9999async fn test_get_session() {
100100 let client = client();
101101- let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL))
101101+ let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await))
102102 .bearer_auth(AUTH_TOKEN)
103103 .send()
104104 .await
···117117 "email": format!("{}@example.com", handle),
118118 "password": "password"
119119 });
120120- let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
120120+ let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
121121 .json(&payload)
122122 .send()
123123 .await;
···126126 "identifier": handle,
127127 "password": "password"
128128 });
129129- let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
129129+ let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
130130 .json(&login_payload)
131131 .send()
132132 .await
···137137 let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string();
138138 let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
139139140140- let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", BASE_URL))
140140+ let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", base_url().await))
141141 .bearer_auth(&refresh_jwt)
142142 .send()
143143 .await
···154154#[tokio::test]
155155async fn test_delete_session() {
156156 let client = client();
157157- let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL))
157157+ let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await))
158158 .bearer_auth(AUTH_TOKEN)
159159 .send()
160160 .await
+2-2
tests/sync.rs
···88 let params = [
99 ("did", AUTH_DID),
1010 ];
1111- let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", BASE_URL))
1111+ let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", base_url().await))
1212 .query(¶ms)
1313 .send()
1414 .await
···2424 ("did", AUTH_DID),
2525 // "cids" would be a list of CIDs
2626 ];
2727- let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", BASE_URL))
2727+ let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", base_url().await))
2828 .query(¶ms)
2929 .send()
3030 .await