In Theseus, Pithos now links the person's Theseus ID to their hosted account by itself, once, right after signing in to Silent Mode (or on the first look at an account already signed in): one consent prompt, then Pithos is listed under Settings › Theseus ID. On a Theseus without the add-on API it does nothing, so the add-on can ship before the Theseus release that carries it. The gateway serves /console on its own origin: sign in with the Theseus ID and see the account's state, sign-up, usage and Theseus ID, with a console session that cannot reach files. Three fixed files under a CSP that allows only this origin. The bundled copy in TheseusNavigator/bundled-addons/pithos is rebuilt from Pithos/ with no version bump; the release that ships it bumps the version.
111 lines
4 KiB
JavaScript
111 lines
4 KiB
JavaScript
// The read-only account console (DESIGN-theseus-id.md §11.3). Theseus signs a
|
|
// Theseus ID sign-in for project "pithos" on this origin; the gateway answers
|
|
// with a "console" session that sees /hosted/v1/me and nothing else.
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
const KEY = 'pithos-console-token';
|
|
const store = {
|
|
get() { try { return sessionStorage.getItem(KEY); } catch { return null; } },
|
|
set(v) { try { if (v) sessionStorage.setItem(KEY, v); else sessionStorage.removeItem(KEY); } catch {} },
|
|
};
|
|
|
|
async function call(method, path, { body, token } = {}) {
|
|
const res = await fetch(path, {
|
|
method,
|
|
headers: { ...(body ? { 'content-type': 'application/json' } : {}), ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw Object.assign(new Error(data.error || `HTTP ${res.status}`), { status: res.status });
|
|
return data;
|
|
}
|
|
|
|
function say(text, error = false) {
|
|
$('status').textContent = text || '';
|
|
$('status').className = error ? 'error' : '';
|
|
}
|
|
|
|
const bytes = (n) => {
|
|
if (!Number.isFinite(n)) return '—';
|
|
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
let i = 0;
|
|
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
|
|
return `${n.toFixed(i ? 1 : 0)} ${u[i]}`;
|
|
};
|
|
const GATES = { bns: 'a BNS name', deposit: 'a deposit', invite: 'an invite code' };
|
|
const STATES = { empty: 'Ready, no drives moved in yet', active: 'Running', 'moving-out': 'Moving out', released: 'Released' };
|
|
|
|
function show(me) {
|
|
const rows = [
|
|
['Account', me.id],
|
|
['Theseus ID', me.theseusId || '—'],
|
|
['State', STATES[me.state] || me.state],
|
|
['Opened', me.createdAt ? new Date(me.createdAt).toLocaleDateString() : '—'],
|
|
['Opened with', me.gate?.name ? `${GATES[me.gate.type] || me.gate.type} (${me.gate.name})` : (GATES[me.gate?.type] || '—')],
|
|
['Downloaded this month', me.downloadCapBytes ? `${bytes(me.usage?.down)} of ${bytes(me.downloadCapBytes)}` : bytes(me.usage?.down)],
|
|
['Uploaded this month', bytes(me.usage?.up)],
|
|
];
|
|
$('facts').replaceChildren(...rows.flatMap(([k, v]) => {
|
|
const dt = document.createElement('dt');
|
|
dt.textContent = k;
|
|
const dd = document.createElement('dd');
|
|
dd.textContent = v;
|
|
return [dt, dd];
|
|
}));
|
|
$('signed-out').hidden = true;
|
|
$('signed-in').hidden = false;
|
|
}
|
|
|
|
function signedOut() {
|
|
$('signed-in').hidden = true;
|
|
$('signed-out').hidden = false;
|
|
const inTheseus = !!window.theseusId;
|
|
$('signin').hidden = !inTheseus;
|
|
$('no-theseus').hidden = inTheseus;
|
|
}
|
|
|
|
async function refresh() {
|
|
const token = store.get();
|
|
if (!token) return signedOut();
|
|
try {
|
|
show(await call('GET', '/hosted/v1/me', { token }));
|
|
} catch (e) {
|
|
store.set(null);
|
|
signedOut();
|
|
if (e.status !== 401) say(e.message, true);
|
|
}
|
|
}
|
|
|
|
const THESEUS_ERRORS = {
|
|
denied: '',
|
|
locked: 'Unlock your Theseus Vault, then try again.',
|
|
'no-vault': 'Set up the Theseus Vault to get a Theseus ID.',
|
|
'origin-not-listed': 'Theseus does not list this page as part of Pithos, so it will not sign in here.',
|
|
};
|
|
|
|
$('signin').addEventListener('click', async () => {
|
|
$('signin').disabled = true;
|
|
say('Signing in…');
|
|
try {
|
|
const { projectId, nonce } = await call('GET', '/hosted/v1/theseus-id/challenge');
|
|
const signed = await window.theseusId.signIn({ projectId, nonce });
|
|
const s = await call('POST', '/hosted/v1/console/session', { body: { message: signed.message, signature: signed.signature, move: signed.move } });
|
|
if (signed.move && s.moved && window.theseusId.moved) await window.theseusId.moved({ projectId, account: signed.account }).catch(() => {});
|
|
store.set(s.token);
|
|
say('');
|
|
await refresh();
|
|
} catch (e) {
|
|
say(e.code in THESEUS_ERRORS ? THESEUS_ERRORS[e.code] : e.message, true);
|
|
} finally {
|
|
$('signin').disabled = false;
|
|
}
|
|
});
|
|
|
|
$('signout').addEventListener('click', async () => {
|
|
const token = store.get();
|
|
store.set(null);
|
|
if (token) await call('DELETE', '/hosted/v1/session', { token }).catch(() => {});
|
|
signedOut();
|
|
});
|
|
|
|
refresh();
|