Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194import { ok } from "@atcute/client";import type { Nsid } from "@atcute/lexicons/syntax";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { spindleXrpc, type SpindleTarget } from "#lib/api/spindlewrite.ts";import { serviceDidForHost } from "#lib/auth/agent.ts";import type { Permission } from "#lib/auth/scopes.ts";import { mainSchema as createWebhookSchema } from "#lib/api/lexicons/types/org/tangled/webhook/createWebhook.ts";import type { Delivery as RawDelivery, Webhook} from "#lib/api/lexicons/types/org/tangled/webhook/defs.ts";import { mainSchema as deleteWebhookSchema } from "#lib/api/lexicons/types/org/tangled/webhook/deleteWebhook.ts";import { mainSchema as listDeliveriesSchema } from "#lib/api/lexicons/types/org/tangled/webhook/listWebhookDeliveries.ts";import { mainSchema as listWebhooksSchema } from "#lib/api/lexicons/types/org/tangled/webhook/listWebhooks.ts";import { mainSchema as retryDeliverySchema } from "#lib/api/lexicons/types/org/tangled/webhook/retryWebhookDelivery.ts";import { mainSchema as updateWebhookSchema } from "#lib/api/lexicons/types/org/tangled/webhook/updateWebhook.ts";import { didOf } from "#lib/api/syntax.ts";
export type { Webhook };
// request and response bodies arrive as lexicon bytes; the UI shows them as text.export interface Delivery extends Omit<RawDelivery, "requestBody" | "responseBody"> { requestBody?: string; responseBody?: string;}
export const WEBHOOK_EVENTS = [ { id: "push", label: "Push events" }, { id: "repository:renamed", label: "Repository renamed" }, { id: "pull_request:created", label: "Pull request opened" }, { id: "pull_request:resubmitted", label: "Pull request resubmitted" }, { id: "pull_request:merged", label: "Pull request merged" }, { id: "pull_request:closed", label: "Pull request closed" }, { id: "pull_request:reopened", label: "Pull request reopened" }] as const;
export type WebhookEvent = (typeof WEBHOOK_EVENTS)[number]["id"];export type WebhookEventSelection = Record<WebhookEvent, boolean>;export type WebhookActivation = "active" | "inactive";
export const webhookActivation = (active: boolean): WebhookActivation => active ? "active" : "inactive";export const webhookIsActive = (activation: WebhookActivation): boolean => activation === "active";
const WEBHOOK_METHODS = [ createWebhookSchema.nsid, deleteWebhookSchema.nsid, listDeliveriesSchema.nsid, listWebhooksSchema.nsid, retryDeliverySchema.nsid, updateWebhookSchema.nsid] as const;
export const webhookPermissions = (spindle: URL): readonly Permission[] => { const aud = didOf(serviceDidForHost(spindle.host)); return WEBHOOK_METHODS.map((lxm) => ({ resource: "rpc", lxm: lxm as Nsid, aud }));};
export const webhookEventSelection = ( events: readonly string[] = ["push"]): WebhookEventSelection => Object.fromEntries( WEBHOOK_EVENTS.map(({ id }) => [id, events.includes(id)]) ) as WebhookEventSelection;
export const selectedWebhookEvents = (selection: WebhookEventSelection): WebhookEvent[] => WEBHOOK_EVENTS.flatMap(({ id }) => (selection[id] ? [id] : []));
export const isWebhookUrl = (value: string): boolean => { try { return ["http:", "https:"].includes(new URL(value.trim()).protocol); } catch { return false; }};
type GenericUri = `${string}:${string}`;
const url = (value: string): GenericUri => { const normalized = value.trim(); if (!isWebhookUrl(normalized)) throw new Error("Webhook URL must use HTTP or HTTPS."); return normalized as GenericUri;};
export const listWebhooks = async ( agent: OAuthUserAgent, spindle: URL, target: SpindleTarget): Promise<Webhook[]> => { const { webhooks } = await ok( (await spindleXrpc(agent, spindle, target)).call(listWebhooksSchema, { params: { repo: target.repoDid } }) ); return webhooks ?? [];};
export interface CreateWebhook { url: string; events: WebhookEvent[]; secret?: string; active?: boolean;}
export const createWebhook = async ( agent: OAuthUserAgent, spindle: URL, target: SpindleTarget, webhook: CreateWebhook): Promise<string> => { const { id } = await ok( (await spindleXrpc(agent, spindle, target)).call(createWebhookSchema, { input: { ...webhook, repo: target.repoDid, url: url(webhook.url) } }) ); return id;};
export interface UpdateWebhook { url?: string; events?: WebhookEvent[]; secret?: string; active?: boolean;}
export const updateWebhook = async ( agent: OAuthUserAgent, spindle: URL, target: SpindleTarget, id: string, webhook: UpdateWebhook): Promise<void> => { const { url: webhookUrl, ...changes } = webhook; await ok( (await spindleXrpc(agent, spindle, target)).call(updateWebhookSchema, { input: { ...changes, repo: target.repoDid, webhook: id, ...(webhookUrl === undefined ? {} : { url: url(webhookUrl) }) } }) );};
export const deleteWebhook = async ( agent: OAuthUserAgent, spindle: URL, target: SpindleTarget, id: string): Promise<void> => { await ok( (await spindleXrpc(agent, spindle, target)).call(deleteWebhookSchema, { input: { repo: target.repoDid, webhook: id } }) );};
export const listWebhookDeliveries = async ( agent: OAuthUserAgent, spindle: URL, target: SpindleTarget, ids: string[], limit = 100): Promise<Delivery[]> => { const { deliveries } = await ok( (await spindleXrpc(agent, spindle, target)).call(listDeliveriesSchema, { params: { repo: target.repoDid, webhooks: ids, limit } }) ); return (deliveries ?? []).map(({ requestBody, responseBody, ...delivery }) => ({ ...delivery, requestBody: requestBody && text(requestBody), responseBody: responseBody && text(responseBody) }));};
const text = ({ $bytes }: { $bytes: string }): string => new TextDecoder().decode(Uint8Array.from(atob($bytes), (c) => c.charCodeAt(0)));
export const retryWebhookDelivery = async ( agent: OAuthUserAgent, spindle: URL, target: SpindleTarget, webhookId: string, deliveryId: string): Promise<void> => { await ok( (await spindleXrpc(agent, spindle, target)).call(retryDeliverySchema, { input: { repo: target.repoDid, webhook: webhookId, delivery: deliveryId } }) );};