Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134import { ClientResponseError } from "@atcute/client";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { mintServiceAuth } from "$lib/auth/agent";import type { Permission } from "$lib/auth/scopes";import type { NotificationOffer } from "$lib/components/notifications/types";import { mainSchema as acceptCollaborationSchema, type $input as CollaborationInput} from "$lib/api/lexicons/types/sh/tangled/repo/acceptCollaboration";import { mainSchema as acceptMembershipSchema, type $input as MembershipInput} from "$lib/api/lexicons/types/sh/tangled/knot/acceptMembership";import type * as CollaboratorAcceptance from "$lib/api/lexicons/types/sh/tangled/repo/collaboratorAcceptance";import type * as MemberAcceptance from "$lib/api/lexicons/types/sh/tangled/knot/memberAcceptance";import { toResponseError } from "$lib/api/_request";import { putRecord } from "$lib/api/write";
const ACCEPTANCES: { membership: { collection: MemberAcceptance.Main["$type"]; procedure: typeof acceptMembershipSchema.nsid; }; collaboration: { collection: CollaboratorAcceptance.Main["$type"]; procedure: typeof acceptCollaborationSchema.nsid; };} = { membership: { collection: "sh.tangled.knot.memberAcceptance", procedure: acceptMembershipSchema.nsid }, collaboration: { collection: "sh.tangled.repo.collaboratorAcceptance", procedure: acceptCollaborationSchema.nsid }};
export const permissionsFor = (offer: NotificationOffer): readonly Permission[] => { const { collection, procedure } = ACCEPTANCES[offer.kind]; return [ { resource: "repo", collection, actions: ["create", "update"] }, { resource: "rpc", lxm: procedure, aud: offer.subject } ];};
export type AcceptStage = "record" | "token" | "call";
const MINT_DEADLINE_MS = 15_000;const CALL_DEADLINE_MS = 30_000;
const SENTENCES: Record< AcceptStage, { silent: (host: string) => string; answered: (host: string, description: string) => string }> = { record: { silent: () => "Your account wouldn't store this acceptance. Nothing was granted.", answered: (_host, description) => `Your account stored nothing, so nothing was granted: ${description}` }, token: { silent: (host) => `Your acceptance is stored, but signing the call to ${host} failed.`, answered: (host, description) => `Your acceptance is stored, but your account wouldn't sign the call to ${host}: ${description}` }, call: { silent: (host) => `${host} didn't answer. Your acceptance is stored, so try again.`, answered: (_host, description) => description }};
export class OfferRefused extends Error { constructor( readonly stage: AcceptStage, cause: unknown ) { super(`accepting failed at ${stage}`, { cause }); }
get description(): string | null { return this.cause instanceof ClientResponseError ? (this.cause.description ?? this.cause.error) : null; } sentence(knot: URL): string { const { silent, answered } = SENTENCES[this.stage]; const host = knot.host; return this.description === null ? silent(host) : answered(host, this.description); }}
const refusing = async <T>(stage: AcceptStage, act: () => Promise<T>): Promise<T> => { try { return await act(); } catch (cause) { throw new OfferRefused(stage, cause); }};
export const acceptOffer = async ( agent: OAuthUserAgent, offer: NotificationOffer, fetch: typeof globalThis.fetch = globalThis.fetch): Promise<void> => { const { collection, procedure } = ACCEPTANCES[offer.kind]; const { uri } = await refusing("record", () => putRecord(agent, collection, offer.subject, { $type: collection, createdAt: new Date().toISOString() }) ); const token = await refusing("token", () => mintServiceAuth(agent, { aud: offer.subject, lxm: procedure, signal: AbortSignal.timeout(MINT_DEADLINE_MS) }) ); const body: MembershipInput & CollaborationInput = { acceptance: uri }; const response = await refusing("call", () => fetch(new URL(`/xrpc/${procedure}`, offer.knot), { method: "POST", headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(body), signal: AbortSignal.timeout(CALL_DEADLINE_MS) }) ); if (!response.ok) throw new OfferRefused("call", await toResponseError(response));};