Archived
62 lines
3.9 KiB
JavaScript
62 lines
3.9 KiB
JavaScript
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; }
|
|
}
|