APIs for links and references in the ATmosphere

look up identity from did-cookie

+468 -25
+161
Cargo.lock
··· 500 500 ] 501 501 502 502 [[package]] 503 + name = "axum-template" 504 + version = "3.0.0" 505 + source = "registry+https://github.com/rust-lang/crates.io-index" 506 + checksum = "3df50f7d669bfc3a8c348f08f536fe37e7acfbeded3cfdffd2ad3d76725fc40c" 507 + dependencies = [ 508 + "axum", 509 + "handlebars", 510 + "serde", 511 + "thiserror 2.0.12", 512 + ] 513 + 514 + [[package]] 503 515 name = "backtrace" 504 516 version = "0.3.74" 505 517 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1164 1176 ] 1165 1177 1166 1178 [[package]] 1179 + name = "derive_builder" 1180 + version = "0.20.2" 1181 + source = "registry+https://github.com/rust-lang/crates.io-index" 1182 + checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 1183 + dependencies = [ 1184 + "derive_builder_macro", 1185 + ] 1186 + 1187 + [[package]] 1188 + name = "derive_builder_core" 1189 + version = "0.20.2" 1190 + source = "registry+https://github.com/rust-lang/crates.io-index" 1191 + checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 1192 + dependencies = [ 1193 + "darling", 1194 + "proc-macro2", 1195 + "quote", 1196 + "syn", 1197 + ] 1198 + 1199 + [[package]] 1200 + name = "derive_builder_macro" 1201 + version = "0.20.2" 1202 + source = "registry+https://github.com/rust-lang/crates.io-index" 1203 + checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 1204 + dependencies = [ 1205 + "derive_builder_core", 1206 + "syn", 1207 + ] 1208 + 1209 + [[package]] 1167 1210 name = "digest" 1168 1211 version = "0.10.7" 1169 1212 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1716 1759 "tokio", 1717 1760 "tokio-util", 1718 1761 "tracing", 1762 + ] 1763 + 1764 + [[package]] 1765 + name = "handlebars" 1766 + version = "6.3.2" 1767 + source = "registry+https://github.com/rust-lang/crates.io-index" 1768 + checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" 1769 + dependencies = [ 1770 + "derive_builder", 1771 + "log", 1772 + "num-order", 1773 + "pest", 1774 + "pest_derive", 1775 + "serde", 1776 + "serde_json", 1777 + "thiserror 2.0.12", 1778 + "walkdir", 1719 1779 ] 1720 1780 1721 1781 [[package]] ··· 2879 2939 ] 2880 2940 2881 2941 [[package]] 2942 + name = "num-modular" 2943 + version = "0.6.1" 2944 + source = "registry+https://github.com/rust-lang/crates.io-index" 2945 + checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" 2946 + 2947 + [[package]] 2948 + name = "num-order" 2949 + version = "1.2.0" 2950 + source = "registry+https://github.com/rust-lang/crates.io-index" 2951 + checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" 2952 + dependencies = [ 2953 + "num-modular", 2954 + ] 2955 + 2956 + [[package]] 2882 2957 name = "num-traits" 2883 2958 version = "0.2.19" 2884 2959 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3058 3133 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 3059 3134 3060 3135 [[package]] 3136 + name = "pest" 3137 + version = "2.8.1" 3138 + source = "registry+https://github.com/rust-lang/crates.io-index" 3139 + checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" 3140 + dependencies = [ 3141 + "memchr", 3142 + "thiserror 2.0.12", 3143 + "ucd-trie", 3144 + ] 3145 + 3146 + [[package]] 3147 + name = "pest_derive" 3148 + version = "2.8.1" 3149 + source = "registry+https://github.com/rust-lang/crates.io-index" 3150 + checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" 3151 + dependencies = [ 3152 + "pest", 3153 + "pest_generator", 3154 + ] 3155 + 3156 + [[package]] 3157 + name = "pest_generator" 3158 + version = "2.8.1" 3159 + source = "registry+https://github.com/rust-lang/crates.io-index" 3160 + checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" 3161 + dependencies = [ 3162 + "pest", 3163 + "pest_meta", 3164 + "proc-macro2", 3165 + "quote", 3166 + "syn", 3167 + ] 3168 + 3169 + [[package]] 3170 + name = "pest_meta" 3171 + version = "2.8.1" 3172 + source = "registry+https://github.com/rust-lang/crates.io-index" 3173 + checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" 3174 + dependencies = [ 3175 + "pest", 3176 + "sha2", 3177 + ] 3178 + 3179 + [[package]] 3061 3180 name = "pin-project-lite" 3062 3181 version = "0.2.16" 3063 3182 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3602 3721 checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 3603 3722 3604 3723 [[package]] 3724 + name = "same-file" 3725 + version = "1.0.6" 3726 + source = "registry+https://github.com/rust-lang/crates.io-index" 3727 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 3728 + dependencies = [ 3729 + "winapi-util", 3730 + ] 3731 + 3732 + [[package]] 3605 3733 name = "schannel" 3606 3734 version = "0.1.27" 3607 3735 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4557 4685 checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 4558 4686 4559 4687 [[package]] 4688 + name = "ucd-trie" 4689 + version = "0.1.7" 4690 + source = "registry+https://github.com/rust-lang/crates.io-index" 4691 + checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 4692 + 4693 + [[package]] 4560 4694 name = "ufos" 4561 4695 version = "0.1.0" 4562 4696 dependencies = [ ··· 4734 4868 ] 4735 4869 4736 4870 [[package]] 4871 + name = "walkdir" 4872 + version = "2.5.0" 4873 + source = "registry+https://github.com/rust-lang/crates.io-index" 4874 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 4875 + dependencies = [ 4876 + "same-file", 4877 + "winapi-util", 4878 + ] 4879 + 4880 + [[package]] 4737 4881 name = "want" 4738 4882 version = "0.3.1" 4739 4883 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4865 5009 version = "0.1.0" 4866 5010 dependencies = [ 4867 5011 "atrium-api 0.25.4", 5012 + "atrium-common 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 4868 5013 "atrium-identity", 4869 5014 "atrium-oauth", 4870 5015 "axum", 4871 5016 "axum-extra", 5017 + "axum-template", 4872 5018 "clap", 5019 + "ctrlc", 5020 + "dashmap", 5021 + "handlebars", 4873 5022 "hickory-resolver", 4874 5023 "metrics", 5024 + "rand 0.9.1", 4875 5025 "serde", 5026 + "serde_json", 4876 5027 "tokio", 4877 5028 "tokio-util", 5029 + "url", 4878 5030 ] 4879 5031 4880 5032 [[package]] ··· 4898 5050 version = "0.4.0" 4899 5051 source = "registry+https://github.com/rust-lang/crates.io-index" 4900 5052 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 5053 + 5054 + [[package]] 5055 + name = "winapi-util" 5056 + version = "0.1.9" 5057 + source = "registry+https://github.com/rust-lang/crates.io-index" 5058 + checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 5059 + dependencies = [ 5060 + "windows-sys 0.48.0", 5061 + ] 4901 5062 4902 5063 [[package]] 4903 5064 name = "winapi-x86_64-pc-windows-gnu"
+8
who-am-i/Cargo.toml
··· 5 5 6 6 [dependencies] 7 7 atrium-api = { version = "0.25.4", default-features = false } 8 + atrium-common = "0.1.2" 8 9 atrium-identity = "0.1.5" 9 10 atrium-oauth = "0.1.3" 10 11 axum = "0.8.4" 11 12 axum-extra = { version = "0.10.1", features = ["cookie-signed", "typed-header"] } 13 + axum-template = { version = "3.0.0", features = ["handlebars"] } 12 14 clap = { version = "4.5.40", features = ["derive"] } 15 + ctrlc = "3.4.7" 16 + dashmap = "6.1.0" 17 + handlebars = { version = "6.3.2", features = ["dir_source"] } 13 18 hickory-resolver = "0.25.2" 14 19 metrics = "0.24.2" 20 + rand = "0.9.1" 15 21 serde = { version = "1.0.219", features = ["derive"] } 22 + serde_json = "1.0.140" 16 23 tokio = { version = "1.45.1", features = ["full", "macros"] } 17 24 tokio-util = "0.7.15" 25 + url = "2.5.4"
+1 -3
who-am-i/demo/index.html
··· 9 9 10 10 <h1>hey</h1> 11 11 12 - <iframe src="http://127.0.0.1:9997/prompt" style="border: 2px solid #000; border-radius: 0.5em;" /> 13 - 14 - 12 + <iframe src="http://127.0.0.1:9997/prompt" style="border: none" height="140" width="280" />
+53
who-am-i/src/expiring_task_map.rs
··· 1 + use dashmap::DashMap; 2 + use rand::{Rng, distr::Alphanumeric}; 3 + use std::sync::Arc; 4 + use std::time::Duration; 5 + use tokio::task::{JoinHandle, spawn}; 6 + use tokio::time::sleep; // 0.8 7 + 8 + #[derive(Clone)] 9 + pub struct ExpiringTaskMap<T>(Arc<TaskMap<T>>); 10 + 11 + impl<T: Send + 'static> ExpiringTaskMap<T> { 12 + pub fn new(expiration: Duration) -> Self { 13 + let map = TaskMap { 14 + map: DashMap::new(), 15 + expiration, 16 + }; 17 + Self(Arc::new(map)) 18 + } 19 + 20 + pub fn dispatch(&self, task: impl Future<Output = T> + Send + 'static) -> String { 21 + let task_key: String = rand::rng() 22 + .sample_iter(&Alphanumeric) 23 + .take(24) 24 + .map(char::from) 25 + .collect(); 26 + 27 + // spawn a tokio task and put the join handle in the map for later retrieval 28 + self.0.map.insert(task_key.clone(), spawn(task)); 29 + 30 + // spawn a second task to clean up the map in case it doesn't get claimed 31 + spawn({ 32 + let me = self.0.clone(); 33 + let key = task_key.clone(); 34 + async move { 35 + sleep(me.expiration).await; 36 + let _ = me.map.remove(&key); 37 + // TODO: also use a cancellation token so taking and expiring can mutually cancel 38 + } 39 + }); 40 + 41 + task_key 42 + } 43 + 44 + pub fn take(&self, key: &str) -> Option<JoinHandle<T>> { 45 + eprintln!("trying to take..."); 46 + self.0.map.remove(key).map(|(_, handle)| handle) 47 + } 48 + } 49 + 50 + struct TaskMap<T> { 51 + map: DashMap<String, JoinHandle<T>>, 52 + expiration: Duration, 53 + }
+20
who-am-i/src/identity_resolver.rs
··· 1 + use atrium_api::types::string::Did; 2 + use atrium_common::resolver::Resolver; 3 + use atrium_identity::did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}; 4 + use atrium_oauth::DefaultHttpClient; 5 + use std::sync::Arc; 6 + 7 + pub async fn resolve_identity(did: String) -> String { 8 + let http_client = Arc::new(DefaultHttpClient::default()); 9 + let resolver = CommonDidResolver::new(CommonDidResolverConfig { 10 + plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), 11 + http_client: Arc::clone(&http_client), 12 + }); 13 + let doc = resolver.resolve(&Did::new(did).unwrap()).await.unwrap(); // TODO: this is only half the resolution? or is atrium checking dns? 14 + if let Some(aka) = doc.also_known_as { 15 + if let Some(f) = aka.first() { 16 + return f.to_string(); 17 + } 18 + } 19 + "who knows".to_string() 20 + }
+4
who-am-i/src/lib.rs
··· 1 1 mod dns_resolver; 2 + mod expiring_task_map; 3 + mod identity_resolver; 2 4 mod oauth; 3 5 mod server; 4 6 5 7 pub use dns_resolver::HickoryDnsTxtResolver; 8 + pub use expiring_task_map::ExpiringTaskMap; 9 + pub use identity_resolver::resolve_identity; 6 10 pub use oauth::{Client, authorize, client}; 7 11 pub use server::serve;
+27 -2
who-am-i/src/main.rs
··· 1 + use clap::Parser; 1 2 use tokio_util::sync::CancellationToken; 2 3 use who_am_i::serve; 3 4 5 + /// Aggregate links in the at-mosphere 6 + #[derive(Parser, Debug, Clone)] 7 + #[command(version, about, long_about = None)] 8 + struct Args { 9 + /// secret key from which the cookie-signing key is derived 10 + /// 11 + /// must have at least 512 bits (64 bytes) of randomness 12 + /// 13 + /// eg: `cat /dev/urandom | head -c 64 | base64` 14 + #[arg(long)] 15 + app_secret: String, 16 + /// Enable dev mode 17 + /// 18 + /// enables automatic template reloading 19 + #[arg(long, action)] 20 + dev: bool, 21 + } 22 + 4 23 #[tokio::main] 5 24 async fn main() { 6 - let server_shutdown = CancellationToken::new(); 7 - serve(server_shutdown).await; 25 + let shutdown = CancellationToken::new(); 26 + 27 + let ctrlc_shutdown = shutdown.clone(); 28 + ctrlc::set_handler(move || ctrlc_shutdown.cancel()).expect("failed to set ctrl-c handler"); 29 + 30 + let args = Args::parse(); 31 + 32 + serve(shutdown, args.app_secret, args.dev).await; 8 33 }
+98 -20
who-am-i/src/server.rs
··· 3 3 use axum::{ 4 4 Router, 5 5 extract::{FromRef, Query, State}, 6 + http::header::{HeaderMap, REFERER}, 6 7 response::{Html, IntoResponse, Redirect}, 7 8 routing::get, 8 9 }; 9 10 use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar}; 11 + use axum_template::{RenderHtml, engine::Engine}; 12 + use handlebars::{Handlebars, handlebars_helper}; 10 13 11 - use serde::Deserialize; 14 + use serde::{Deserialize, Serialize}; 15 + use serde_json::Value; 12 16 use std::sync::Arc; 17 + use std::time::Duration; 13 18 use tokio::net::TcpListener; 14 19 use tokio_util::sync::CancellationToken; 20 + use url::Url; 15 21 16 - use crate::{Client, authorize, client}; 22 + use crate::{Client, ExpiringTaskMap, authorize, client, resolve_identity}; 17 23 18 24 const FAVICON: &[u8] = include_bytes!("../static/favicon.ico"); 19 25 const INDEX_HTML: &str = include_str!("../static/index.html"); 20 26 const LOGIN_HTML: &str = include_str!("../static/login.html"); 21 27 22 - pub async fn serve(shutdown: CancellationToken) { 28 + const DID_COOKIE_KEY: &str = "did"; 29 + 30 + type AppEngine = Engine<Handlebars<'static>>; 31 + 32 + #[derive(Clone)] 33 + struct AppState { 34 + pub key: Key, 35 + pub engine: AppEngine, 36 + pub client: Arc<Client>, 37 + pub resolving: ExpiringTaskMap<String>, 38 + } 39 + 40 + impl FromRef<AppState> for Key { 41 + fn from_ref(state: &AppState) -> Self { 42 + state.key.clone() 43 + } 44 + } 45 + 46 + pub async fn serve(shutdown: CancellationToken, app_secret: String, dev: bool) { 47 + let mut hbs = Handlebars::new(); 48 + hbs.set_dev_mode(dev); 49 + hbs.register_templates_directory("templates", Default::default()) 50 + .unwrap(); 51 + 52 + handlebars_helper!(json: |v: Value| serde_json::to_string(&v).unwrap()); 53 + hbs.register_helper("json", Box::new(json)); 54 + 55 + // clients have to pick up their identity-resolving tasks within this period 56 + let task_pickup_expiration = Duration::from_secs(15); 57 + 23 58 let state = AppState { 24 - key: Key::generate(), // TODO: via config 59 + engine: Engine::new(hbs), 60 + key: Key::from(app_secret.as_bytes()), // TODO: via config 25 61 client: Arc::new(client()), 62 + resolving: ExpiringTaskMap::new(task_pickup_expiration), 26 63 }; 27 64 28 65 let app = Router::new() 29 66 .route("/", get(|| async { Html(INDEX_HTML) })) 30 67 .route("/favicon.ico", get(|| async { FAVICON })) // todo MIME 31 68 .route("/prompt", get(prompt)) 69 + .route("/user-info", get(user_info)) 32 70 .route("/auth", get(start_oauth)) 33 71 .route("/authorized", get(complete_oauth)) 34 72 .with_state(state); ··· 43 81 .unwrap(); 44 82 } 45 83 46 - #[derive(Clone)] 47 - struct AppState { 48 - pub key: Key, 49 - pub client: Arc<Client>, 84 + #[derive(Debug, Serialize)] 85 + struct Known { 86 + did: Value, 87 + fetch_key: Value, 88 + parent_host: String, 50 89 } 90 + async fn prompt( 91 + State(AppState { 92 + engine, resolving, .. 93 + }): State<AppState>, 94 + jar: SignedCookieJar, 95 + headers: HeaderMap, 96 + ) -> impl IntoResponse { 97 + let Some(referrer) = headers.get(REFERER) else { 98 + return Html::<&'static str>("missing referrer, sorry").into_response(); 99 + }; 100 + let Ok(referrer) = referrer.to_str() else { 101 + return "referer contained opaque bytes".into_response(); 102 + }; 103 + let Ok(url) = Url::parse(referrer) else { 104 + return "referrer was not a url".into_response(); 105 + }; 106 + let Some(parent_host) = url.host_str() else { 107 + return "could nto get host from url".into_response(); 108 + }; 109 + let m = if let Some(did) = jar.get(DID_COOKIE_KEY) { 110 + let did = did.value_trimmed().to_string(); 51 111 52 - impl FromRef<AppState> for Key { 53 - fn from_ref(state: &AppState) -> Self { 54 - state.key.clone() 55 - } 112 + let fetch_key = resolving.dispatch(resolve_identity(did.clone())); 113 + 114 + let json_did = Value::String(did); 115 + let json_fetch_key = Value::String(fetch_key); 116 + let known = Known { 117 + did: json_did, 118 + fetch_key: json_fetch_key, 119 + parent_host: parent_host.to_string(), 120 + }; 121 + return (jar, RenderHtml("prompt-known", engine, known)).into_response(); 122 + } else { 123 + LOGIN_HTML.into_response() 124 + }; 125 + (jar, Html(m)).into_response() 56 126 } 57 127 58 - async fn prompt(jar: SignedCookieJar) -> impl IntoResponse { 59 - let m = if let Some(did) = jar.get("did") { 60 - format!("oh i know you: {did}") 61 - } else { 62 - LOGIN_HTML.into() 128 + #[derive(Debug, Deserialize)] 129 + #[serde(rename_all = "kebab-case")] 130 + struct UserInfoParams { 131 + fetch_key: String, 132 + } 133 + async fn user_info( 134 + State(AppState { resolving, .. }): State<AppState>, 135 + Query(params): Query<UserInfoParams>, 136 + ) -> impl IntoResponse { 137 + // let fetch_key: [char; 16] = params.fetch_key.chars().collect::<Vec<_>>().try_into().unwrap(); 138 + let Some(handle) = resolving.take(&params.fetch_key) else { 139 + return "oops, task does not exist or is gone".into_response(); 63 140 }; 64 - (jar, Html(m)) 141 + let s = handle.await.unwrap(); 142 + format!("sup: {s}").into_response() 65 143 } 66 144 67 145 #[derive(Debug, Deserialize)] ··· 74 152 jar: SignedCookieJar, 75 153 ) -> (SignedCookieJar, Redirect) { 76 154 // if any existing session was active, clear it first 77 - let jar = jar.remove("did"); 155 + let jar = jar.remove(DID_COOKIE_KEY); 78 156 79 157 let auth_url = authorize(&state.client, &params.handle).await; 80 158 (jar, Redirect::to(&auth_url)) ··· 89 167 panic!("failed to do client callback"); 90 168 }; 91 169 let did = oauth_session.did().await.expect("a did to be present"); 92 - let cookie = Cookie::build(("did", did.to_string())) 170 + let cookie = Cookie::build((DID_COOKIE_KEY, did.to_string())) 93 171 .http_only(true) 94 172 .secure(true) 95 173 .same_site(SameSite::None)
+96
who-am-i/templates/prompt-known.hbs
··· 1 + <!doctype html> 2 + 3 + <style> 4 + body { 5 + color: #434; 6 + font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif; 7 + margin: 0; 8 + min-height: 100vh; 9 + padding: 0; 10 + } 11 + .wrap { 12 + border: 2px solid #221828; 13 + border-radius: 0.5rem; 14 + box-sizing: border-box; 15 + overflow: hidden; 16 + display: flex; 17 + flex-direction: column; 18 + height: 100vh; 19 + } 20 + header { 21 + background: #221828; 22 + display: flex; 23 + justify-content: space-between; 24 + padding: 0 0.25rem; 25 + color: #c9b; 26 + display: flex; 27 + gap: 0.5rem; 28 + align-items: baseline; 29 + } 30 + header > * { 31 + flex-basis: 33%; 32 + } 33 + header > .title { 34 + text-align: center; 35 + } 36 + header > a.micro { 37 + text-decoration: none; 38 + font-size: 0.8rem; 39 + text-align: right; 40 + opacity: 0.5; 41 + } 42 + header > a.micro:hover { 43 + opacity: 1; 44 + } 45 + main { 46 + padding: 0.25rem 0.5rem; 47 + background: #ccc; 48 + flex-grow: 1; 49 + } 50 + p { 51 + margin: 0.5rem 0; 52 + } 53 + </style> 54 + 55 + <div class="wrap"> 56 + <header> 57 + <div class="empty"></div> 58 + <code class="title" style="font-family: monospace;" 59 + >who-am-i</code> 60 + <a href="https://microcosm.blue" target="_blank" class="micro" 61 + ><span style="color: #f396a9">m</span 62 + ><span style="color: #f49c5c">i</span 63 + ><span style="color: #c7b04c">c</span 64 + ><span style="color: #92be4c">r</span 65 + ><span style="color: #4ec688">o</span 66 + ><span style="color: #51c2b6">c</span 67 + ><span style="color: #54bed7">o</span 68 + ><span style="color: #8fb1f1">s</span 69 + ><span style="color: #ce9df1">m</span 70 + ></a> 71 + </header> 72 + 73 + <main> 74 + <p>Share your identity with {{ parent_host }}?</p> 75 + <div id="user-info">Loading&hellip;</div> 76 + </main> 77 + </div> 78 + 79 + 80 + <script> 81 + const infoEl = document.getElementById('user-info'); 82 + var DID = {{{json did}}}; 83 + let user_info = new URL('/user-info', window.location); 84 + user_info.searchParams.set('fetch-key', {{{json fetch_key}}}); 85 + fetch(user_info).then( 86 + info => { 87 + infoEl.textContent = 'yay'; 88 + console.log(info); 89 + }, 90 + err => { 91 + infoEl.textContent = 'ohno'; 92 + console.error(err); 93 + }, 94 + ); 95 + 96 + </script>