import { ClientResponseError, isXRPCErrorPayload, type XRPCErrorPayload } from "@atcute/client"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { createBobbinClient, type BobbinContext, type QueryValue, type XrpcRequestInit } from "$lib/api/client"; import { hostForServiceDid } from "$lib/auth/agent"; export const serviceUrlFor = (host: string): string => /^[a-z][a-z0-9+.-]*:\/\//i.test(host) ? host.replace(/\/+$/, "") : `https://${host}`; export const serviceClient = (agent: OAuthUserAgent, host: string): BobbinContext => createBobbinClient({ serviceUrl: serviceUrlFor(host), agent }); const BARE_HOST_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?::[0-9]{1,5})?$/i; const isBareHost = (value: string): boolean => { if (!value || !BARE_HOST_PATTERN.test(value)) return false; const colon = value.indexOf(":"); if (colon !== -1) { const port = Number(value.slice(colon + 1)); if (port < 1 || port > 65535) return false; } return true; }; export const hostForKnot = (knot: string): string => { if (knot.startsWith("did:web:")) { if (knot.slice("did:web:".length).includes(":")) { throw new Error(`invalid knot identifier: ${knot}`); } const host = hostForServiceDid(knot); if (host && isBareHost(host)) return host; throw new Error(`invalid knot identifier: ${knot}`); } if (isBareHost(knot)) return knot; throw new Error(`invalid knot identifier: ${knot}`); }; export const buildUrl = ( origin: string, nsid: string, params?: Record ): URL => { const url = new URL(`/xrpc/${nsid}`, `${origin}/`); if (params) { for (const [key, value] of Object.entries(params)) { if (value === undefined) continue; if (Array.isArray(value)) { for (const item of value) url.searchParams.append(key, String(item)); } else { url.searchParams.set(key, String(value)); } } } return url; }; export const toResponseError = async (response: Response): Promise => { let data: XRPCErrorPayload = { error: "XRPCError", message: response.statusText }; try { const body: unknown = await response.json(); if (isXRPCErrorPayload(body)) data = body; } catch { // keeps the status-line fallback for non-json bodies } return new ClientResponseError({ status: response.status, headers: response.headers, data }); }; export const jsonGet = async ( ctx: BobbinContext, nsid: string, params?: Record, init?: XrpcRequestInit ): Promise => { const response = await ctx.fetch(buildUrl(ctx.serviceUrl, nsid, params), { headers: { accept: "application/json", ...init?.headers }, signal: init?.signal }); if (!response.ok) throw await toResponseError(response); if (typeof response.text === "function") { const text = await response.text(); return (text.length > 0 ? JSON.parse(text) : undefined) as T; } return (await response.json()) as T; }; export const jsonPost = async ( ctx: BobbinContext, nsid: string, body: unknown, init?: XrpcRequestInit ): Promise => { const response = await ctx.fetch(buildUrl(ctx.serviceUrl, nsid), { method: "POST", headers: { ...(body !== undefined ? { "content-type": "application/json" } : {}), accept: "application/json", ...init?.headers }, body: body !== undefined ? JSON.stringify(body) : undefined, signal: init?.signal }); if (!response.ok) throw await toResponseError(response); if (typeof response.text === "function") { const text = await response.text(); return (text.length > 0 ? JSON.parse(text) : undefined) as T; } return (await response.json()) as T; }; export const rawGet = async ( ctx: BobbinContext, nsid: string, params?: Record, init?: XrpcRequestInit ): Promise => { const response = await ctx.fetch(buildUrl(ctx.serviceUrl, nsid, params), { headers: init?.headers, signal: init?.signal }); if (!response.ok) throw await toResponseError(response); return response; };