~james/core

core/web/src/lib/api/sites.ts -rw-r--r-- 4.4 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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 }));
};