// 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();