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