kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { HTTPException } from "hono/http-exception";
2import { getGithubApp } from "../../plugins/github/utils/github-app";
3
4async function verifyGithubInstallation({
5 repositoryOwner,
6 repositoryName,
7}: {
8 repositoryOwner: string;
9 repositoryName: string;
10}) {
11 const githubApp = getGithubApp();
12
13 try {
14 if (!githubApp) {
15 throw new HTTPException(500, {
16 message: "GitHub app not configured",
17 });
18 }
19
20 const { data: installation } =
21 await githubApp.octokit.rest.apps.getRepoInstallation({
22 owner: repositoryOwner,
23 repo: repositoryName,
24 });
25
26 const octokit = await githubApp.getInstallationOctokit(installation.id);
27 const { data: repo } = await octokit.rest.repos.get({
28 owner: repositoryOwner,
29 repo: repositoryName,
30 });
31
32 const requiredPermissions = ["issues"];
33 const hasRequiredPermissions = checkPermissions(
34 installation.permissions,
35 requiredPermissions,
36 );
37 const missingPermissions = getMissingPermissions(
38 installation.permissions,
39 requiredPermissions,
40 );
41
42 if (!hasRequiredPermissions) {
43 return {
44 isInstalled: true,
45 installationId: installation.id,
46 repositoryExists: true,
47 repositoryPrivate: repo.private,
48 permissions: installation.permissions,
49 hasRequiredPermissions: false,
50 missingPermissions,
51 message: `GitHub App is installed but missing required permissions: ${missingPermissions.join(", ")}`,
52 settingsUrl: `https://github.com/settings/installations/${installation.id}`,
53 installationUrl: process.env.GITHUB_APP_NAME
54 ? `https://github.com/apps/${process.env.GITHUB_APP_NAME}/installations/new/permissions?target_id=${repo.id}`
55 : undefined,
56 };
57 }
58
59 return {
60 isInstalled: true,
61 installationId: installation.id,
62 repositoryExists: true,
63 repositoryPrivate: repo.private,
64 permissions: installation.permissions,
65 hasRequiredPermissions: true,
66 missingPermissions: [],
67 installationUrl: `https://github.com/apps/${process.env.GITHUB_APP_NAME}/installations/new/permissions?target_id=${repo.id}`,
68 message:
69 "GitHub App is properly installed and has all required permissions",
70 settingsUrl: `https://github.com/settings/installations/${installation.id}`,
71 };
72 } catch (error) {
73 const githubError = error as { status?: number; message?: string };
74
75 if (githubError.status === 404) {
76 try {
77 if (!githubApp) {
78 throw new HTTPException(500, {
79 message: "GitHub app not configured",
80 });
81 }
82
83 await githubApp.octokit.rest.repos.get({
84 owner: repositoryOwner,
85 repo: repositoryName,
86 });
87
88 const repoId = await getRepositoryId(repositoryOwner, repositoryName);
89
90 return {
91 isInstalled: false,
92 installationId: null,
93 repositoryExists: true,
94 repositoryPrivate: null,
95 permissions: null,
96 hasRequiredPermissions: false,
97 missingPermissions: [],
98 message: "Repository exists but GitHub App is not installed",
99 installationUrl: process.env.GITHUB_APP_NAME
100 ? `https://github.com/apps/${process.env.GITHUB_APP_NAME}/installations/new/permissions?target_id=${repoId}`
101 : undefined,
102 settingsUrl: process.env.GITHUB_APP_NAME
103 ? `https://github.com/apps/${process.env.GITHUB_APP_NAME}`
104 : undefined,
105 };
106 } catch (repoError) {
107 const repoGithubError = repoError as {
108 status?: number;
109 message?: string;
110 };
111
112 if (repoGithubError.status === 404) {
113 return {
114 isInstalled: false,
115 installationId: null,
116 repositoryExists: false,
117 repositoryPrivate: null,
118 permissions: null,
119 hasRequiredPermissions: false,
120 missingPermissions: [],
121 settingsUrl: undefined,
122 installationUrl: undefined,
123 message: "Repository does not exist or is not accessible",
124 };
125 }
126 throw new HTTPException(500, {
127 message: `Failed to verify GitHub installation: ${repoGithubError.status || repoGithubError.message || "Unknown error"}`,
128 });
129 }
130 }
131
132 throw new HTTPException(500, {
133 message: `Failed to verify GitHub installation: ${githubError.message || "Unknown error"}`,
134 });
135 }
136}
137
138function checkPermissions(
139 permissions: Record<string, string> | undefined,
140 required: string[],
141): boolean {
142 if (!permissions) return false;
143
144 return required.every((perm) => {
145 const permissionLevel = permissions[perm];
146 return permissionLevel === "write" || permissionLevel === "admin";
147 });
148}
149
150function getMissingPermissions(
151 permissions: Record<string, string> | undefined,
152 required: string[],
153): string[] {
154 if (!permissions) return required;
155
156 return required.filter((perm) => {
157 const permissionLevel = permissions[perm];
158 return permissionLevel !== "write" && permissionLevel !== "admin";
159 });
160}
161
162async function getRepositoryId(owner: string, repo: string): Promise<number> {
163 const githubApp = getGithubApp();
164
165 try {
166 if (!githubApp) {
167 throw new HTTPException(500, {
168 message: "GitHub app not configured",
169 });
170 }
171
172 const { data } = await githubApp.octokit.rest.repos.get({
173 owner,
174 repo,
175 });
176 return data.id;
177 } catch {
178 return 0;
179 }
180}
181
182export default verifyGithubInstallation;