Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

post for user info follow-up instead of get

saw some spurious double-requests that led to taking the expiring task map key early

which is maybe an indication that the fragility of only having once chance to retrieve it by key is kind of a problem, but hey we can paper over that by POSTing for now for a slightly higher chance that nothing in the stack will try to re-request somehow.

Changed files
+43 -43
who-am-i
+9 -4
who-am-i/src/expiring_task_map.rs
··· 49 .run_until_cancelled(sleep(expiration)) 50 .await 51 .is_some() 52 { 53 - // is Some if the (sleep) task completed first 54 map.remove(&k); 55 cancel.cancel(); 56 metrics::counter!("whoami_task_map_completions", "result" => "expired") ··· 62 } 63 64 pub fn take(&self, key: &str) -> Option<JoinHandle<T>> { 65 - metrics::counter!("whoami_task_map_completions", "result" => "retrieved").increment(1); 66 - // when the _guard drops, the token gets cancelled for us 67 - self.0.map.remove(key).map(|(_, (_guard, handle))| handle) 68 } 69 } 70
··· 49 .run_until_cancelled(sleep(expiration)) 50 .await 51 .is_some() 52 + // the (sleep) task completed first 53 { 54 map.remove(&k); 55 cancel.cancel(); 56 metrics::counter!("whoami_task_map_completions", "result" => "expired") ··· 62 } 63 64 pub fn take(&self, key: &str) -> Option<JoinHandle<T>> { 65 + if let Some((_key, (_guard, handle))) = self.0.map.remove(key) { 66 + // when the _guard drops, it cancels the token for us 67 + metrics::counter!("whoami_task_map_completions", "result" => "retrieved").increment(1); 68 + Some(handle) 69 + } else { 70 + metrics::counter!("whoami_task_map_gones").increment(1); 71 + None 72 + } 73 } 74 } 75
+7 -16
who-am-i/src/server.rs
··· 1 use atrium_api::types::string::Did; 2 use axum::{ 3 Router, 4 - extract::{FromRef, Query, State}, 5 http::{ 6 StatusCode, 7 - header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER, X_FRAME_OPTIONS}, 8 }, 9 response::{IntoResponse, Json, Redirect, Response}, 10 routing::{get, post}, ··· 87 .route("/favicon.ico", get(favicon)) // todo MIME 88 .route("/style.css", get(css)) 89 .route("/prompt", get(prompt)) 90 - .route("/user-info", get(user_info)) 91 .route("/auth", get(start_oauth)) 92 .route("/authorized", get(complete_oauth)) 93 .route("/disconnect", post(disconnect)) ··· 137 } else { 138 json!({}) 139 }; 140 - let frame_headers = [ 141 - (X_FRAME_OPTIONS, "deny"), 142 - (CONTENT_SECURITY_POLICY, "frame-ancestors 'none'"), 143 - ]; 144 (frame_headers, jar, RenderHtml("hello", engine, info)).into_response() 145 } 146 ··· 205 return err("Referer origin is opaque", true); 206 } 207 208 - let frame_headers = [ 209 - (X_FRAME_OPTIONS, format!("allow-from {parent_origin}")), 210 - ( 211 - CONTENT_SECURITY_POLICY, 212 - format!("frame-ancestors {parent_origin}"), 213 - ), 214 - ]; 215 216 if let Some(did) = jar.get(DID_COOKIE_KEY) { 217 let Ok(did) = Did::new(did.value_trimmed().to_string()) else { ··· 258 } 259 260 #[derive(Debug, Deserialize)] 261 - #[serde(rename_all = "kebab-case")] 262 struct UserInfoParams { 263 fetch_key: String, 264 } ··· 266 State(AppState { 267 resolve_handles, .. 268 }): State<AppState>, 269 - Query(params): Query<UserInfoParams>, 270 ) -> impl IntoResponse { 271 let err = |status, reason: &str| { 272 metrics::counter!("whoami_user_info", "found" => "false", "reason" => reason.to_string())
··· 1 use atrium_api::types::string::Did; 2 use axum::{ 3 Router, 4 + extract::{FromRef, Json as ExtractJson, Query, State}, 5 http::{ 6 StatusCode, 7 + header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER}, 8 }, 9 response::{IntoResponse, Json, Redirect, Response}, 10 routing::{get, post}, ··· 87 .route("/favicon.ico", get(favicon)) // todo MIME 88 .route("/style.css", get(css)) 89 .route("/prompt", get(prompt)) 90 + .route("/user-info", post(user_info)) 91 .route("/auth", get(start_oauth)) 92 .route("/authorized", get(complete_oauth)) 93 .route("/disconnect", post(disconnect)) ··· 137 } else { 138 json!({}) 139 }; 140 + let frame_headers = [(CONTENT_SECURITY_POLICY, "frame-ancestors 'none'")]; 141 (frame_headers, jar, RenderHtml("hello", engine, info)).into_response() 142 } 143 ··· 202 return err("Referer origin is opaque", true); 203 } 204 205 + let csp = format!("frame-ancestors {parent_origin}"); 206 + let frame_headers = [(CONTENT_SECURITY_POLICY, &csp)]; 207 208 if let Some(did) = jar.get(DID_COOKIE_KEY) { 209 let Ok(did) = Did::new(did.value_trimmed().to_string()) else { ··· 250 } 251 252 #[derive(Debug, Deserialize)] 253 struct UserInfoParams { 254 fetch_key: String, 255 } ··· 257 State(AppState { 258 resolve_handles, .. 259 }): State<AppState>, 260 + ExtractJson(params): ExtractJson<UserInfoParams>, 261 ) -> impl IntoResponse { 262 let err = |status, reason: &str| { 263 metrics::counter!("whoami_user_info", "found" => "false", "reason" => reason.to_string())
+5 -4
who-am-i/templates/hello.hbs
··· 38 ({{{json did}}}) && (async () => { 39 40 const handle = await lookUp({{{json fetch_key}}}); 41 - console.log('got handle', handle); 42 43 loaderEl.classList.add('hidden'); 44 handleViewEl.textContent = `@${handle}`; ··· 54 })(); 55 56 async function lookUp(fetch_key) { 57 - const user_info = new URL('/user-info', window.location); 58 - user_info.searchParams.set('fetch-key', fetch_key); 59 let info; 60 try { 61 - const resp = await fetch(user_info); 62 if (!resp.ok) throw resp; 63 info = await resp.json(); 64 } catch (e) {
··· 38 ({{{json did}}}) && (async () => { 39 40 const handle = await lookUp({{{json fetch_key}}}); 41 42 loaderEl.classList.add('hidden'); 43 handleViewEl.textContent = `@${handle}`; ··· 53 })(); 54 55 async function lookUp(fetch_key) { 56 let info; 57 try { 58 + const resp = await fetch('/user-info', { 59 + method: 'POST', 60 + headers: {'Content-Type': 'application/json'}, 61 + body: JSON.stringify({ fetch_key }), 62 + }); 63 if (!resp.ok) throw resp; 64 info = await resp.json(); 65 } catch (e) {
+22 -19
who-am-i/templates/prompt.hbs
··· 49 50 // already-known user 51 ({{{json did}}}) && (async () => { 52 - 53 const handle = await lookUp({{{json fetch_key}}}); 54 - console.log('got handle', handle); 55 - 56 loaderEl.classList.add('hidden'); 57 handleViewEl.textContent = `@${handle}`; 58 allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}})); ··· 74 // so if you have two flows going, it grants for both (or the first responder?) if you grant for either. 75 // (letting this slide while parent pages are allowlisted to microcosm only) 76 77 - const fail = (e, msg) => { 78 - loaderEl.classList.add('hidden'); 79 - formEl.classList.remove('hidden'); 80 - handleInputEl.focus(); 81 - handleInputEl.select(); 82 - err(e, msg); 83 - } 84 85 - const details = localStorage.getItem("who-am-i"); 86 if (!details) { 87 - console.error("hmm, heard from localstorage but did not get DID"); 88 - return; 89 } 90 - localStorage.removeItem("who-am-i"); 91 92 let parsed; 93 try { ··· 96 err(e, "something went wrong getting the details back"); 97 } 98 99 if (parsed.result === "fail") { 100 fail(`uh oh: ${parsed.reason}`); 101 } ··· 108 109 const handle = await lookUp(parsed.fetch_key); 110 111 - shareAllow(handle, token); 112 }); 113 114 async function lookUp(fetch_key) { 115 - const user_info = new URL('/user-info', window.location); 116 - user_info.searchParams.set('fetch-key', fetch_key); 117 let info; 118 try { 119 - const resp = await fetch(user_info); 120 if (!resp.ok) throw resp; 121 info = await resp.json(); 122 } catch (e) { 123 - err(e, 'failed to resolve handle from DID') 124 } 125 return info.handle; 126 } ··· 130 { action: "allow", handle, token }, 131 {{{json parent_origin}}}, 132 ); 133 } 134 135 const shareDeny = reason => {
··· 49 50 // already-known user 51 ({{{json did}}}) && (async () => { 52 const handle = await lookUp({{{json fetch_key}}}); 53 loaderEl.classList.add('hidden'); 54 handleViewEl.textContent = `@${handle}`; 55 allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}})); ··· 71 // so if you have two flows going, it grants for both (or the first responder?) if you grant for either. 72 // (letting this slide while parent pages are allowlisted to microcosm only) 73 74 + if (e.key !== 'who-am-i') return; 75 + if (e.newValue === null) return; 76 77 + const details = e.newValue; 78 if (!details) { 79 + console.error("hmm, heard from localstorage but did not get DID", details, e); 80 + err('sorry, something went wrong getting your details'); 81 } 82 + localStorage.removeItem(e.key); 83 84 let parsed; 85 try { ··· 88 err(e, "something went wrong getting the details back"); 89 } 90 91 + const fail = (e, msg) => { 92 + loaderEl.classList.add('hidden'); 93 + formEl.classList.remove('hidden'); 94 + handleInputEl.focus(); 95 + handleInputEl.select(); 96 + err(e, msg); 97 + } 98 + 99 if (parsed.result === "fail") { 100 fail(`uh oh: ${parsed.reason}`); 101 } ··· 108 109 const handle = await lookUp(parsed.fetch_key); 110 111 + shareAllow(handle, parsed.token); 112 }); 113 114 async function lookUp(fetch_key) { 115 let info; 116 try { 117 + const resp = await fetch('/user-info', { 118 + method: 'POST', 119 + headers: { 'Content-Type': 'application/json' }, 120 + body: JSON.stringify({ fetch_key }), 121 + }); 122 if (!resp.ok) throw resp; 123 info = await resp.json(); 124 } catch (e) { 125 + err(e, `failed to resolve handle from DID with ${fetch_key}`); 126 } 127 return info.handle; 128 } ··· 132 { action: "allow", handle, token }, 133 {{{json parent_origin}}}, 134 ); 135 + promptEl.textContent = '✔️ shared'; 136 } 137 138 const shareDeny = reason => {