import {pageTarget, metadata, publicSource, request} from './core.mjs'; const $ = id => document.getElementById(id); let target; let connected = false; let busy = false; const allowed = pattern => chrome.permissions.contains({origins: [pattern]}); const scopes = () => [...document.querySelectorAll('input[name=scope]:checked')].map(input => input.value); const settings = () => metadata({origin: target.origin, name: $('name').value, scopes: scopes(), minutes: Number($('minutes').value)}); function controls() { $('connect').disabled = busy; $('save').disabled = busy || !target || !connected; $('squeeze').disabled = busy || !connected || !$('sources').value; for (const button of $('juices').querySelectorAll('button')) button.disabled = busy; } async function run(work) { if (busy) return; busy = true; controls(); $('status').textContent = ''; try { await work(); } catch { $('status').textContent = 'Operation failed safely. Refresh before retrying.'; } finally { busy = false; controls(); } } async function api(path, body) { try { return await request(path, body); } catch (error) { $('status').textContent = error.message; return null; } } async function listJuice() { $('juices').replaceChildren(); const data = await api('juice/list'); if (!data || !Array.isArray(data.juice)) { $('juice-status').textContent = 'Juice unavailable. Saved sources remain in My vault.'; return; } $('juice-status').textContent = 'Juice · My vault'; for (const item of data.juice.slice(0, 100)) { if (item.vault_kind !== 'user' || typeof item.id !== 'string' || !/^[A-Za-z0-9_-]{1,100}$/.test(item.id)) continue; const id = item.id; const row = document.createElement('li'); if (!['active', 'revoked'].includes(item.status)) continue; const status = item.status; row.append(document.createTextNode(`Juice ${id} · ${status} `)); if (status === 'revoked') { $('juices').append(row); continue; } const button = document.createElement('button'); button.textContent = 'Revoke'; button.onclick = () => run(async () => { const result = await api('juice/revoke', {id, vault_kind: 'user'}); if (result) { await listJuice(); $('status').textContent = 'Juice revoke request accepted. Source credentials remain in My vault.'; } }); row.append(button); $('juices').append(row); } } async function refresh() { connected = false; $('sources').replaceChildren(new Option('Choose a saved source', '')); $('juices').replaceChildren(); if (!await allowed('https://teahub.xyz/*')) { $('vault-status').textContent = 'Connect Teahub to list My vault.'; return; } const data = await api('list'); if (!data || !Array.isArray(data.connections)) { $('vault-status').textContent = 'My vault unavailable. Sign in to Teahub and refresh.'; return; } connected = true; for (const item of data.connections.slice(0, 200)) { if (item.vault_kind !== 'user') continue; try { const source = publicSource(item); $('sources').add(new Option(source.name, source.id)); } catch { /* Ignore malformed public metadata. */ } } $('vault-status').textContent = 'Connected · My vault'; await listJuice(); } $('connect').onclick = () => { // Request directly in the gesture, before any async work. const grant = chrome.permissions.request({origins: ['https://teahub.xyz/*']}); run(async () => { if (await grant) await refresh(); else $('status').textContent = 'Teahub permission was not granted.'; }); }; $('sources').onchange = controls; $('save-form').onsubmit = event => { event.preventDefault(); run(async () => { let credential = ''; try { const meta = settings(); if (!connected) return; credential = $('token').value; $('token').value = ''; if (!credential || new TextEncoder().encode(credential).length > 16384) { $('status').textContent = 'Provide a credential of at most 16 KiB.'; return; } const result = await api('create', {...meta, credential}); credential = ''; if (!result) return; const source = publicSource(result); await refresh(); $('sources').value = source.id; $('status').textContent = 'Saved to My vault. Only public source metadata is retained.'; } finally { credential = ''; $('token').value = ''; } }); }; $('squeeze').onclick = () => run(async () => { const source_id = $('sources').value; const minutes = Number($('minutes').value); const subject = $('subject').value.trim(); if (!source_id || !subject || subject.length > 200 || !scopes().length || !Number.isInteger(minutes) || minutes < 1 || minutes > 60) { $('status').textContent = 'Choose a source, enter a public subject, select at least one scope and use 1–60 minutes.'; return; } const result = await api('juice/create', {source_id, subject, vault_kind: 'user', scopes: scopes(), expires_at: new Date(Date.now() + minutes * 60000).toISOString()}); if (result) { await listJuice(); $('status').textContent = 'Juice policy saved. Downstream scope and expiry enforcement is not yet available.'; } }); window.addEventListener('pagehide', () => { $('token').value = ''; }); await run(async () => { try { const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); target = pageTarget(tab.url); $('origin').textContent = target.origin; } catch { $('origin').textContent = 'Use an HTTPS website tab.'; } // Juice requests begin only when its secondary disclosure is opened. }); $('juice-panel').addEventListener('toggle', () => { if ($('juice-panel').open && !$('juice-panel').hidden) run(refresh); });