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