import { authedGet, authedPost, type AppviewContext } from "$lib/api/appview"; import { serviceDidForHost } from "$lib/auth/agent"; import type { Permission } from "$lib/auth/scopes"; import { didOf } from "$lib/api/syntax"; import type { XrpcRequestInit } from "$lib/api/client"; import type { Nsid } from "@atcute/lexicons/syntax"; interface DomainClaimResponse { domain?: string; } const GET_DOMAIN_CLAIM = "org.tangled.temp.site.getDomainClaim"; const CLAIM_DOMAIN = "org.tangled.temp.site.claimDomain"; const RELEASE_DOMAIN = "org.tangled.temp.site.releaseDomain"; const GET_SITE_CONFIG = "org.tangled.temp.repo.getSiteConfig"; const GET_DEPLOY_HISTORY = "org.tangled.temp.repo.getDeployHistory"; const UPDATE_SITE_CONFIG = "org.tangled.temp.repo.updateSiteConfig"; const DISABLE_SITE = "org.tangled.temp.repo.disableSite"; const DEPLOY_SITE = "org.tangled.temp.repo.deploySite"; // these procedures differ only by nsid and input shape const posting = (nsid: string, body: (arg: Arg) => object) => (ctx: AppviewContext, arg: Arg, init?: XrpcRequestInit): Promise => authedPost(ctx, nsid, body(arg), init).then(() => undefined); const byRepoDid = (repoDid: string) => ({ repoDid }); export const claimDomain = posting(CLAIM_DOMAIN, (domain: string) => ({ domain })); export const releaseDomain = posting(RELEASE_DOMAIN, (domain: string) => ({ domain })); export interface SiteConfig { branch: string; dir: string; isIndex: boolean; } interface SiteConfigResponse { config?: SiteConfig; } export interface DeployHistoryEntry { status: "success" | "failure"; trigger: string; branch: string; dir: string; commitSha: string; error: string; createdAt: string; } interface DeployHistoryResponse { deploys?: DeployHistoryEntry[]; } export interface SiteConfigParams { repoDid: string; name: string; rkey: string; knot: string; branch: string; dir: string; isIndex: boolean; } export const updateSiteConfig = posting(UPDATE_SITE_CONFIG, (params: SiteConfigParams) => params); export const deploySite = posting(DEPLOY_SITE, byRepoDid); export const disableSite = posting(DISABLE_SITE, byRepoDid); const SITE_METHODS = [ GET_SITE_CONFIG, GET_DEPLOY_HISTORY, UPDATE_SITE_CONFIG, DISABLE_SITE, DEPLOY_SITE ] as const; // repo-panel methods; the account-level claim tab (/settings/sites) needs no grant export const sitesPermissions = (service: URL): readonly Permission[] => { const aud = didOf(serviceDidForHost(service.host)); return SITE_METHODS.map((lxm) => ({ resource: "rpc", lxm: lxm as Nsid, aud })); }; export const getDomainClaim = async ( ctx: AppviewContext, init?: XrpcRequestInit ): Promise => { const res = await authedGet(ctx, GET_DOMAIN_CLAIM, undefined, init); return res.domain ?? null; }; export const getSiteConfig = async ( ctx: AppviewContext, repoDid: string, init?: XrpcRequestInit ): Promise => { const res = await authedGet(ctx, GET_SITE_CONFIG, { repoDid }, init); return res.config ?? null; }; export const getDeployHistory = async ( ctx: AppviewContext, repoDid: string, init?: XrpcRequestInit ): Promise<{ deploys: DeployHistoryEntry[] }> => { const res = await authedGet(ctx, GET_DEPLOY_HISTORY, { repoDid }, init); return { deploys: res.deploys ?? [] }; }; export interface SitePanel { domain: string | null; config: SiteConfig | null; deploys: DeployHistoryEntry[]; } // sites returns 404 for a repo with no config or a lapsed domain claim, so skip history until both exist to keep the setup form reachable export const loadSitePanel = async ( ctx: AppviewContext, repoDid: string, init?: XrpcRequestInit ): Promise => { const [domain, config] = await Promise.all([ getDomainClaim(ctx, init), getSiteConfig(ctx, repoDid, init) ]); if (!config || !domain) return { domain, config: config ?? null, deploys: [] }; const { deploys } = await getDeployHistory(ctx, repoDid, init); return { domain, config, deploys }; }; // keep a selected branch that was deleted or renamed so the form shows what the site deploys export const branchChoices = (selected: string, names: readonly string[]): { value: string }[] => { const listed = names.filter((name) => name.length > 0); const values = selected && !listed.includes(selected) ? [selected, ...listed] : listed; return values.map((value) => ({ value })); };