Archived
50 lines
3.2 KiB
JavaScript
50 lines
3.2 KiB
JavaScript
export const BASE = 'https://teahub.xyz/tea/site/chat/persimmons';
|
|
export const SCOPES = ['content:read', 'content:draft-write', 'manage:users', 'billing', 'destructive'];
|
|
export const DEFAULT_SCOPES = SCOPES.slice(0, 2);
|
|
const bytes = value => new TextEncoder().encode(value).length;
|
|
export function pageTarget(url) {
|
|
const parsed = new URL(url);
|
|
if (parsed.protocol !== 'https:' || parsed.username || parsed.password) throw new Error('Use an HTTPS website tab.');
|
|
return {origin: parsed.origin};
|
|
}
|
|
|
|
export function metadata({origin, name, scopes}) {
|
|
if (!name.trim() || name.length > 100) throw new Error('Enter a public name of at most 100 characters.');
|
|
if (!Array.isArray(scopes) || scopes.some(s => !SCOPES.includes(s))) throw new Error('Invalid scopes.');
|
|
return {name: name.trim(), provider: new URL(origin).hostname, account: origin, vault_kind: 'user',
|
|
credential_type: 'api-token', scopes,
|
|
notes: 'Persimmons extension v0. Requested scopes are metadata; provider authority is unchanged.'};
|
|
}
|
|
|
|
export function publicSource(item) {
|
|
if (!item || item.vault_kind !== 'user' || typeof item.id !== 'string' ||
|
|
!/^[A-Za-z0-9_-]{1,100}$/.test(item.id) || typeof item.name !== 'string' || item.name.length > 200) {
|
|
throw new Error('Unexpected public source response. Refresh My vault.');
|
|
}
|
|
return {id: item.id, name: item.name};
|
|
}
|
|
|
|
export async function request(path, body, fetcher = fetch) {
|
|
if (!['list', 'create', 'juice/list', 'juice/create', 'juice/revoke'].includes(path)) throw new Error('Invalid operation.');
|
|
const encoded = body === undefined ? undefined : JSON.stringify(body);
|
|
if (encoded && bytes(encoded) > 24 * 1024) throw new Error('Request exceeds 24 KiB; nothing sent.');
|
|
const options = {credentials: 'include', cache: 'no-store', redirect: 'error',
|
|
signal: AbortSignal.timeout(15000), headers: {Accept: 'application/json'}};
|
|
if (encoded !== undefined) Object.assign(options, {method: 'POST', body: encoded,
|
|
headers: {...options.headers, 'Content-Type': 'text/plain;charset=UTF-8'}});
|
|
try {
|
|
const response = await fetcher(`${BASE}/${path}`, options);
|
|
if (response.status === 401 || response.status === 403) throw new Error('Sign in to teahub.xyz and retry; access may be denied.');
|
|
if (!response.ok) throw new Error(path.startsWith('juice/') ? 'Juice unavailable; no capability confirmed.' : 'My vault request failed; refresh before retrying.');
|
|
if (!(response.headers.get('content-type') || '').includes('application/json')) throw new Error('Service unavailable; expected JSON.');
|
|
const data = await response.json();
|
|
if (!data || typeof data !== 'object' || data.error || data.ok === false) throw new Error('Service did not confirm the operation.');
|
|
return data;
|
|
} catch (error) {
|
|
// Only our fixed messages leave this boundary, never server bodies or network errors.
|
|
const safe = ['Sign in to teahub.xyz and retry; access may be denied.', 'Juice unavailable; no capability confirmed.',
|
|
'My vault request failed; refresh before retrying.', 'Service unavailable; expected JSON.', 'Service did not confirm the operation.'];
|
|
throw new Error(safe.includes(error.message) ? error.message : 'Service unreachable or timed out; refresh before retrying.');
|
|
}
|
|
}
|