Files
app/extension/popup.mjs
T

146 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});