atproto Thingiverse but good
10

Configure Feed

Select the types of activity you want to include in your feed.

PM-46: SSR session bridge

Orual (Jun 29, 2026, 7:06 PM EDT) 708124c2 63584366

+273 -20
+1 -1
.polytoken/subagents/playwright-explorer.md
··· 2 2 name: playwright-explorer 3 3 description: Use when exploring websites, proving hypotheses about web application behavior, automating browser interactions, or generating E2E tests - investigates page structure through accessibility snapshots, tests assumptions systematically, and coordinates Playwright MCP tools with tenacity to complete complex multi-step investigations 4 4 polytoken: 5 - model: codex/gpt-5.4-mini(high) 5 + model: codex/gpt-5.4-mini 6 6 tools: [file_read, grep, glob, skill, mcp__atlassian, mcp__playwright] 7 7 undeferred_tools: [file_read, grep, glob] 8 8 skills_allow: ["frontend-design", "polytoken:investigating-a-codebase"]
+17 -7
src/appview/actor.rs
··· 5 5 use axum::extract::State; 6 6 use jacquard::identity::PublicResolver; 7 7 use jacquard::identity::resolver::IdentityResolver; 8 - use jacquard_axum::oauth::OptionalBrowserOAuthSession; 8 + use jacquard::oauth::client::OAuthSession; 9 + use jacquard_axum::oauth::ExtractOptionalOAuthSession; 9 10 use jacquard_axum::{ExtractXrpc, XrpcResponse}; 10 11 use jacquard_common::deps::smol_str::SmolStr; 11 12 use jacquard_common::types::ident::AtIdentifier; ··· 87 88 88 89 pub(super) async fn get_session( 89 90 State(state): State<AppState>, 90 - OptionalBrowserOAuthSession(session): OptionalBrowserOAuthSession< 91 + ExtractOptionalOAuthSession(session): ExtractOptionalOAuthSession< 91 92 PublicResolver, 92 93 SqliteAuthStore, 93 94 >, 94 95 ) -> AppResult<XrpcResponse<GetSessionRequest>> { 96 + Ok(XrpcResponse( 97 + get_session_output(&state, session.as_ref()).await?, 98 + )) 99 + } 100 + 101 + pub(crate) async fn get_session_output( 102 + state: &AppState, 103 + session: Option<&OAuthSession<PublicResolver, SqliteAuthStore>>, 104 + ) -> AppResult<GetSessionOutput> { 95 105 if let Some(session) = session.as_ref() { 96 106 let t_session = std::time::Instant::now(); 97 107 let did = session.session_info().await.0; ··· 128 138 profile_ms = t_profile.elapsed().as_millis(), 129 139 "get_session authed timing" 130 140 ); 131 - return Ok(XrpcResponse(GetSessionOutput { 141 + return Ok(GetSessionOutput { 132 142 value: SessionView::SessionAuthenticated(Box::new(SessionAuthenticated { 133 143 authenticated: true, 134 144 did, ··· 137 147 extra_data: None, 138 148 })), 139 149 extra_data: None, 140 - })); 150 + }); 141 151 } 142 152 Err(error) => { 143 153 tracing::warn!(error = ?error, did = %did, "session handle resolution failed; reporting unauthenticated"); ··· 148 158 Ok(unauthenticated_session()) 149 159 } 150 160 151 - fn unauthenticated_session() -> XrpcResponse<GetSessionRequest> { 152 - XrpcResponse(GetSessionOutput { 161 + fn unauthenticated_session() -> GetSessionOutput { 162 + GetSessionOutput { 153 163 value: SessionView::SessionUnauthenticated(Box::new(SessionUnauthenticated { 154 164 authenticated: false, 155 165 extra_data: None, 156 166 })), 157 167 extra_data: None, 158 - }) 168 + } 159 169 } 160 170 161 171 fn minimal_session_profile(did: Did, handle: String) -> AppResult<ProfileView> {
+5 -1
src/appview/mod.rs
··· 15 15 //! forwarded with their real status + body. 16 16 17 17 pub mod error; 18 + pub mod ssr; 18 19 pub mod state; 19 20 pub mod views; 20 21 ··· 106 107 )) 107 108 // getSession takes no parameters, so it bypasses ExtractXrpc (which 108 109 // decodes the query string; a unit-struct request from an empty query is 109 - // rejected by serde_html_form). Identity comes from the session. 110 + // rejected by serde_html_form). It uses Jacquard's API/headless optional 111 + // extractor so the documented x-jacquard-session fallback is confined to 112 + // this session bridge surface; other public read endpoints keep the 113 + // browser-oriented optional extractor semantics. 110 114 .route( 111 115 GetSessionRequest::PATH, 112 116 axum::routing::get(actor::get_session),
+70
src/appview/ssr.rs
··· 1 + //! Server-side bridge for SSR and Dioxus server functions. 2 + //! 3 + //! Normal SSR/server-function reads should extract the inbound OAuth session 4 + //! from the current Axum request and call appview helpers directly in-process. 5 + //! The `x-jacquard-session` utility below is only an escape hatch for the rare 6 + //! server-side outbound XRPC call that cannot be avoided. 7 + 8 + use axum::extract::FromRequestParts; 9 + use dioxus::prelude::{ServerFnError, dioxus_fullstack::FullstackContext}; 10 + use http::HeaderValue; 11 + use jacquard::identity::PublicResolver; 12 + use jacquard::oauth::client::OAuthSession; 13 + use jacquard_axum::oauth::ExtractOptionalOAuthSession; 14 + use jacquard_common::session::SessionKey; 15 + use jacquard_common::xrpc::CallOptions; 16 + use polymodel_api::space_polymodel::actor::get_session::GetSessionOutput; 17 + 18 + use super::actor; 19 + use super::state::AppState; 20 + use crate::oauth::SqliteAuthStore; 21 + 22 + pub type PolymodelOAuthSession = OAuthSession<PublicResolver, SqliteAuthStore>; 23 + 24 + /// Extract the current request's optional OAuth session and resolve the 25 + /// `getSession` appview output without issuing a same-origin XRPC request. 26 + pub async fn get_session_output_from_fullstack_context() -> Result<GetSessionOutput, ServerFnError> 27 + { 28 + let context = FullstackContext::current() 29 + .ok_or_else(|| ServerFnError::new("missing Dioxus fullstack request context"))?; 30 + let state = context 31 + .extension::<AppState>() 32 + .ok_or_else(|| ServerFnError::new("missing Polymodel AppState request extension"))?; 33 + let session = extract_optional_oauth_session(&context, &state).await?; 34 + actor::get_session_output(&state, session.as_ref()) 35 + .await 36 + .map_err(|error| ServerFnError::new(format!("{error:?}"))) 37 + } 38 + 39 + async fn extract_optional_oauth_session( 40 + context: &FullstackContext, 41 + state: &AppState, 42 + ) -> Result<Option<PolymodelOAuthSession>, ServerFnError> { 43 + let mut parts = context.parts_mut().clone(); 44 + ExtractOptionalOAuthSession::<PublicResolver, SqliteAuthStore>::from_request_parts( 45 + &mut parts, state, 46 + ) 47 + .await 48 + .map(|ExtractOptionalOAuthSession(session)| session) 49 + .map_err(|error| ServerFnError::new(error.to_string())) 50 + } 51 + 52 + /// Build Jacquard call options carrying the encoded session-key header. 53 + /// 54 + /// Use this only when a server-side outbound XRPC hop is unavoidable. Normal 55 + /// SSR/server-function appview reads should stay in-process via the helpers 56 + /// above. 57 + #[allow(dead_code)] 58 + pub fn session_key_call_options( 59 + state: &AppState, 60 + key: &SessionKey, 61 + ) -> Result<CallOptions, ServerFnError> { 62 + let encoded = jacquard_axum::oauth::encode_session_key(key) 63 + .map_err(|error| ServerFnError::new(error.to_string()))?; 64 + let value = 65 + HeaderValue::from_str(&encoded).map_err(|error| ServerFnError::new(error.to_string()))?; 66 + Ok(CallOptions { 67 + extra_headers: vec![(state.oauth_config.session_header.clone(), value)], 68 + ..CallOptions::default() 69 + }) 70 + }
+152
src/appview/tests.rs
··· 10 10 11 11 use axum::body::Body; 12 12 use axum::http::{Request, StatusCode}; 13 + use dioxus::prelude::dioxus_fullstack::FullstackContext; 14 + use jacquard::oauth::authstore::ClientAuthStore; 15 + use jacquard::oauth::client::OAuthSession; 16 + use jacquard::oauth::keyset::Keyset; 17 + use jacquard::oauth::scopes::Scopes; 18 + use jacquard::oauth::session::{ClientSessionData, DpopClientData}; 19 + use jacquard::oauth::types::{OAuthTokenType, TokenSet}; 20 + use jacquard_common::deps::fluent_uri::Uri; 21 + use jacquard_common::deps::smol_str::SmolStr; 22 + use jacquard_common::session::SessionKey; 23 + use jacquard_common::types::datetime::Datetime; 13 24 use jacquard_common::types::string::Did; 14 25 use polymodel_api::space_polymodel::actor::ProfileViewUnion; 26 + use polymodel_api::space_polymodel::actor::get_session::{GetSessionOutput, SessionView}; 15 27 use serde_json::json; 16 28 use sqlx::SqlitePool; 17 29 use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; ··· 28 40 29 41 fn did(s: &str) -> Did { 30 42 Did::new_owned(s).unwrap() 43 + } 44 + 45 + fn client_session(account_did: &str, session_id: &str) -> ClientSessionData { 46 + let dpop_key = Keyset::generate_es256("test") 47 + .unwrap() 48 + .public_jwks() 49 + .keys 50 + .into_iter() 51 + .next() 52 + .unwrap() 53 + .key; 54 + ClientSessionData { 55 + account_did: did(account_did), 56 + session_id: SmolStr::new(session_id), 57 + host_url: Uri::parse("https://pds.example.com").unwrap().to_owned(), 58 + authserver_url: SmolStr::new("https://issuer.example.com"), 59 + authserver_token_endpoint: SmolStr::new("https://issuer.example.com/token"), 60 + authserver_revocation_endpoint: None, 61 + scopes: Scopes::empty(), 62 + dpop_data: DpopClientData { 63 + dpop_key, 64 + dpop_authserver_nonce: SmolStr::default(), 65 + dpop_host_nonce: SmolStr::default(), 66 + }, 67 + token_set: TokenSet { 68 + iss: SmolStr::new("https://issuer.example.com"), 69 + sub: did(account_did), 70 + aud: SmolStr::new("https://pds.example.com"), 71 + scope: None, 72 + refresh_token: None, 73 + access_token: SmolStr::new("token"), 74 + token_type: OAuthTokenType::DPoP, 75 + expires_at: Some(Datetime::raw_str("2099-01-01T00:00:00.000Z")), 76 + }, 77 + resolved_scopes: None, 78 + } 79 + } 80 + 81 + async fn seed_oauth_session(state: &AppState, account_did: &str, session_id: &str) -> SessionKey { 82 + let data = client_session(account_did, session_id); 83 + let key = SessionKey::new(data.account_did.clone(), data.session_id.clone()); 84 + state 85 + .oauth 86 + .registry 87 + .store 88 + .upsert_session(data) 89 + .await 90 + .unwrap(); 91 + key 31 92 } 32 93 33 94 async fn pool() -> SqlitePool { ··· 658 719 assert_eq!(resp.status(), StatusCode::OK); 659 720 let body = body_text(resp).await; 660 721 assert!(body.contains("\"authenticated\":false"), "body was: {body}"); 722 + } 723 + 724 + #[tokio::test] 725 + async fn get_session_in_process_helper_returns_authenticated_viewer() { 726 + let state = state().await; 727 + seed_identity(&state.pool, DID_A, "alice.com").await; 728 + seed_profile(&state.pool, DID_A, "Alice").await; 729 + let session_data = client_session(DID_A, "in-process-session"); 730 + let session = OAuthSession::new( 731 + state.oauth.registry.clone(), 732 + state.oauth.client.clone(), 733 + session_data, 734 + ); 735 + 736 + let output = super::actor::get_session_output(&state, Some(&session)) 737 + .await 738 + .unwrap(); 739 + 740 + match output.value { 741 + SessionView::SessionAuthenticated(auth) => { 742 + assert!(auth.authenticated); 743 + assert_eq!(auth.did, did(DID_A)); 744 + assert_eq!(auth.profile.display_name.as_deref(), Some("Alice")); 745 + } 746 + other => panic!("expected authenticated session, got {other:?}"), 747 + } 748 + } 749 + 750 + #[tokio::test] 751 + async fn get_session_route_accepts_x_jacquard_session_header() { 752 + let state = state().await; 753 + seed_identity(&state.pool, DID_A, "alice.com").await; 754 + seed_profile(&state.pool, DID_A, "Alice").await; 755 + let key = seed_oauth_session(&state, DID_A, "header-session").await; 756 + let encoded = jacquard_axum::oauth::encode_session_key(&key).unwrap(); 757 + let header_name = state.oauth_config.session_header.clone(); 758 + let app = super::router().with_state(state); 759 + 760 + let resp = app 761 + .oneshot( 762 + Request::builder() 763 + .uri("/xrpc/space.polymodel.actor.getSession") 764 + .header(header_name, encoded) 765 + .body(Body::empty()) 766 + .unwrap(), 767 + ) 768 + .await 769 + .unwrap(); 770 + 771 + assert_eq!(resp.status(), StatusCode::OK); 772 + let body = body_text(resp).await; 773 + let output: GetSessionOutput = serde_json::from_str(&body).unwrap(); 774 + match output.value { 775 + SessionView::SessionAuthenticated(auth) => { 776 + assert!(auth.authenticated); 777 + assert_eq!(auth.did, did(DID_A)); 778 + } 779 + other => panic!("expected authenticated session from header, got {other:?}"), 780 + } 781 + } 782 + 783 + #[tokio::test] 784 + async fn fullstack_context_bridge_returns_authenticated_session_from_header() { 785 + let state = state().await; 786 + seed_identity(&state.pool, DID_A, "alice.com").await; 787 + seed_profile(&state.pool, DID_A, "Alice").await; 788 + let key = seed_oauth_session(&state, DID_A, "fullstack-header-session").await; 789 + let encoded = jacquard_axum::oauth::encode_session_key(&key).unwrap(); 790 + let header_name = state.oauth_config.session_header.clone(); 791 + 792 + let mut parts = Request::builder() 793 + .uri("/") 794 + .header(header_name, encoded) 795 + .body(()) 796 + .unwrap() 797 + .into_parts() 798 + .0; 799 + parts.extensions.insert(state); 800 + 801 + let output = FullstackContext::new(parts) 802 + .scope(super::ssr::get_session_output_from_fullstack_context()) 803 + .await 804 + .unwrap(); 805 + 806 + match output.value { 807 + SessionView::SessionAuthenticated(auth) => { 808 + assert!(auth.authenticated); 809 + assert_eq!(auth.did, did(DID_A)); 810 + } 811 + other => panic!("expected authenticated session from fullstack bridge, got {other:?}"), 812 + } 661 813 } 662 814 663 815 #[tokio::test]
+28 -11
src/main.rs
··· 1 + use dioxus::prelude::ServerFnError; 1 2 use dioxus::prelude::*; 2 3 use jacquard_common::xrpc::XrpcClient; 3 - use polymodel_api::space_polymodel::actor::get_session::GetSession; 4 + use polymodel_api::space_polymodel::actor::get_session::{GetSession, GetSessionOutput}; 4 5 5 6 mod auth; 6 7 mod browse; ··· 84 85 .expect("failed to bootstrap OAuth client"); 85 86 // The same SQLite pool backs both indexing and the appview read handlers. 86 87 let app_state = appview::state::AppState::new(db, oauth); 87 - Ok(dioxus::server::router(App).merge( 88 - appview::router() 89 - .merge(jacquard_axum::oauth::routes::< 90 - jacquard::identity::PublicResolver, 91 - oauth::SqliteAuthStore, 92 - appview::state::AppState, 93 - >(&app_state.oauth_config.clone())) 94 - .with_state(app_state), 95 - )) 88 + Ok(dioxus::server::router(App) 89 + .layer(axum::Extension(app_state.clone())) 90 + .merge( 91 + appview::router() 92 + .merge(jacquard_axum::oauth::routes::< 93 + jacquard::identity::PublicResolver, 94 + oauth::SqliteAuthStore, 95 + appview::state::AppState, 96 + >(&app_state.oauth_config.clone())) 97 + .with_state(app_state), 98 + )) 96 99 }); 97 100 } 98 101 ··· 133 136 #[component] 134 137 fn App() -> Element { 135 138 let client = use_context_provider(PolymodelClient::default); 136 - let session = use_context_provider(|| Signal::new(SessionIdentity::Anonymous)); 139 + let mut session = use_context_provider(|| Signal::new(SessionIdentity::Anonymous)); 137 140 let toasts = use_context_provider(|| Signal::new(auth::ToastState::default())); 141 + 142 + let ssr_session_seed = use_server_future(server_session_seed)?; 143 + if let Some(Ok(output)) = ssr_session_seed() { 144 + session.set(SessionIdentity::from_get_session(output)); 145 + } 138 146 139 147 let _session_seed = use_resource(move || { 140 148 let client = client.clone(); 141 149 let mut session = session; 142 150 async move { 151 + #[cfg(feature = "server")] 152 + if dioxus::prelude::dioxus_fullstack::FullstackContext::current().is_some() { 153 + return; 154 + } 143 155 match client.send(GetSession).await { 144 156 Ok(response) => match response.into_output() { 145 157 Ok(output) => session.set(SessionIdentity::from_get_session(output)), ··· 168 180 ToastShelf { toasts } 169 181 } 170 182 } 183 + 184 + #[server] 185 + async fn server_session_seed() -> Result<GetSessionOutput, ServerFnError> { 186 + appview::ssr::get_session_output_from_fullstack_context().await 187 + }