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

more (still problematic) flow-y stuff

+13 -5
who-am-i/demo/index.html
···
<iframe src="http://127.0.0.1:9997/prompt" id="whoami" style="border: none" height="160" width="320"></iframe>
<script type="text/javascript">
-
window.onmessage = message => {
-
if (!message || !message.data || message.data.source !== 'whoami') return;
-
document.getElementById('whoami').remove();
-
document.getElementById('who').textContent = message.data.handle;
-
};
</script>
</body>
</html>
···
<iframe src="http://127.0.0.1:9997/prompt" id="whoami" style="border: none" height="160" width="320"></iframe>
<script type="text/javascript">
+
(whoami => {
+
const handleMessage = ev => {
+
if (ev.source !== whoami.contentWindow) {
+
console.log('nah');
+
return;
+
}
+
whoami.remove();
+
window.removeEventListener('message', handleMessage);
+
+
document.getElementById('who').textContent = ev.data.handle;
+
}
+
window.addEventListener('message', handleMessage);
+
})(document.getElementById('whoami'));
</script>
</body>
</html>
+26 -20
who-am-i/src/server.rs
···
use axum_template::{RenderHtml, engine::Engine};
use handlebars::{Handlebars, handlebars_helper};
-
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::sync::Arc;
use std::time::Duration;
···
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
const INDEX_HTML: &str = include_str!("../static/index.html");
-
const LOGIN_HTML: &str = include_str!("../static/login.html");
const DID_COOKIE_KEY: &str = "did";
···
.unwrap();
}
-
#[derive(Debug, Serialize)]
-
struct Known {
-
did: Value,
-
fetch_key: Value,
-
parent_host: String,
-
}
async fn prompt(
State(AppState {
engine,
···
let Some(parent_host) = url.host_str() else {
return "could nto get host from url".into_response();
};
-
let m = if let Some(did) = jar.get(DID_COOKIE_KEY) {
let did = did.value_trimmed().to_string();
let task_shutdown = shutdown.child_token();
···
let json_did = Value::String(did);
let json_fetch_key = Value::String(fetch_key);
-
let known = Known {
-
did: json_did,
-
fetch_key: json_fetch_key,
-
parent_host: parent_host.to_string(),
-
};
-
return (jar, RenderHtml("prompt-known", engine, known)).into_response();
} else {
-
LOGIN_HTML.into_response()
-
};
-
(jar, Html(m)).into_response()
}
#[derive(Debug, Deserialize)]
···
State(state): State<AppState>,
Query(params): Query<CallbackParams>,
jar: SignedCookieJar,
-
) -> (SignedCookieJar, Html<String>) {
let Ok((oauth_session, _)) = state.client.callback(params).await else {
panic!("failed to do client callback");
};
···
.same_site(SameSite::None)
.max_age(std::time::Duration::from_secs(86_400).try_into().unwrap());
let jar = jar.add(cookie);
-
(jar, Html(format!("sup: {did:?}")))
}
···
use axum_template::{RenderHtml, engine::Engine};
use handlebars::{Handlebars, handlebars_helper};
+
use serde::Deserialize;
use serde_json::{Value, json};
use std::sync::Arc;
use std::time::Duration;
···
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
const INDEX_HTML: &str = include_str!("../static/index.html");
const DID_COOKIE_KEY: &str = "did";
···
.unwrap();
}
async fn prompt(
State(AppState {
engine,
···
let Some(parent_host) = url.host_str() else {
return "could nto get host from url".into_response();
};
+
if let Some(did) = jar.get(DID_COOKIE_KEY) {
let did = did.value_trimmed().to_string();
let task_shutdown = shutdown.child_token();
···
let json_did = Value::String(did);
let json_fetch_key = Value::String(fetch_key);
+
RenderHtml(
+
"prompt-known",
+
engine,
+
json!({
+
"did": json_did,
+
"fetch_key": json_fetch_key,
+
"parent_host": parent_host,
+
}),
+
)
+
.into_response()
} else {
+
RenderHtml(
+
"prompt-anon",
+
engine,
+
json!({
+
"parent_host": parent_host,
+
}),
+
)
+
.into_response()
+
}
}
#[derive(Debug, Deserialize)]
···
State(state): State<AppState>,
Query(params): Query<CallbackParams>,
jar: SignedCookieJar,
+
) -> (SignedCookieJar, impl IntoResponse) {
let Ok((oauth_session, _)) = state.client.callback(params).await else {
panic!("failed to do client callback");
};
···
.same_site(SameSite::None)
.max_age(std::time::Duration::from_secs(86_400).try_into().unwrap());
let jar = jar.add(cookie);
+
(
+
jar,
+
RenderHtml("authorized", state.engine, json!({ "did": did })),
+
)
}
-17
who-am-i/static/login.html
···
-
<!doctype html>
-
<html lang="en">
-
<head>
-
<meta charset="utf-8" />
-
<title>Who-am-i</title>
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
-
<meta name="description" content="Log in" />
-
</head>
-
<body>
-
<form action="/auth" method="GET">
-
<label>
-
@<input name="handle" placeholder="example.bsky.social" />
-
</label>
-
<button type="submit">log in</button>
-
</form>
-
</body>
-
</html>
···
who-am-i/static/prompt-anon.html

This is a binary file and will not be displayed.

+9
who-am-i/templates/authorized.hbs
···
···
+
<!doctype html>
+
+
<p>oh sick. hey {{ did }}. you can close this window now.</p>
+
+
<script>
+
// TODO: tie this back to its source...........
+
localStorage.setItem("did", {{{json did}}});
+
window.close();
+
</script>
+34
who-am-i/templates/prompt-anon.hbs
···
···
+
{{#*inline "main"}}
+
<p>
+
Share your identity with
+
<span class="parent-host">{{ parent_host }}</span>?
+
</p>
+
+
<div id="user-info">
+
<form id="action" action="/auth" method="GET" target="_blank">
+
<label>
+
@<input id="handle" name="handle" placeholder="example.bsky.social" />
+
</label>
+
<button id="allow" type="submit">connect</button>
+
</form>
+
</div>
+
+
<script>
+
const formEl = document.getElementById('action');
+
const handleEl = document.getElementById('handle');
+
formEl.onsubmit = e => {
+
e.preventDefault();
+
// TODO: include expected referer! (..this system is probably bad)
+
var url = new URL('/auth', window.location);
+
url.searchParams.set('handle', handleEl.value);
+
var flow = window.open(url, '_blank');
+
window.f = flow;
+
+
window.addEventListener('storage', e => {
+
location.reload();
+
});
+
}
+
</script>
+
{{/inline}}
+
+
{{#> prompt-base}}{{/prompt-base}}
+165
who-am-i/templates/prompt-base.hbs
···
···
+
<!doctype html>
+
+
<style>
+
body {
+
color: #434;
+
font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
+
margin: 0;
+
min-height: 100vh;
+
padding: 0;
+
}
+
.wrap {
+
border: 2px solid #221828;
+
border-radius: 0.5rem;
+
box-sizing: border-box;
+
overflow: hidden;
+
display: flex;
+
flex-direction: column;
+
height: 100vh;
+
}
+
header {
+
background: #221828;
+
display: flex;
+
justify-content: space-between;
+
padding: 0 0.25rem;
+
color: #c9b;
+
display: flex;
+
gap: 0.5rem;
+
align-items: baseline;
+
}
+
header > * {
+
flex-basis: 33%;
+
}
+
header > .empty {
+
font-size: 0.8rem;
+
opacity: 0.5;
+
}
+
header > .title {
+
text-align: center;
+
}
+
header > a.micro {
+
text-decoration: none;
+
font-size: 0.8rem;
+
text-align: right;
+
opacity: 0.5;
+
}
+
header > a.micro:hover {
+
opacity: 1;
+
}
+
main {
+
background: #ccc;
+
display: flex;
+
flex-direction: column;
+
flex-grow: 1;
+
padding: 0.25rem 0.5rem;
+
}
+
p {
+
margin: 1rem 0 0;
+
text-align: center;
+
}
+
.parent-host {
+
font-weight: bold;
+
color: #48c;
+
display: inline-block;
+
padding: 0 0.125rem;
+
border-radius: 0.25rem;
+
border: 1px solid #aaa;
+
font-size: 0.8rem;
+
}
+
+
#loader {
+
display: flex;
+
flex-grow: 1;
+
justify-content: center;
+
align-items: center;
+
}
+
.spinner {
+
animation: rotation 1.618s ease-in-out infinite;
+
border-radius: 50%;
+
border: 3px dashed #434;
+
box-sizing: border-box;
+
display: inline-block;
+
height: 1.5em;
+
width: 1.5em;
+
}
+
@keyframes rotation {
+
0% { transform: rotate(0deg) }
+
100% { transform: rotate(360deg) }
+
}
+
+
#user-info {
+
flex-grow: 1;
+
display: flex;
+
flex-direction: column;
+
justify-content: center;
+
}
+
#action {
+
background: #eee;
+
display: flex;
+
justify-content: space-between;
+
padding: 0.5rem 0.25rem 0.5rem 0.5rem;
+
font-size: 0.8rem;
+
align-items: baseline;
+
border-radius: 0.5rem;
+
border: 1px solid #bbb;
+
cursor: pointer;
+
}
+
#action:hover {
+
background: #fff;
+
}
+
#allow {
+
background: transparent;
+
border: none;
+
border-left: 1px solid #bbb;
+
padding: 0 0.5rem;
+
color: #375;
+
font: inherit;
+
cursor: pointer;
+
}
+
#action:hover #allow {
+
color: #285;
+
}
+
+
#or {
+
font-size: 0.8rem;
+
text-align: center;
+
}
+
#or p {
+
margin: 0 0 1rem;
+
}
+
+
input#handle {
+
border: none;
+
border-bottom: 1px dashed #aaa;
+
background: transparent;
+
}
+
+
.hidden {
+
display: none !important;
+
}
+
+
</style>
+
+
<div class="wrap">
+
<header>
+
<div class="empty">🔒</div>
+
<code class="title" style="font-family: monospace;"
+
>who-am-i</code>
+
<a href="https://microcosm.blue" target="_blank" class="micro"
+
><span style="color: #f396a9">m</span
+
><span style="color: #f49c5c">i</span
+
><span style="color: #c7b04c">c</span
+
><span style="color: #92be4c">r</span
+
><span style="color: #4ec688">o</span
+
><span style="color: #51c2b6">c</span
+
><span style="color: #54bed7">o</span
+
><span style="color: #8fb1f1">s</span
+
><span style="color: #ce9df1">m</span
+
></a>
+
</header>
+
+
<main>
+
{{> main}}
+
</main>
+
</div>
+
+19 -174
who-am-i/templates/prompt-known.hbs
···
-
<!doctype html>
-
-
<style>
-
body {
-
color: #434;
-
font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
-
margin: 0;
-
min-height: 100vh;
-
padding: 0;
-
}
-
.wrap {
-
border: 2px solid #221828;
-
border-radius: 0.5rem;
-
box-sizing: border-box;
-
overflow: hidden;
-
display: flex;
-
flex-direction: column;
-
height: 100vh;
-
}
-
header {
-
background: #221828;
-
display: flex;
-
justify-content: space-between;
-
padding: 0 0.25rem;
-
color: #c9b;
-
display: flex;
-
gap: 0.5rem;
-
align-items: baseline;
-
}
-
header > * {
-
flex-basis: 33%;
-
}
-
header > .empty {
-
font-size: 0.8rem;
-
opacity: 0.5;
-
}
-
header > .title {
-
text-align: center;
-
}
-
header > a.micro {
-
text-decoration: none;
-
font-size: 0.8rem;
-
text-align: right;
-
opacity: 0.5;
-
}
-
header > a.micro:hover {
-
opacity: 1;
-
}
-
main {
-
background: #ccc;
-
display: flex;
-
flex-direction: column;
-
flex-grow: 1;
-
padding: 0.25rem 0.5rem;
-
}
-
p {
-
margin: 1rem 0 0;
-
text-align: center;
-
}
-
.parent-host {
-
font-weight: bold;
-
color: #48c;
-
display: inline-block;
-
padding: 0 0.125rem;
-
border-radius: 0.25rem;
-
border: 1px solid #aaa;
-
font-size: 0.8rem;
-
}
-
-
#loader {
-
display: flex;
-
flex-grow: 1;
-
justify-content: center;
-
align-items: center;
-
}
-
.spinner {
-
animation: rotation 1.618s ease-in-out infinite;
-
border-radius: 50%;
-
border: 3px dashed #434;
-
box-sizing: border-box;
-
display: inline-block;
-
height: 1.5em;
-
width: 1.5em;
-
}
-
@keyframes rotation {
-
0% { transform: rotate(0deg) }
-
100% { transform: rotate(360deg) }
-
}
-
-
#user-info {
-
flex-grow: 1;
-
display: flex;
-
flex-direction: column;
-
justify-content: center;
-
}
-
#action {
-
background: #eee;
-
display: flex;
-
justify-content: space-between;
-
padding: 0.5rem 0.25rem 0.5rem 0.5rem;
-
font-size: 0.8rem;
-
align-items: baseline;
-
border-radius: 0.5rem;
-
border: 1px solid #bbb;
-
cursor: pointer;
-
}
-
#action:hover {
-
background: #fff;
-
}
-
#allow {
-
background: transparent;
-
border: none;
-
border-left: 1px solid #bbb;
-
padding: 0 0.5rem;
-
color: #375;
-
font: inherit;
-
cursor: pointer;
-
}
-
#action:hover #allow {
-
color: #285;
-
}
-
-
#or {
-
font-size: 0.8rem;
-
text-align: center;
-
}
-
#or p {
-
margin: 0 0 1rem;
-
}
-
-
.hidden {
-
display: none !important;
-
}
-
-
</style>
-
-
<div class="wrap">
-
<header>
-
<div class="empty">🔒</div>
-
<code class="title" style="font-family: monospace;"
-
>who-am-i</code>
-
<a href="https://microcosm.blue" target="_blank" class="micro"
-
><span style="color: #f396a9">m</span
-
><span style="color: #f49c5c">i</span
-
><span style="color: #c7b04c">c</span
-
><span style="color: #92be4c">r</span
-
><span style="color: #4ec688">o</span
-
><span style="color: #51c2b6">c</span
-
><span style="color: #54bed7">o</span
-
><span style="color: #8fb1f1">s</span
-
><span style="color: #ce9df1">m</span
-
></a>
-
</header>
-
-
<main>
-
<p>
-
Share your identity with
-
<span class="parent-host">{{ parent_host }}</span>?
-
</p>
-
<div id="loader">
-
<span class="spinner"></span>
-
</div>
-
<div id="user-info" class="hidden">
-
<div id="action">
-
<span id="handle"></span>
-
<button id="allow">Allow</button>
-
</div>
-
</div>
-
<div id="or">
-
<p>or, <a id="switch" href="#">use another account</a></p>
-
</div>
-
</main>
</div>
-
<script>
var loaderEl = document.getElementById('loader');
···
function share(handle) {
top.postMessage({ source: 'whoami', handle }, '*'); // TODO: pass the referrer back from server
}
-
</script>
···
+
{{#*inline "main"}}
+
<p>
+
Share your identity with
+
<span class="parent-host">{{ parent_host }}</span>?
+
</p>
+
<div id="loader">
+
<span class="spinner"></span>
+
</div>
+
<div id="user-info" class="hidden">
+
<div id="action">
+
<span id="handle"></span>
+
<button id="allow">Allow</button>
+
</div>
+
</div>
+
<div id="or">
+
<p>or, <a id="switch" href="#">use another account</a></p>
</div>
<script>
var loaderEl = document.getElementById('loader');
···
function share(handle) {
top.postMessage({ source: 'whoami', handle }, '*'); // TODO: pass the referrer back from server
}
</script>
+
{{/inline}}
+
+
{{#> prompt-base}}{{/prompt-base}}