Scopes apply across the source account’s accessible resources in that API category. They are not limited to this vault or one repository. No automatic expiry.
`;
form.onsubmit = async event => {
event.preventDefault(); juiceBusy = true; form.querySelector('button').disabled = true;
const result = await extensionMessage('persimmons.juice.v1', currentOwner, {action: 'create', source_id: item.id,
target: {id: vault.id, name: vault.name}, policy: {scopes: [form.elements.scope.value]}});
juiceBusy = false;
if (result?.ok) {
openVaults.add(vault.owner + '/' + vault.name); dialog.close(); await refresh();
} else {
form.querySelector('[role=status]').textContent = result?.message || 'ANDON: Outcome unknown. Close and inspect Juice before retrying.';
form.querySelector('button').disabled = false;
}
};
body.append(form);
}
dialog.showModal();
}
function openNewVault() {
const dialog = ensureDialog();
const body = $('siteModalBody');
body.replaceChildren(closeButton(dialog));
const title = document.createElement('h2');
title.textContent = 'New vault';
const form = document.createElement('form');
form.innerHTML = `
Creates an empty private vault owned by you. Spaces become hyphens.
`;
form.onsubmit = async event => {
event.preventDefault();
form.querySelector('button').disabled = true;
try {
const response = await fetch('/api/vaults', {method: 'POST', credentials: 'same-origin',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'Persimmons'},
body: JSON.stringify({name: form.querySelector('#vaultName').value})});
const result = await response.json();
if (!response.ok) {
form.querySelector('#vaultStatus').textContent = result.error || 'Vault creation unavailable. Retry shortly.';
form.querySelector('button').disabled = false;
return;
}
dialog.close();
await refresh();
} catch {
form.querySelector('#vaultStatus').textContent = 'Vault creation unavailable. Refresh the page before retrying.';
form.querySelector('button').disabled = false;
}
};
body.append(title, form);
dialog.showModal();
}
function ensureDialog() {
let dialog = $('siteModal');
if (dialog) return dialog;
dialog = document.createElement('dialog');
dialog.id = 'siteModal';
dialog.style.cssText = 'border:0;border-radius:16px;padding:0;width:min(460px,calc(100vw - 32px));box-shadow:0 24px 80px rgba(36,26,20,.24)';
dialog.innerHTML = '';
document.body.append(dialog);
dialog.addEventListener('close', () => refresh().catch(failed));
return dialog;
}
function closeButton(dialog) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = '×';
button.setAttribute('aria-label', 'Close');
button.style.cssText = 'float:right;border:0;background:none;font-size:22px';
button.onclick = () => dialog.close();
return button;
}
function openAuth(mode = 'login') {
const dialog = ensureDialog();
const body = $('siteModalBody');
body.replaceChildren(closeButton(dialog));
const title = document.createElement('h2');
title.textContent = mode === 'register' ? 'Create account' : 'Sign in';
const form = document.createElement('form');
form.autocomplete = 'on';
form.innerHTML = `
`;
form.onsubmit = async event => {
event.preventDefault();
const password = form.querySelector('#modalPassword');
const payload = {name: form.querySelector('#modalName').value, password: password.value};
password.value = '';
form.querySelector('button').disabled = true;
try {
const response = await fetch('/api/auth/' + mode, {
method: 'POST', credentials: 'same-origin',
headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
});
payload.password = '';
if (!response.ok) throw new Error();
dialog.close();
await refresh();
} catch {
payload.password = '';
form.querySelector('#modalStatus').textContent = 'Sign-in unavailable. Check details and retry.';
form.querySelector('button').disabled = false;
}
};
body.append(title, form);
dialog.showModal();
}
function openInstall(update = false) {
const dialog = ensureDialog();
const body = $('siteModalBody');
body.replaceChildren(closeButton(dialog));
const title = document.createElement('h2');
title.textContent = update ? 'Update Persimmons' : 'Get Persimmons';
const text = document.createElement('p');
text.textContent = update ? 'Install the current build into your unpacked extension folder, then reload it in Chromium.' : 'Download the current build, unzip it, then use Chromium → Extensions → Developer mode → Load unpacked.';
const link = document.createElement('a');
link.href = '/assets/extension.zip';
link.className = 'buttonLink primary';
link.textContent = update ? 'Download current build' : 'Download extension';
body.append(title, text, link);
dialog.showModal();
}
function openAddCredential() {
const dialog = ensureDialog();
const body = $('siteModalBody');
body.replaceChildren(closeButton(dialog));
const title = document.createElement('h2');
title.textContent = 'Add a credential';
const text = document.createElement('p');
text.textContent = 'Open the HTTPS site where you use the credential, enter it without submitting, then open 🍊 and choose Save for this site.';
const note = document.createElement('p');
note.className = 'hint';
note.textContent = 'The page updates automatically after the extension saves it.';
body.append(title, text, note);
dialog.showModal();
}
const failed = () => {
$('userState').hidden = false;
$('userState').textContent = 'Service unavailable. Retry shortly.';
$('extensionState').hidden = false;
$('extensionState').textContent = 'Connection could not be checked.';
};
$('signOutButton').onclick = async () => {
try {
const response = await fetch('/api/logout', {method: 'POST'});
if (!response.ok) throw new Error();
await refresh();
} catch {
$('userState').hidden = false;
$('userState').textContent = 'Sign-out unavailable. Retry shortly.';
}
};
$('signInLink').onclick = event => { event.preventDefault(); openAuth('login'); };
$('createAccount').onclick = () => openAuth('register');
$('getExtension').onclick = () => openInstall(false);
$('updateExtension').onclick = () => openInstall(true);
$('addCredential').onclick = openAddCredential;
$('newVault').onclick = openNewVault;
window.addEventListener('pointerdown', event => {
if (event.target.closest?.('.connection[draggable="true"]')) dragging = true;
}, true);
window.addEventListener('pointerup', () => { dragging = false; }, true);
window.addEventListener('pageshow', () => refresh().catch(failed));
window.addEventListener('focus', () => refresh().catch(failed));
setInterval(() => { if (!document.hidden && !$('siteModal')?.open) refresh().catch(() => {}); }, 1500);
refresh().catch(failed);