Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185import { ApiError } from "/js/api.js";
const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
const FORBIDDEN_HEADERS = ["cookie"];const MAX_BODY_CHARS = 1_000_000;export const MAX_RESPONSE_BYTES = 100_000_000;
// Read-only AppView query endpoints plugins may call through the user's// session via the xrpcQuery host method. "public" queries return the same// data an unauthenticated caller could fetch; "private" queries reveal// viewer-scoped state and require the "privateData" action permission.const PUBLIC_XRPC_QUERIES = new Set([ "app.bsky.actor.getProfile", "app.bsky.actor.getProfiles", "app.bsky.actor.searchActors", "app.bsky.actor.searchActorsTypeahead", "app.bsky.feed.getActorFeeds", "app.bsky.feed.getAuthorFeed", "app.bsky.feed.getFeedGenerator", "app.bsky.feed.getFeedGenerators", "app.bsky.feed.getLikes", "app.bsky.feed.getListFeed", "app.bsky.feed.getPostThread", "app.bsky.feed.getPosts", "app.bsky.feed.getQuotes", "app.bsky.feed.getRepostedBy", "app.bsky.feed.searchPosts", "app.bsky.graph.getFollowers", "app.bsky.graph.getFollows", "app.bsky.graph.getKnownFollowers", "app.bsky.graph.getList", "app.bsky.graph.getLists", "app.bsky.graph.getRelationships", "app.bsky.graph.getStarterPack", "app.bsky.graph.getStarterPacks", "app.bsky.labeler.getServices",]);
const PRIVATE_XRPC_QUERIES = new Set([ "app.bsky.actor.getPreferences", "app.bsky.bookmark.getBookmarks", "app.bsky.feed.getActorLikes", "app.bsky.feed.getTimeline", "app.bsky.graph.getBlocks", "app.bsky.graph.getListBlocks", "app.bsky.graph.getListMutes", "app.bsky.graph.getMutes", "app.bsky.notification.getUnreadCount", "app.bsky.notification.listNotifications",]);
export class PluginRequests { constructor({ dataLayer, session, permissionsManager, fetchImpl }) { this.dataLayer = dataLayer; this.session = session; this.permissionsManager = permissionsManager; this.fetchImpl = fetchImpl; }
async pluginFetch(permissions, url, init) { if (!permissions.allowsFetch(url)) { throw new Error(`fetch to "${url}" not permitted`); } const fetchImpl = this.fetchImpl ?? fetch.bind(globalThis); const response = await fetchImpl(url, { ...sanitizeFetchInit(init), credentials: "omit", redirect: "error", mode: "cors", referrerPolicy: "no-referrer", }); const bodyBuffer = await response.arrayBuffer(); if (bodyBuffer.byteLength > MAX_RESPONSE_BYTES) { throw new Error( `fetch response too large (${bodyBuffer.byteLength} bytes, max ${MAX_RESPONSE_BYTES})`, ); } return { status: response.status, ok: response.ok, headers: filterResponseHeaders(response.headers, ["content-type"]), body: bodyBuffer, }; }
async pluginXrpcRequest(plugin, nsid, params) { const access = getXrpcQueryAccess(nsid); if (!access) { throw new Error(`xrpcQuery: "${nsid}" is not an allowed query`); } if (access === "private") { if (!this.session) throw new Error("Not signed in"); this.permissionsManager.requireActionPermission(plugin, "privateData"); } const query = sanitizeXrpcParams(params); try { const res = await this.dataLayer.api.appViewRequest(nsid, { query }); return { ok: true, status: res.status, data: res.data }; } catch (error) { if (error instanceof ApiError) { return { ok: false, status: error.status, data: error.data ?? null }; } throw new Error(`xrpcQuery: request to "${nsid}" failed`); } }}
function sanitizeFetchInit(init) { const safeInit = {}; const method = (init?.method ?? "GET").toUpperCase(); if (!ALLOWED_METHODS.includes(method)) { throw new Error(`fetch method "${method}" not permitted`); } safeInit.method = method; const headers = {}; for (const [name, value] of Object.entries(init?.headers ?? {})) { const lowerName = String(name).toLowerCase(); if (FORBIDDEN_HEADERS.includes(lowerName)) { throw new Error(`fetch header "${name}" not permitted`); } headers[name] = String(value); } safeInit.headers = headers; if (init?.body != null) { if (typeof init.body !== "string") { throw new Error("fetch body must be a string"); } if (init.body.length > MAX_BODY_CHARS) { throw new Error("fetch body too large"); } safeInit.body = init.body; } return safeInit;}
function getXrpcQueryAccess(nsid) { if (PUBLIC_XRPC_QUERIES.has(nsid)) return "public"; if (PRIVATE_XRPC_QUERIES.has(nsid)) return "private"; return null;}
const MAX_XRPC_PARAMS = 20;const MAX_XRPC_PARAM_CHARS = 2000;
function sanitizeXrpcParams(params) { if (params == null) return {}; if (typeof params !== "object" || Array.isArray(params)) { throw new Error("xrpcQuery params must be an object"); } const entries = Object.entries(params); if (entries.length > MAX_XRPC_PARAMS) { throw new Error("xrpcQuery: too many params"); } const sanitized = {}; for (const [name, value] of entries) { if (value == null) continue; const values = Array.isArray(value) ? value : [value]; for (const entry of values) { const entryType = typeof entry; if ( entryType !== "string" && entryType !== "number" && entryType !== "boolean" ) { throw new Error(`xrpcQuery: param "${name}" has an unsupported type`); } if (entryType === "string" && entry.length > MAX_XRPC_PARAM_CHARS) { throw new Error(`xrpcQuery: param "${name}" is too long`); } } sanitized[name] = value; } return sanitized;}
function filterResponseHeaders(headers, allowedNames) { const picked = {}; for (const name of allowedNames) { const value = headers.get(name); if (value != null) picked[name] = value; } return picked;}