Consolidate Persimmons source
This commit is contained in:
1 parent
40f4edd363
commit
f8f7d286f8
1 file changed
+49
@@ -0,0 +1,49 @@
|
||||
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.');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user