This repository has been archived on 2026-10-14. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
extension/human-api.mjs
T

61 lines
3.9 KiB
JavaScript
Raw Normal View History

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; }
}