// favela /login controller — drives the DID web sign-in DOM against the live /api/auth/* routes.
// Loaded as an ES module by the SSR /login page. All crypto is client-side (the sub-key secret never
// leaves the browser); this file is only glue between the AtprotoIdentity client and the page elements.
import { AtprotoIdentity } from './browser-identity.js'
const id = new AtprotoIdentity()
let autoSignInTried = false // pgv3-parity: attempt a silent re-sign-in once per page load
const $ = (sel) => document.querySelector(sel)
const show = (el, on) => { if (el) el.style.display = on ? '' : 'none' }
function status (msg, kind) {
const el = $('#status')
if (!el) return
el.textContent = msg || ''
el.className = kind || ''
}
async function render () {
const subKey = id.subKeyHex()
const skEl = $('#subkey')
if (skEl) skEl.textContent = subKey
let session = await id.whoami()
const pairing = id.getPairing()
// pgv3-parity: hold a delegation but no live session (e.g. favela-web restarted + cleared its
// in-memory sessions)? Silently re-sign-in with the stored delegation — never fall back to
// paste-invite. Try once per load; on failure (expired/revoked delegation) fall through to the
// manual sign-in / pair boxes below.
if (!session && id.isPaired() && !autoSignInTried) {
autoSignInTried = true
try { await id.signIn(); session = await id.whoami() } catch {}
}
// Signed in → show the session + a link home.
show($('#session-box'), !!session)
if (session) {
const who = $('#session-who')
if (who) who.textContent = session.handle ? `@${session.handle}` : session.did
}
// Paired (delegation stored) but not signed in → offer sign-in.
show($('#signin-box'), !session && id.isPaired())
if (!session && pairing) {
const p = $('#paired-did')
if (p) p.textContent = pairing.handle ? `@${pairing.handle} (${pairing.did})` : pairing.did
}
// Not paired → show the pairing instructions + form.
show($('#pair-box'), !session && !id.isPaired())
}
// Pair: paste the ONE hash-URL invite the hpp minted (favela-pair). favela-web joins the tunnel, has the
// hpp delegate this browser's sub-key, and signs us in — no manual delegation shuttle.
async function onPair (e) {
e.preventDefault()
const field = ($('#pair-invite') && $('#pair-invite').value.trim()) || ''
const invite = field || (location.hash || '').replace(/^.*[#?&]pair=/, '')
try {
status('Pairing over the tunnel (this can take a few seconds)…')
const s = await id.pairWithInvite(invite)
status('Signed in as ' + (s.handle ? '@' + s.handle : s.did) + '.', 'ok')
await render()
setTimeout(() => { window.location.href = '/' }, 600)
} catch (err) {
status(err.message || 'pairing failed', 'err')
await render()
}
}
async function onSignIn () {
status('Signing in…')
try {
await id.signIn()
status('Signed in.', 'ok')
setTimeout(() => { window.location.href = '/' }, 400)
} catch (err) {
status(err.message || 'sign-in failed', 'err')
// A stale/expired delegation can't be recovered by retrying — surface the unpair option.
show($('#pair-box'), true)
}
}
async function onLogout () {
await id.logout()
status('Signed out.')
await render()
}
async function onUnpair () {
id.unpair()
status('Unpaired this browser.')
await render()
}
async function onCopy () {
try { await navigator.clipboard.writeText(id.subKeyHex()); status('Sub-key copied.', 'ok') } catch { status('Copy failed — select it manually.', 'err') }
}
window.addEventListener('DOMContentLoaded', () => {
// Prefill the invite from a `…/login#pair=<invite>` link the hpp printed.
const fromHash = (location.hash || '').replace(/^.*[#?&]pair=/, '')
const inviteField = $('#pair-invite'); if (inviteField && fromHash && fromHash !== location.hash) inviteField.value = fromHash
const pairForm = $('#pair-form'); if (pairForm) pairForm.addEventListener('submit', onPair)
const signinBtn = $('#signin-btn'); if (signinBtn) signinBtn.addEventListener('click', onSignIn)
const logoutBtn = $('#logout-btn'); if (logoutBtn) logoutBtn.addEventListener('click', onLogout)
const unpairBtn = $('#unpair-btn'); if (unpairBtn) unpairBtn.addEventListener('click', onUnpair)
const copyBtn = $('#copy-subkey'); if (copyBtn) copyBtn.addEventListener('click', onCopy)
render().catch((e) => status(e.message, 'err'))
})