import {VAULT_CAPABILITY} from './connection-state.mjs'; export class VaultUnavailableError extends Error {} const BASE = 'https://teahub.xyz/tea/site/chat/persimmons'; export async function vaultRequest(method = 'GET', owner, id, body, fetcher = fetch) { if (!['GET', 'POST', 'PUT', 'DELETE'].includes(method) || (id !== undefined && !/^[0-9a-f]{32}$/.test(id))) throw new Error('Invalid vault operation.'); try { const stored = (await chrome.storage.local.get(VAULT_CAPABILITY))[VAULT_CAPABILITY]; const capability = stored?.owner === owner && typeof stored?.capability === 'string' ? stored.capability : null; if (!capability) throw new Error(); const response = await fetcher(`${BASE}/human-vault/items${id ? '/' + id : ''}${method === 'GET' ? '/read' : ''}`, { method: method === 'GET' ? 'POST' : method, credentials: 'omit', cache: 'no-store', redirect: 'error', signal: AbortSignal.timeout(15000), headers: {Accept: 'application/json', 'X-Vault-Capability': capability, ...(owner ? {'X-Vault-Owner': owner} : {}), ...(body ? {'Content-Type': 'application/json'} : {})}, ...(body ? {body: JSON.stringify(body)} : {})}); if (!response.ok || !(response.headers.get('content-type') || '').includes('application/json')) throw new Error(); const data = await response.json(); if (!/^[a-z0-9][a-z0-9_.-]{0,99}$/.test(data.owner) || (owner && data.owner !== owner) || data.error) throw new Error(); return data; } catch { throw new VaultUnavailableError('Vault unavailable or session changed. Sign in to Persimmons and reconnect.'); } } // Serialized into the isolated main-frame world only after an explicit Fill click. // Check origin again inside the actual target document to close navigation races. export function fillAccount(expectedOrigin, account) { try { if (location.protocol !== 'https:' || location.origin !== expectedOrigin || window.top !== window) return false; const visible = input => !input.disabled && !input.readOnly && input.getClientRects().length > 0; const passwords = [...document.querySelectorAll('input[type="password"]')].filter(visible); if (passwords.length !== 1) return false; const password = passwords[0]; const form = password.form; if (!form || new URL(form.action || location.href, location.href).origin !== expectedOrigin) return false; const users = [...form.querySelectorAll('input')].filter(input => ['text', 'email'].includes(input.type) && visible(input)); if (users.length !== 1) return false; const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; setter.call(users[0], account.username); setter.call(password, account.password); for (const input of [users[0], password]) { input.dispatchEvent(new Event('input', {bubbles: true})); input.dispatchEvent(new Event('change', {bubbles: true})); } return true; } finally { account.username = ''; account.password = ''; account = null; } } // Serialized into the isolated main-frame world only after an explicit Capture click. export function captureAccount(expectedOrigin) { try { if (location.protocol !== 'https:' || location.origin !== expectedOrigin) return null; const visible = input => !input.disabled && !input.readOnly && input.getClientRects().length > 0; const passwords = [...document.querySelectorAll('input[type="password"]')].filter(input => visible(input) && input.value); if (passwords.length !== 1) return null; const password = passwords[0]; const form = password.form; if (!form || new URL(form.action || location.href, location.href).origin !== expectedOrigin) return null; const users = [...form.querySelectorAll('input')].filter(input => ['text', 'email'].includes(input.type) && visible(input) && input.value); if (users.length !== 1) return null; return {username: users[0].value, password: password.value}; } catch { return null; } }