/**
* pgv3 page bootstrap.
*
* Runs on every SSR page load. Hydrates the "you" badge and admin nav
* link from the current envelope-signed identity.
*
* **Phase 2a migration:** identity is established by calling whoami via
* a per-call signed envelope (signed-rpc.js). The browser keypair
* survives in localStorage, so the user never sees a "logged out"
* transition between sessions. Previous implementation used a
* challenge + verify + session cookie dance; that path is retained in
* browser-identity.js as a fallback for daemons without /api/rpc but is
* no longer the primary flow.
*
* Sign-in is still explicit: the anon "Sign In" button in the nav calls
* window.pgv3SignIn() on click. The only reason this exists (vs auto-
* signing on every page load) is that the FIRST sign-in also registers
* the user server-side via TOFU, and some visitors may want to read
* public content without getting their keypair auto-registered.
*
* On a fresh identity (first-time sign-in for this browser's keypair),
* the welcome modal opens asking the user to set a display name +
* optional profile picture.
*
* Exposes the result on `window.__pgv3`:
* {
* publicKeyHex: 'hex' | null
* me: { publicKey, userId, trust, ... } | null
* }
*/
import { signRpc, getPublicKeyHex, getKeypair, clearIdentity } from './signed-rpc.js'
const __prefix = (document.body && document.body.dataset.prefix) || ''
window.__pgv3 = { publicKeyHex: null, me: null, prefix: __prefix }
// ── Hydration: populate badge + admin link from envelope-signed whoami ──
async function hydrateIdentity () {
// Has the user chosen to sign in on this browser? The signal is whether
// a keypair exists in localStorage. No keypair = anonymous visitor; do
// nothing, leave the Sign In button visible.
let hasKeypair = false
try { hasKeypair = typeof localStorage !== 'undefined' && !!localStorage.getItem('pgv3-keypair') } catch {}
if (!hasKeypair) return
// Envelope path: call whoami through /api/rpc with a signed envelope.
// The server TOFU-registers the user if new, returns profile info.
let rpcResult = null
try {
rpcResult = await signRpc('whoami', {})
} catch (err) {
// /api/rpc rejected the envelope (stale keypair? deprecated daemon?
// network?). Leave sign-in UI visible.
return
}
const me = rpcResult && rpcResult.result
if (!me || !me.publicKey) return
window.__pgv3.me = me
window.__pgv3.publicKeyHex = me.publicKey
// Admin link visibility
if (me.trust === 'owner' || me.trust === 'local') {
const el = document.getElementById('admin-nav')
if (el) el.style.display = ''
}
// Hydrate the badge with display name + pfp from get-profile
if (me.userId) {
try {
const profileResp = await signRpc('get-profile', { userId: me.userId })
const user = profileResp && profileResp.result && profileResp.result.user
if (user) {
const label = (user.name && user.name !== 'Anonymous')
? user.name
: ('you: ' + me.publicKey.slice(0, 8) + '…')
const labelSlot = document.getElementById('you-label-slot')
if (labelSlot) labelSlot.textContent = label
if (user.profilePicture) {
const avatarSlot = document.getElementById('you-avatar-slot')
if (avatarSlot) {
avatarSlot.innerHTML = '<img src="' + __prefix + '/files/raw?path=' +
encodeURIComponent('/attachments/' + user.profilePicture) +
'" style="width:20px;height:20px;border-radius:50%;vertical-align:middle;margin-right:4px;object-fit:cover" alt="">'
}
}
const badge = document.getElementById('you-badge')
if (badge) badge.classList.remove('shimmer')
}
} catch {}
// Ensure shimmer is removed even if profile fetch fails
const badgeFallback = document.getElementById('you-badge')
if (badgeFallback) badgeFallback.classList.remove('shimmer')
}
}
// ── Explicit sign-in handler wired to the "Sign In" button ──
window.pgv3SignIn = async function pgv3SignIn () {
const btn = document.getElementById('signin-btn')
if (btn) {
btn.disabled = true
btn.textContent = 'Signing in…'
}
try {
// Generate/load keypair + call whoami. Server TOFU-registers on
// first call. The `identity.newUser` flag in the response tells us
// whether this is a fresh registration.
await getKeypair()
const rpcResult = await signRpc('whoami', {})
const isNewUser = rpcResult && rpcResult.identity && rpcResult.identity.newUser
if (isNewUser) {
showWelcomeModal()
} else {
try {
sessionStorage.setItem('pgv3-flash-deferred', 'success:Signed in')
} catch {}
window.location.reload()
}
} catch (err) {
// eslint-disable-next-line no-console
console.error('[pgv3] sign-in failed:', err)
if (btn) {
btn.disabled = false
btn.textContent = 'Sign In'
btn.title = (err && err.message) || 'sign-in failed'
}
}
}
// ── Welcome modal (shown after fresh sign-in) ──
function showWelcomeModal () {
const modal = document.getElementById('welcome-modal')
if (!modal) {
window.location.reload()
return
}
modal.classList.add('visible')
const form = document.getElementById('welcome-form')
const skipBtn = document.getElementById('welcome-skip')
const errEl = document.getElementById('welcome-error')
const finish = () => { window.location.reload() }
if (skipBtn) skipBtn.addEventListener('click', finish, { once: true })
if (form) {
form.addEventListener('submit', async (ev) => {
ev.preventDefault()
if (errEl) errEl.textContent = ''
const submitBtn = form.querySelector('button[type="submit"]')
if (submitBtn) {
submitBtn.disabled = true
submitBtn.textContent = 'Saving…'
}
try {
const nameInput = form.querySelector('input[name="name"]')
const fileInput = form.querySelector('input[name="picture"]')
const name = nameInput ? nameInput.value.trim() : ''
let profilePicture = null
if (fileInput && fileInput.files && fileInput.files[0]) {
const file = fileInput.files[0]
// File upload goes through the raw /api/upload route (not the
// envelope RPC). That route was hardened to require an authenticated
// session over the network (router.js: non-uds requests without
// ctx.identity are rejected 401), so we MUST send the session cookie
// that the whoami sign-in above just minted — credentials:'same-origin'
// (matches pgv3-upload.js + profile.js). 'omit' here 401s the upload
// even for a freshly signed-in owner (the initial-TOFU avatar bug).
// TODO: migrate to the envelope RPC once lib/handlers/files.js does.
const upload = await fetch(__prefix + '/api/upload', {
method: 'POST',
headers: { 'Content-Type': file.type || 'application/octet-stream' },
credentials: 'same-origin',
body: file
})
if (!upload.ok) throw new Error('upload failed: ' + upload.status)
const body = await upload.json()
profilePicture = body.hash
}
const updates = {}
if (name) updates.name = name
if (profilePicture) updates.profilePicture = profilePicture
if (updates.name || updates.profilePicture) {
await signRpc('update-profile', updates)
}
finish()
} catch (err) {
if (errEl) errEl.textContent = (err && err.message) || 'something went wrong'
if (submitBtn) {
submitBtn.disabled = false
submitBtn.textContent = 'Save'
}
}
})
}
}
// Expose a "sign out" helper for UI buttons that want it.
window.pgv3SignOut = function pgv3SignOut () {
clearIdentity()
window.__pgv3.me = null
window.__pgv3.publicKeyHex = null
window.location.reload()
}
// Run hydration on page load
hydrateIdentity()