import { authedGet, authedPost, type AppviewContext } from "#lib/api/appview.ts";
import { serviceDidForHost } from "#lib/auth/agent.ts";
import type { Permission } from "#lib/auth/scopes.ts";
import { didOf, nsidOf, type Nsid } from "#lib/api/syntax.ts";
import type { XrpcRequestInit } from "#lib/api/client.ts";
interface DomainClaimResponse {
domain?: string;
}
const GET_DOMAIN_CLAIM = nsidOf("org.tangled.temp.site.getDomainClaim");
const CLAIM_DOMAIN = nsidOf("org.tangled.temp.site.claimDomain");
const RELEASE_DOMAIN = nsidOf("org.tangled.temp.site.releaseDomain");
const GET_SITE_CONFIG = nsidOf("org.tangled.temp.repo.getSiteConfig");
const GET_DEPLOY_HISTORY = nsidOf("org.tangled.temp.repo.getDeployHistory");
const UPDATE_SITE_CONFIG = nsidOf("org.tangled.temp.repo.updateSiteConfig");
const DISABLE_SITE = nsidOf("org.tangled.temp.repo.disableSite");
const DEPLOY_SITE = nsidOf("org.tangled.temp.repo.deploySite");
// these procedures differ only by nsid and input shape
const posting =
<Arg>(nsid: Nsid, body: (arg: Arg) => object) =>
(ctx: AppviewContext, arg: Arg, init?: XrpcRequestInit): Promise<void> =>
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, aud }));
};
export const getDomainClaim = async (
ctx: AppviewContext,
init?: XrpcRequestInit
): Promise<string | null> => {
const res = await authedGet<DomainClaimResponse>(ctx, GET_DOMAIN_CLAIM, undefined, init);
return res.domain ?? null;
};
export const getSiteConfig = async (
ctx: AppviewContext,
repoDid: string,
init?: XrpcRequestInit
): Promise<SiteConfig | null> => {
const res = await authedGet<SiteConfigResponse>(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<DeployHistoryResponse>(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<SitePanel> => {
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 }));
};