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

Configure Feed

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

make jwts with the did in em

phil 65d7a109 6d65c3f4

+177 -8
+59
Cargo.lock
··· 1697 1697 checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 1698 1698 dependencies = [ 1699 1699 "cfg-if", 1700 + "js-sys", 1700 1701 "libc", 1701 1702 "wasi 0.11.0+wasi-snapshot-preview1", 1703 + "wasm-bindgen", 1702 1704 ] 1703 1705 1704 1706 [[package]] ··· 2455 2457 ] 2456 2458 2457 2459 [[package]] 2460 + name = "jsonwebtoken" 2461 + version = "9.3.1" 2462 + source = "registry+https://github.com/rust-lang/crates.io-index" 2463 + checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" 2464 + dependencies = [ 2465 + "base64 0.22.1", 2466 + "js-sys", 2467 + "pem", 2468 + "ring", 2469 + "serde", 2470 + "serde_json", 2471 + "simple_asn1", 2472 + ] 2473 + 2474 + [[package]] 2458 2475 name = "langtag" 2459 2476 version = "0.3.4" 2460 2477 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2955 2972 ] 2956 2973 2957 2974 [[package]] 2975 + name = "num-bigint" 2976 + version = "0.4.6" 2977 + source = "registry+https://github.com/rust-lang/crates.io-index" 2978 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 2979 + dependencies = [ 2980 + "num-integer", 2981 + "num-traits", 2982 + ] 2983 + 2984 + [[package]] 2958 2985 name = "num-conv" 2959 2986 version = "0.1.0" 2960 2987 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2971 2998 ] 2972 2999 2973 3000 [[package]] 3001 + name = "num-integer" 3002 + version = "0.1.46" 3003 + source = "registry+https://github.com/rust-lang/crates.io-index" 3004 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 3005 + dependencies = [ 3006 + "num-traits", 3007 + ] 3008 + 3009 + [[package]] 2974 3010 name = "num-modular" 2975 3011 version = "0.6.1" 2976 3012 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3159 3195 ] 3160 3196 3161 3197 [[package]] 3198 + name = "pem" 3199 + version = "3.0.5" 3200 + source = "registry+https://github.com/rust-lang/crates.io-index" 3201 + checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" 3202 + dependencies = [ 3203 + "base64 0.22.1", 3204 + "serde", 3205 + ] 3206 + 3207 + [[package]] 3162 3208 name = "percent-encoding" 3163 3209 version = "2.3.1" 3164 3210 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4084 4130 dependencies = [ 4085 4131 "digest", 4086 4132 "rand_core 0.6.4", 4133 + ] 4134 + 4135 + [[package]] 4136 + name = "simple_asn1" 4137 + version = "0.6.3" 4138 + source = "registry+https://github.com/rust-lang/crates.io-index" 4139 + checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" 4140 + dependencies = [ 4141 + "num-bigint", 4142 + "num-traits", 4143 + "thiserror 2.0.12", 4144 + "time", 4087 4145 ] 4088 4146 4089 4147 [[package]] ··· 5087 5145 "dashmap", 5088 5146 "handlebars", 5089 5147 "hickory-resolver", 5148 + "jsonwebtoken", 5090 5149 "metrics", 5091 5150 "metrics-exporter-prometheus 0.17.2", 5092 5151 "rand 0.9.1",
+1
who-am-i/.gitignore
··· 1 + jwt-key.pem
+1
who-am-i/Cargo.toml
··· 16 16 dashmap = "6.1.0" 17 17 handlebars = { version = "6.3.2", features = ["dir_source"] } 18 18 hickory-resolver = "0.25.2" 19 + jsonwebtoken = "9.3.1" 19 20 metrics = "0.24.2" 20 21 rand = "0.9.1" 21 22 reqwest = { version = "0.12.22", features = ["native-tls-vendored"] }
+2
who-am-i/demo/index.html
··· 12 12 13 13 <body> 14 14 <h1>hey <span id="who"></span></h1> 15 + <p><code id="jwt"></code></p> 15 16 16 17 <iframe src="http://127.0.0.1:9997/prompt" id="whoami" style="border: none" height="160" width="320"></iframe> 17 18 ··· 27 28 window.removeEventListener('message', handleMessage); 28 29 29 30 document.getElementById('who').textContent = ev.data.handle; 31 + document.getElementById('jwt').textContent = ev.data.token; 30 32 } 31 33 window.addEventListener('message', handleMessage); 32 34 })(document.getElementById('whoami'));
+55
who-am-i/src/jwt.rs
··· 1 + use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError}; 2 + use serde::Serialize; 3 + use std::fs; 4 + use std::io::Error as IOError; 5 + use std::path::Path; 6 + use std::time::{Duration, SystemTime, UNIX_EPOCH}; 7 + use thiserror::Error; 8 + 9 + #[derive(Debug, Error)] 10 + pub enum TokensSetupError { 11 + #[error(transparent)] 12 + Io(#[from] IOError), 13 + #[error("failed to retrieve ec key: {0}")] 14 + FromEc(JWTError), 15 + } 16 + 17 + #[derive(Debug, Error)] 18 + pub enum TokenMintingError { 19 + #[error("failed to mint: {0}")] 20 + FromEc(#[from] JWTError), 21 + } 22 + 23 + pub struct Tokens { 24 + encoding_key: EncodingKey, 25 + } 26 + 27 + impl Tokens { 28 + pub fn from_file(f: impl AsRef<Path>) -> Result<Self, TokensSetupError> { 29 + let data: Vec<u8> = fs::read(f)?; 30 + let encoding_key = EncodingKey::from_ec_pem(&data).map_err(TokensSetupError::FromEc)?; 31 + Ok(Self { encoding_key }) 32 + } 33 + 34 + pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> { 35 + let sub = t.to_string(); 36 + 37 + let dt_now = SystemTime::now() 38 + .duration_since(UNIX_EPOCH) 39 + .expect("unix epoch is in the past"); 40 + let dt_exp = dt_now + Duration::from_secs(30 * 86_400); 41 + let exp = dt_exp.as_secs(); 42 + 43 + Ok(encode( 44 + &Header::new(Algorithm::ES256), 45 + &Claims { sub, exp }, 46 + &self.encoding_key, 47 + )?) 48 + } 49 + } 50 + 51 + #[derive(Debug, Serialize)] 52 + struct Claims { 53 + sub: String, 54 + exp: u64, 55 + }
+2
who-am-i/src/lib.rs
··· 1 1 mod expiring_task_map; 2 + mod jwt; 2 3 mod oauth; 3 4 mod server; 4 5 5 6 pub use expiring_task_map::ExpiringTaskMap; 7 + pub use jwt::Tokens; 6 8 pub use oauth::{OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError}; 7 9 pub use server::serve;
+22 -3
who-am-i/src/main.rs
··· 1 1 use clap::{ArgAction, Parser}; 2 - use metrics_exporter_prometheus::{PrometheusBuilder, BuildError as PromBuildError}; 2 + use metrics_exporter_prometheus::{BuildError as PromBuildError, PrometheusBuilder}; 3 + use std::path::PathBuf; 3 4 use tokio_util::sync::CancellationToken; 4 - use who_am_i::serve; 5 + use who_am_i::{Tokens, serve}; 5 6 6 7 /// Aggregate links in the at-mosphere 7 8 #[derive(Parser, Debug, Clone)] ··· 14 15 /// eg: `cat /dev/urandom | head -c 64 | base64` 15 16 #[arg(long, env)] 16 17 app_secret: String, 18 + /// path to jwt key (PEM format) 19 + /// 20 + /// generate with: 21 + /// ```bash 22 + /// openssl ecparam -genkey -noout -name prime256v1 \ 23 + /// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-JWT-KEY>.pem 24 + /// ``` 25 + #[arg(long)] 26 + jwt_key: PathBuf, 17 27 /// Enable dev mode 18 28 /// 19 29 /// enables automatic template reloading ··· 44 54 println!(" - {host}"); 45 55 } 46 56 57 + let tokens = Tokens::from_file(args.jwt_key).unwrap(); 58 + 47 59 if let Err(e) = install_metrics_server() { 48 60 eprintln!("failed to install metrics server: {e:?}"); 49 61 }; 50 62 51 - serve(shutdown, args.app_secret, args.allowed_hosts, args.dev).await; 63 + serve( 64 + shutdown, 65 + args.app_secret, 66 + tokens, 67 + args.allowed_hosts, 68 + args.dev, 69 + ) 70 + .await; 52 71 } 53 72 54 73 fn install_metrics_server() -> Result<(), PromBuildError> {
+30 -1
who-am-i/src/server.rs
··· 22 22 use tokio_util::sync::CancellationToken; 23 23 use url::Url; 24 24 25 - use crate::{ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError}; 25 + use crate::{ 26 + ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError, Tokens, 27 + }; 26 28 27 29 const FAVICON: &[u8] = include_bytes!("../static/favicon.ico"); 28 30 const STYLE_CSS: &str = include_str!("../static/style.css"); ··· 41 43 pub oauth: Arc<OAuth>, 42 44 pub resolve_handles: ExpiringTaskMap<Result<String, ResolveHandleError>>, 43 45 pub shutdown: CancellationToken, 46 + pub tokens: Arc<Tokens>, 44 47 } 45 48 46 49 impl FromRef<AppState> for Key { ··· 52 55 pub async fn serve( 53 56 shutdown: CancellationToken, 54 57 app_secret: String, 58 + tokens: Tokens, 55 59 allowed_hosts: Vec<String>, 56 60 dev: bool, 57 61 ) { ··· 75 79 oauth: Arc::new(oauth), 76 80 resolve_handles: ExpiringTaskMap::new(task_pickup_expiration), 77 81 shutdown: shutdown.clone(), 82 + tokens: Arc::new(tokens), 78 83 }; 79 84 80 85 let app = Router::new() ··· 166 171 oauth, 167 172 resolve_handles, 168 173 shutdown, 174 + tokens, 169 175 .. 170 176 }): State<AppState>, 171 177 jar: SignedCookieJar, ··· 213 219 214 220 // push cookie expiry 215 221 let jar = jar.add(cookie(&did)); 222 + 223 + let token = match tokens.mint(&*did) { 224 + Ok(t) => t, 225 + Err(e) => { 226 + eprintln!("failed to create JWT: {e:?}"); 227 + return err("failed to create JWT", false); 228 + } 229 + }; 216 230 217 231 let fetch_key = resolve_handles.dispatch( 218 232 { ··· 226 240 metrics::counter!("whoami_auth_prompt", "ok" => "true", "known" => "true").increment(1); 227 241 let info = json!({ 228 242 "did": did, 243 + "token": token, 229 244 "fetch_key": fetch_key, 230 245 "parent_host": parent_host, 231 246 "parent_origin": parent_origin, ··· 340 355 resolve_handles, 341 356 oauth, 342 357 shutdown, 358 + tokens, 343 359 .. 344 360 }): State<AppState>, 345 361 Query(params): Query<OAuthCallbackParams>, ··· 386 402 387 403 let jar = jar.add(cookie(&did)); 388 404 405 + let token = match tokens.mint(&*did) { 406 + Ok(t) => t, 407 + Err(e) => { 408 + eprintln!("failed to create JWT: {e:?}"); 409 + return err( 410 + StatusCode::INTERNAL_SERVER_ERROR, 411 + "fail", 412 + "failed to create JWT", 413 + ); 414 + } 415 + }; 416 + 389 417 let fetch_key = resolve_handles.dispatch( 390 418 { 391 419 let oauth = oauth.clone(); ··· 398 426 metrics::counter!("whoami_auth_complete", "ok" => "true").increment(1); 399 427 let info = json!({ 400 428 "did": did, 429 + "token": token, 401 430 "fetch_key": fetch_key, 402 431 }); 403 432 (jar, RenderHtml("authorized", engine, info)).into_response()
+1
who-am-i/templates/authorized.hbs
··· 8 8 localStorage.setItem("who-am-i", JSON.stringify({ 9 9 result: "success", 10 10 did: {{{json did}}}, 11 + token: {{{json token}}}, 11 12 fetch_key: {{{json fetch_key}}}, 12 13 })); 13 14 window.close();
+4 -4
who-am-i/templates/prompt.hbs
··· 55 55 56 56 loaderEl.classList.add('hidden'); 57 57 handleViewEl.textContent = `@${handle}`; 58 - allowEl.addEventListener('click', () => shareAllow(handle)); 58 + allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}})); 59 59 })(); 60 60 61 61 // anon user ··· 108 108 109 109 const handle = await lookUp(parsed.fetch_key); 110 110 111 - shareAllow(handle); 111 + shareAllow(handle, token); 112 112 }); 113 113 114 114 async function lookUp(fetch_key) { ··· 125 125 return info.handle; 126 126 } 127 127 128 - const shareAllow = handle => { 128 + const shareAllow = (handle, token) => { 129 129 top.postMessage( 130 - { action: "allow", handle }, 130 + { action: "allow", handle, token }, 131 131 {{{json parent_origin}}}, 132 132 ); 133 133 }