Files
app/extension/popup.mjs
T

145 lines
5.6 KiB
JavaScript
Raw Normal View History

2026-10-04 02:46:12 +00:00
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);
});