Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
at main 5.5 kB view raw
1{{#*inline "main"}} 2<p> 3 Connect in the ATmosphere 4</p> 5 6<p id="error-message" class="hidden"></p> 7 8<p id="prompt" class="detail"> 9 <span class="parent-host">{{ parent_host }}</span> wants to confirm your handle 10</p> 11 12<div id="loader" {{#unless did}}class="hidden"{{/unless}}> 13 <span class="spinner"></span> 14</div> 15 16<div id="user-info"> 17 <form id="form-action" action="/auth" method="GET" target="_blank" class="action {{#if did}}hidden{{/if}}"> 18 <label> 19 @<input id="handle-input" class="handle" name="handle" placeholder="example.bsky.social" /> 20 </label> 21 <button id="connect" type="submit">connect</button> 22 </form> 23 24 <div id="handle-action" class="action"> 25 <span id="handle-view" class="handle"></span> 26 <button id="allow">Allow</button> 27 </div> 28</div> 29 30<div id="need-storage" class="hidden"> 31 <p class="problem">Sorry, your browser is blocking access.</p> 32 <p> 33 Try <a href="/" target="_blank">connecting directly</a> first (but no promises). 34 Clicking <button id="desperation">this button</button> might also help. 35 </p> 36</div> 37 38 39 40<script> 41const errorEl = document.getElementById('error-message'); 42const promptEl = document.getElementById('prompt'); 43const loaderEl = document.getElementById('loader'); 44const infoEl = document.getElementById('user-info'); 45const handleInputEl = document.getElementById('handle-input'); 46const handleViewEl = document.getElementById('handle-view'); 47const formEl = document.getElementById('form-action'); // for anon 48const allowEl = document.getElementById('handle-action'); // for known-did 49const connectEl = document.getElementById('connect'); // for anon 50const needStorageEl = document.getElementById('need-storage'); // for safari/frame isolation 51const desperationEl = document.getElementById('desperation'); 52 53function err(e, msg) { 54 loaderEl.classList.add('hidden'); 55 errorEl.classList.remove('hidden'); 56 errorEl.textContent = msg || e; 57 throw new Error(e); 58} 59 60// already-known user 61({{{json did}}}) && (async () => { 62 const handle = await lookUp({{{json fetch_key}}}); 63 loaderEl.classList.add('hidden'); 64 handleViewEl.textContent = `@${handle}`; 65 allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}})); 66})(); 67 68// anon user 69formEl.onsubmit = e => { 70 e.preventDefault(); 71 loaderEl.classList.remove('hidden'); 72 // TODO: include expected referer! (..this system is probably bad) 73 // maybe a random localstorage key that we specifically listen for? 74 const url = new URL('/auth', window.location); 75 url.searchParams.set('handle', handleInputEl.value); 76 window.open(url, '_blank'); 77}; 78 79// check if we may be partitioned, preventing access after auth completion 80// this should only happen if on a browser that implements storage access api 81if ('hasStorageAccess' in document) { 82 document.hasStorageAccess().then((hasAccess) => { 83 if (!hasAccess) { 84 promptEl.classList.add('hidden'); 85 infoEl.classList.add('hidden'); 86 needStorageEl.classList.remove('hidden'); 87 desperation.addEventListener('click', () => { 88 document.requestStorageAccess({ 89 cookies: true, 90 localStorage: true, 91 }).then( 92 () => { 93 desperation.textContent = "(maybe helped?)"; 94 setTimeout(() => location.reload(), 350); 95 }, 96 () => desperation.textContent = "(doubtful)", 97 ); 98 }) 99 } 100 }); 101} 102 103window.addEventListener('storage', async e => { 104 // here's a fun minor vuln: we can't tell which flow triggers the storage event. 105 // so if you have two flows going, it grants for both (or the first responder?) if you grant for either. 106 // (letting this slide while parent pages are allowlisted to microcosm only) 107 108 if (e.key !== 'who-am-i') return; 109 if (e.newValue === null) return; 110 111 const details = e.newValue; 112 if (!details) { 113 console.error("hmm, heard from localstorage but did not get DID", details, e); 114 err('sorry, something went wrong getting your details'); 115 } 116 117 let parsed; 118 try { 119 parsed = JSON.parse(details); 120 } catch (e) { 121 err(e, "something went wrong getting the details back"); 122 } 123 124 const fail = (e, msg) => { 125 loaderEl.classList.add('hidden'); 126 formEl.classList.remove('hidden'); 127 handleInputEl.focus(); 128 handleInputEl.select(); 129 err(e, msg); 130 } 131 132 if (parsed.result === "fail") { 133 fail(`uh oh: ${parsed.reason}`); 134 } 135 136 if (parsed.result === "deny") { 137 fail(parsed.reason); 138 } 139 140 infoEl.classList.add('hidden'); 141 142 const handle = await lookUp(parsed.fetch_key); 143 144 shareAllow(handle, parsed.token); 145}); 146 147async function lookUp(fetch_key) { 148 let info; 149 try { 150 const resp = await fetch('/user-info', { 151 method: 'POST', 152 headers: { 'Content-Type': 'application/json' }, 153 body: JSON.stringify({ fetch_key }), 154 }); 155 if (!resp.ok) throw resp; 156 info = await resp.json(); 157 } catch (e) { 158 err(e, `failed to resolve handle from DID with ${fetch_key}`); 159 } 160 return info.handle; 161} 162 163const parentTarget = {{{json parent_target}}} ?? {{{json parent_origin}}}; 164 165const shareAllow = (handle, token) => { 166 try { 167 top.postMessage( 168 { action: "allow", handle, token }, 169 parentTarget, 170 ); 171 } catch (e) { 172 err(e, 'Identity verified but failed to connect with app'); 173 }; 174 promptEl.textContent = '✔️ shared'; 175} 176 177const shareDeny = reason => { 178 top.postMessage( 179 { action: "deny", reason }, 180 parentTarget, 181 ); 182} 183</script> 184 185{{/inline}} 186 187{{#> base-framed}}{{/base-framed}}