@opengeni/api-router 2.2.0-canary.0 → 2.3.2-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +5 -2
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/organization-user-setup.d.ts +57 -0
- package/dist/{chunk-3TP54PPX.js → chunk-IBV7Z6F4.js} +3545 -2009
- package/dist/chunk-IBV7Z6F4.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-app-home.d.ts +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/routes/automations.d.ts +13 -0
- package/dist/routes/insights.d.ts +2 -1
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/package.json +18 -18
- package/src/app.ts +56 -5
- package/src/auth/managed-auth.ts +29 -34
- package/src/auth/managed-email.ts +174 -0
- package/src/auth/organization-user-setup.ts +217 -0
- package/src/http/auth.ts +15 -0
- package/src/http/sse.ts +62 -13
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/mcp/company-brain-governed-writes.ts +4 -4
- package/src/mcp/company-profile-agent-admin.ts +11 -18
- package/src/mcp/remember.ts +4 -4
- package/src/mcp/server.ts +50 -4
- package/src/mcp/session-view.ts +8 -2
- package/src/routes/automations.ts +3 -3
- package/src/routes/documents.ts +2 -0
- package/src/routes/insights.ts +61 -19
- package/src/routes/managed-onboarding.ts +317 -0
- package/src/routes/organization-memberships.ts +212 -155
- package/src/routes/pr-review-github.ts +844 -0
- package/src/routes/pr-review.ts +20 -0
- package/src/routes/rigs.ts +37 -4
- package/src/routes/sessions.ts +101 -0
- package/src/sandbox/channel-a.ts +34 -7
- package/src/sandbox/viewer.ts +47 -11
- package/dist/chunk-3TP54PPX.js.map +0 -1
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
2
|
+
import {
|
|
3
|
+
AUTOMATION_WEBHOOK_MAX_BYTES,
|
|
4
|
+
OPENGENI_PR_REVIEW_PACK_ID,
|
|
5
|
+
PrReviewManagedGitHubSetup,
|
|
6
|
+
type AccessGrant,
|
|
7
|
+
type GitHubInstallationBindingCandidate,
|
|
8
|
+
type GitHubInstallationBindingProof,
|
|
9
|
+
} from "@opengeni/contracts";
|
|
10
|
+
import {
|
|
11
|
+
automationRequestDigest,
|
|
12
|
+
getCapabilityPack,
|
|
13
|
+
hasPermission,
|
|
14
|
+
PR_REVIEW_AUTOMATION_TEMPLATE_ID,
|
|
15
|
+
prReviewPackConnectorId,
|
|
16
|
+
requireAutomationAdapter,
|
|
17
|
+
requireAccessGrant,
|
|
18
|
+
requirePermission,
|
|
19
|
+
verifyPrReviewWebhook,
|
|
20
|
+
type ApiRouteDeps,
|
|
21
|
+
} from "@opengeni/core";
|
|
22
|
+
import {
|
|
23
|
+
AutomationDeliveryConflictError,
|
|
24
|
+
encryptVariableSetValue,
|
|
25
|
+
getAutomationSourceSecret,
|
|
26
|
+
getPackInstallation,
|
|
27
|
+
listPrReviewAppRegistrations,
|
|
28
|
+
listPrReviewRepositoryBindings,
|
|
29
|
+
nestedPostgresSqlState,
|
|
30
|
+
PrReviewDispatchAuthorityError,
|
|
31
|
+
recordAuditEvent,
|
|
32
|
+
resolveManagedGitHubPrReviewRoute,
|
|
33
|
+
syncManagedGitHubPrReviewInstallation,
|
|
34
|
+
} from "@opengeni/db";
|
|
35
|
+
import {
|
|
36
|
+
authorizeGitHubInstallationBinding,
|
|
37
|
+
createSignedState,
|
|
38
|
+
discoverGitHubInstallationBindingCandidates,
|
|
39
|
+
GitHubAppApiError,
|
|
40
|
+
GitHubAppConfigurationError,
|
|
41
|
+
GitHubInstallationAuthorityError,
|
|
42
|
+
githubOAuthAuthorizeUrl,
|
|
43
|
+
prReviewGitHubAppMissingSettings,
|
|
44
|
+
readSignedState,
|
|
45
|
+
settingsForPrReviewGitHubApp,
|
|
46
|
+
stateMaxAgeSeconds,
|
|
47
|
+
type GitHubSignedStatePayload,
|
|
48
|
+
} from "@opengeni/github";
|
|
49
|
+
import type { Context, Hono } from "hono";
|
|
50
|
+
import { deleteCookie, setCookie } from "hono/cookie";
|
|
51
|
+
import { HTTPException } from "hono/http-exception";
|
|
52
|
+
import { githubBrowserBaseUrl } from "../github-browser-flow";
|
|
53
|
+
import { acceptAutomationEvent, readAutomationWebhookBody } from "./automations";
|
|
54
|
+
|
|
55
|
+
const stateCookie = "opengeni_pr_review_github_state";
|
|
56
|
+
const bindingStateMaxAgeSeconds = 10 * 60;
|
|
57
|
+
const appName = "OpenGeni Lens" as const;
|
|
58
|
+
|
|
59
|
+
export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
60
|
+
app.post("/v1/webhooks/pr-review/github", async (c) => {
|
|
61
|
+
const secret = deps.settings.prReviewGithubWebhookSecret?.trim();
|
|
62
|
+
if (!secret) {
|
|
63
|
+
throw new HTTPException(503, { message: "OpenGeni Lens webhook is unavailable" });
|
|
64
|
+
}
|
|
65
|
+
const rawBody = await readAutomationWebhookBody(c.req.raw, AUTOMATION_WEBHOOK_MAX_BYTES);
|
|
66
|
+
if (
|
|
67
|
+
!verifyPrReviewWebhook({
|
|
68
|
+
provider: "github",
|
|
69
|
+
rawBody,
|
|
70
|
+
headers: c.req.raw.headers,
|
|
71
|
+
secret,
|
|
72
|
+
webhookUsername: null,
|
|
73
|
+
})
|
|
74
|
+
) {
|
|
75
|
+
throw new HTTPException(401, { message: "OpenGeni Lens signature is invalid" });
|
|
76
|
+
}
|
|
77
|
+
let payload: unknown;
|
|
78
|
+
try {
|
|
79
|
+
payload = JSON.parse(new TextDecoder().decode(rawBody));
|
|
80
|
+
} catch {
|
|
81
|
+
throw new HTTPException(400, { message: "OpenGeni Lens payload is invalid JSON" });
|
|
82
|
+
}
|
|
83
|
+
const record = asRecord(payload);
|
|
84
|
+
const installationId = positiveInteger(asRecord(record?.installation)?.id);
|
|
85
|
+
const repositoryId = positiveInteger(asRecord(record?.repository)?.id);
|
|
86
|
+
if (installationId === null || repositoryId === null) {
|
|
87
|
+
return c.json(ignoredWebhook("unsupported_event"), 202);
|
|
88
|
+
}
|
|
89
|
+
const route = await resolveManagedGitHubPrReviewRoute(deps.db, {
|
|
90
|
+
installationId: String(installationId),
|
|
91
|
+
providerRepositoryId: String(repositoryId),
|
|
92
|
+
});
|
|
93
|
+
if (!route) return c.json(ignoredWebhook("repository_not_connected"), 202);
|
|
94
|
+
const source = await getAutomationSourceSecret(deps.db, route);
|
|
95
|
+
if (!source || source.status !== "active") {
|
|
96
|
+
return c.json(ignoredWebhook("source_disabled"), 202);
|
|
97
|
+
}
|
|
98
|
+
const adapter = requireAutomationAdapter(source.adapterId);
|
|
99
|
+
const requestDigest = automationRequestDigest(source.adapterId, rawBody);
|
|
100
|
+
try {
|
|
101
|
+
return c.json(
|
|
102
|
+
await acceptAutomationEvent(deps, source, {
|
|
103
|
+
deliveryKey: adapter.deliveryKey({
|
|
104
|
+
headers: c.req.raw.headers,
|
|
105
|
+
requestDigest,
|
|
106
|
+
}),
|
|
107
|
+
requestDigest,
|
|
108
|
+
normalizedEvent: adapter.normalize({
|
|
109
|
+
rawBody,
|
|
110
|
+
headers: c.req.raw.headers,
|
|
111
|
+
sourceConfiguration: source.configuration,
|
|
112
|
+
}),
|
|
113
|
+
}),
|
|
114
|
+
202,
|
|
115
|
+
);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof AutomationDeliveryConflictError) {
|
|
118
|
+
throw new HTTPException(409, { message: error.message });
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
app.get("/v1/workspaces/:workspaceId/pr-review/github", async (c) => {
|
|
125
|
+
const workspaceId = c.req.param("workspaceId");
|
|
126
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
127
|
+
await requireActivePack(deps, workspaceId);
|
|
128
|
+
const missing = prReviewGitHubAppMissingSettings(deps.settings);
|
|
129
|
+
const configured = missing.length === 0;
|
|
130
|
+
const registrations = (
|
|
131
|
+
await listPrReviewAppRegistrations(deps.db, grant.accountId, workspaceId)
|
|
132
|
+
).filter((registration) => registration.credentialKind === "managed_github_app");
|
|
133
|
+
const repositories = await listPrReviewRepositoryBindings(
|
|
134
|
+
deps.db,
|
|
135
|
+
grant.accountId,
|
|
136
|
+
workspaceId,
|
|
137
|
+
);
|
|
138
|
+
const canManage =
|
|
139
|
+
hasPermission(grant.permissions, "workspace:admin") &&
|
|
140
|
+
hasPermission(grant.permissions, "secrets:write");
|
|
141
|
+
const connectState =
|
|
142
|
+
configured && canManage
|
|
143
|
+
? createSignedState(deps.githubStateSecret, {
|
|
144
|
+
accountId: grant.accountId,
|
|
145
|
+
workspaceId,
|
|
146
|
+
intent: "pr_review_github_authority",
|
|
147
|
+
...prReviewBrowserGrantClaims(deps, grant),
|
|
148
|
+
})
|
|
149
|
+
: null;
|
|
150
|
+
const baseUrl = openGeniBaseUrl(deps, c);
|
|
151
|
+
const connectUrl = connectState
|
|
152
|
+
? `${baseUrl}/v1/workspaces/${workspaceId}/pr-review/github/connect?state=${encodeURIComponent(connectState)}`
|
|
153
|
+
: null;
|
|
154
|
+
return c.json(
|
|
155
|
+
PrReviewManagedGitHubSetup.parse({
|
|
156
|
+
configured,
|
|
157
|
+
status: !configured
|
|
158
|
+
? "unavailable"
|
|
159
|
+
: registrations.some((registration) => registration.status === "active")
|
|
160
|
+
? "connected"
|
|
161
|
+
: "not_connected",
|
|
162
|
+
appName,
|
|
163
|
+
connectUrl,
|
|
164
|
+
installations: registrations.map((registration) => {
|
|
165
|
+
const installationId = registration.installationId!;
|
|
166
|
+
const configureState = connectState
|
|
167
|
+
? createSignedState(deps.githubStateSecret, {
|
|
168
|
+
accountId: grant.accountId,
|
|
169
|
+
workspaceId,
|
|
170
|
+
expectedInstallationId: Number(installationId),
|
|
171
|
+
intent: "pr_review_github_install",
|
|
172
|
+
...prReviewBrowserGrantClaims(deps, grant),
|
|
173
|
+
})
|
|
174
|
+
: null;
|
|
175
|
+
return {
|
|
176
|
+
registrationId: registration.id,
|
|
177
|
+
installationId,
|
|
178
|
+
accountLogin: registration.providerAccountLogin,
|
|
179
|
+
configureUrl: configureState
|
|
180
|
+
? `${baseUrl}/v1/workspaces/${workspaceId}/pr-review/github/installations/${installationId}/configure?state=${encodeURIComponent(configureState)}`
|
|
181
|
+
: null,
|
|
182
|
+
repositoryCount: repositories.filter(
|
|
183
|
+
(repository) =>
|
|
184
|
+
repository.registrationId === registration.id && repository.status === "active",
|
|
185
|
+
).length,
|
|
186
|
+
};
|
|
187
|
+
}),
|
|
188
|
+
missing: deps.settings.productAccessMode === "managed" ? [] : missing,
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
app.get("/v1/workspaces/:workspaceId/pr-review/github/connect", async (c) => {
|
|
194
|
+
const workspaceId = c.req.param("workspaceId");
|
|
195
|
+
const state = requireStateQuery(c, "missing OpenGeni Lens installation state");
|
|
196
|
+
const payload = requireFreshState(state, deps, "pr_review_github_authority", workspaceId);
|
|
197
|
+
await requirePrReviewManageGrant(c, deps, workspaceId, payload);
|
|
198
|
+
await requireActivePack(deps, workspaceId);
|
|
199
|
+
assertManagedCompute(deps);
|
|
200
|
+
requireConfiguredApp(deps);
|
|
201
|
+
const discoveryState = createSignedState(deps.githubStateSecret, {
|
|
202
|
+
accountId: payload.accountId,
|
|
203
|
+
workspaceId,
|
|
204
|
+
intent: "pr_review_github_discovery",
|
|
205
|
+
...continuedBrowserGrantClaims(payload),
|
|
206
|
+
});
|
|
207
|
+
setStateCookie(c, deps, discoveryState);
|
|
208
|
+
return c.redirect(
|
|
209
|
+
githubOAuthAuthorizeUrl({
|
|
210
|
+
clientId: deps.settings.prReviewGithubClientId!,
|
|
211
|
+
state: discoveryState,
|
|
212
|
+
redirectUri: `${openGeniBaseUrl(deps, c)}/v1/pr-review/github/oauth/callback`,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.get(
|
|
218
|
+
"/v1/workspaces/:workspaceId/pr-review/github/installations/:installationId/configure",
|
|
219
|
+
async (c) => {
|
|
220
|
+
const workspaceId = c.req.param("workspaceId");
|
|
221
|
+
const installationId = positiveInteger(c.req.param("installationId"));
|
|
222
|
+
const state = requireStateQuery(c, "missing OpenGeni Lens configuration state");
|
|
223
|
+
const payload = requireFreshState(state, deps, "pr_review_github_install", workspaceId);
|
|
224
|
+
if (installationId === null || payload.expectedInstallationId !== installationId) {
|
|
225
|
+
throw new HTTPException(400, { message: "invalid OpenGeni Lens installation" });
|
|
226
|
+
}
|
|
227
|
+
await requirePrReviewManageGrant(c, deps, workspaceId, payload);
|
|
228
|
+
await requireActivePack(deps, workspaceId);
|
|
229
|
+
assertManagedCompute(deps);
|
|
230
|
+
const registrations = await listPrReviewAppRegistrations(
|
|
231
|
+
deps.db,
|
|
232
|
+
payload.accountId!,
|
|
233
|
+
workspaceId,
|
|
234
|
+
);
|
|
235
|
+
const registration = registrations.find(
|
|
236
|
+
(candidate) =>
|
|
237
|
+
candidate.credentialKind === "managed_github_app" &&
|
|
238
|
+
candidate.installationId === String(installationId),
|
|
239
|
+
);
|
|
240
|
+
if (!registration) {
|
|
241
|
+
throw new HTTPException(404, { message: "OpenGeni Lens installation is not connected" });
|
|
242
|
+
}
|
|
243
|
+
setStateCookie(c, deps, state);
|
|
244
|
+
const configureUrl = githubInstallationSettingsUrl(
|
|
245
|
+
installationId,
|
|
246
|
+
registration.providerAccountLogin,
|
|
247
|
+
registration.providerAccountType,
|
|
248
|
+
);
|
|
249
|
+
configureUrl.searchParams.set("state", state);
|
|
250
|
+
return c.redirect(configureUrl.toString());
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const handleInstallCallback = async (c: Context) => {
|
|
255
|
+
const state =
|
|
256
|
+
c.req.query("state") ??
|
|
257
|
+
allCookieValues(c, stateCookie).find((candidate) => {
|
|
258
|
+
const payload = readSignedState(candidate, deps.githubStateSecret);
|
|
259
|
+
return payload?.intent === "pr_review_github_install" && isFreshState(payload);
|
|
260
|
+
});
|
|
261
|
+
if (!state) throw new HTTPException(400, { message: "missing OpenGeni Lens state" });
|
|
262
|
+
const payload = requireFreshState(state, deps, "pr_review_github_install");
|
|
263
|
+
requireStateCookie(c, state);
|
|
264
|
+
await requirePrReviewManageGrant(c, deps, payload.workspaceId!, payload);
|
|
265
|
+
await requireActivePack(deps, payload.workspaceId!);
|
|
266
|
+
assertManagedCompute(deps);
|
|
267
|
+
const setupAction = c.req.query("setup_action");
|
|
268
|
+
if (setupAction === "request") return c.html(setupPendingHtml());
|
|
269
|
+
if (setupAction !== "install" && setupAction !== "update") {
|
|
270
|
+
throw new HTTPException(400, { message: "unsupported GitHub setup action" });
|
|
271
|
+
}
|
|
272
|
+
const installationId = positiveInteger(c.req.query("installation_id"));
|
|
273
|
+
if (installationId === null) {
|
|
274
|
+
throw new HTTPException(400, { message: "missing or invalid GitHub installation_id" });
|
|
275
|
+
}
|
|
276
|
+
if (
|
|
277
|
+
payload.expectedInstallationId !== undefined &&
|
|
278
|
+
payload.expectedInstallationId !== installationId
|
|
279
|
+
) {
|
|
280
|
+
throw new HTTPException(409, {
|
|
281
|
+
message: "GitHub returned a different OpenGeni Lens installation",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
requireConfiguredApp(deps);
|
|
285
|
+
const oauthState = createSignedState(deps.githubStateSecret, {
|
|
286
|
+
accountId: payload.accountId,
|
|
287
|
+
workspaceId: payload.workspaceId,
|
|
288
|
+
installationId,
|
|
289
|
+
intent: "pr_review_github_oauth",
|
|
290
|
+
...continuedBrowserGrantClaims(payload),
|
|
291
|
+
});
|
|
292
|
+
setStateCookie(c, deps, oauthState);
|
|
293
|
+
return c.redirect(
|
|
294
|
+
githubOAuthAuthorizeUrl({
|
|
295
|
+
clientId: deps.settings.prReviewGithubClientId!,
|
|
296
|
+
state: oauthState,
|
|
297
|
+
redirectUri: `${openGeniBaseUrl(deps, c)}/v1/pr-review/github/oauth/callback`,
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
app.get("/v1/pr-review/github/setup", handleInstallCallback);
|
|
303
|
+
app.get("/v1/pr-review/github/install/callback", handleInstallCallback);
|
|
304
|
+
|
|
305
|
+
app.get("/v1/pr-review/github/oauth/callback", async (c) => {
|
|
306
|
+
const code = c.req.query("code");
|
|
307
|
+
const state = c.req.query("state");
|
|
308
|
+
if (!code || !state) {
|
|
309
|
+
throw new HTTPException(400, { message: "missing OpenGeni Lens OAuth code or state" });
|
|
310
|
+
}
|
|
311
|
+
const payload = requireFreshState(state, deps);
|
|
312
|
+
requireStateCookie(c, state);
|
|
313
|
+
const grant = await requirePrReviewManageGrant(c, deps, payload.workspaceId!, payload);
|
|
314
|
+
const packInstallation = await requireActivePack(deps, grant.workspaceId);
|
|
315
|
+
assertManagedCompute(deps);
|
|
316
|
+
requireConfiguredApp(deps);
|
|
317
|
+
|
|
318
|
+
if (payload.intent === "pr_review_github_discovery") {
|
|
319
|
+
let candidates: GitHubInstallationBindingCandidate[] | null;
|
|
320
|
+
try {
|
|
321
|
+
candidates = deps.prReviewGithubAppApi?.discoverInstallationBindingCandidates
|
|
322
|
+
? await deps.prReviewGithubAppApi.discoverInstallationBindingCandidates({ code })
|
|
323
|
+
: deps.prReviewGithubAppApi
|
|
324
|
+
? null
|
|
325
|
+
: await discoverGitHubInstallationBindingCandidates(
|
|
326
|
+
settingsForPrReviewGitHubApp(deps.settings),
|
|
327
|
+
{ code },
|
|
328
|
+
);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
throw authorityHttpError(error);
|
|
331
|
+
}
|
|
332
|
+
if (!candidates || !consistentCandidates(candidates)) {
|
|
333
|
+
throw new HTTPException(409, {
|
|
334
|
+
message: "OpenGeni Lens could not prove owner-authorized installations",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const selectionState = createSignedState(deps.githubStateSecret, {
|
|
338
|
+
accountId: grant.accountId,
|
|
339
|
+
workspaceId: grant.workspaceId,
|
|
340
|
+
intent: "pr_review_github_selection",
|
|
341
|
+
allowedInstallationIds: candidates.map(({ installation }) => installation.installationId),
|
|
342
|
+
...continuedBrowserGrantClaims(payload),
|
|
343
|
+
});
|
|
344
|
+
if (candidates.length === 0) return redirectToInstallation(c, deps, selectionState);
|
|
345
|
+
if (candidates.length === 1) {
|
|
346
|
+
return redirectToExactAuthorization(
|
|
347
|
+
c,
|
|
348
|
+
deps,
|
|
349
|
+
selectionState,
|
|
350
|
+
candidates[0]!.installation.installationId,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
setStateCookie(c, deps, selectionState);
|
|
354
|
+
return c.html(
|
|
355
|
+
installationChooserHtml(candidates, selectionState, grant.workspaceId, deps, c),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (payload.intent !== "pr_review_github_oauth") {
|
|
360
|
+
throw new HTTPException(400, { message: "invalid or expired OpenGeni Lens OAuth state" });
|
|
361
|
+
}
|
|
362
|
+
const installationId = positiveInteger(payload.installationId);
|
|
363
|
+
if (installationId === null) {
|
|
364
|
+
throw new HTTPException(400, { message: "invalid OpenGeni Lens installation id" });
|
|
365
|
+
}
|
|
366
|
+
let proof: GitHubInstallationBindingProof | null;
|
|
367
|
+
try {
|
|
368
|
+
proof = deps.prReviewGithubAppApi?.authorizeInstallationBinding
|
|
369
|
+
? await deps.prReviewGithubAppApi.authorizeInstallationBinding({ code, installationId })
|
|
370
|
+
: deps.prReviewGithubAppApi
|
|
371
|
+
? null
|
|
372
|
+
: await authorizeGitHubInstallationBinding(settingsForPrReviewGitHubApp(deps.settings), {
|
|
373
|
+
code,
|
|
374
|
+
installationId,
|
|
375
|
+
});
|
|
376
|
+
} catch (error) {
|
|
377
|
+
throw authorityHttpError(error);
|
|
378
|
+
}
|
|
379
|
+
if (!proof || !consistentProof(proof, installationId)) {
|
|
380
|
+
throw new HTTPException(409, {
|
|
381
|
+
message: "OpenGeni Lens installation proof is stale or invalid",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const repositoryIds = new Set(proof.repositories.map((repository) => repository.id));
|
|
385
|
+
if (repositoryIds.size !== proof.repositories.length) {
|
|
386
|
+
throw new HTTPException(409, { message: "GitHub returned duplicate repository identities" });
|
|
387
|
+
}
|
|
388
|
+
const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
|
|
389
|
+
(candidate) => candidate.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
|
|
390
|
+
);
|
|
391
|
+
if (!template) {
|
|
392
|
+
throw new HTTPException(503, { message: "PR Review automation template is unavailable" });
|
|
393
|
+
}
|
|
394
|
+
const encryptionKey = environmentsEncryptionKeyBytes(deps.settings);
|
|
395
|
+
if (!encryptionKey) {
|
|
396
|
+
throw new HTTPException(503, {
|
|
397
|
+
message: "OpenGeni Lens requires configured secret encryption",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
let synchronized;
|
|
401
|
+
try {
|
|
402
|
+
synchronized = await syncManagedGitHubPrReviewInstallation(deps.db, {
|
|
403
|
+
accountId: grant.accountId,
|
|
404
|
+
workspaceId: grant.workspaceId,
|
|
405
|
+
installationId,
|
|
406
|
+
providerAccountLogin: proof.installation.accountLogin,
|
|
407
|
+
providerAccountType: proof.installation.accountType as "User" | "Organization",
|
|
408
|
+
githubActorId: proof.actorId,
|
|
409
|
+
authorityKind: proof.authorityKind,
|
|
410
|
+
authorityCheckedAt: new Date(),
|
|
411
|
+
authorityExpiresAt: new Date((payload.iat + bindingStateMaxAgeSeconds) * 1_000),
|
|
412
|
+
authorityNonce: payload.nonce,
|
|
413
|
+
appId: deps.settings.prReviewGithubAppId!,
|
|
414
|
+
webhookSecretEncrypted: encryptVariableSetValue(
|
|
415
|
+
encryptionKey,
|
|
416
|
+
deps.settings.prReviewGithubWebhookSecret!,
|
|
417
|
+
),
|
|
418
|
+
repositories: proof.repositories,
|
|
419
|
+
createdBySubjectId: grant.subjectId,
|
|
420
|
+
packInstallationId: packInstallation.id,
|
|
421
|
+
packConnectorId: prReviewPackConnectorId("github"),
|
|
422
|
+
packTemplateId: template.id,
|
|
423
|
+
adapterId: template.adapterId,
|
|
424
|
+
eventTypes: template.eventTypes,
|
|
425
|
+
configuration: template.configuration,
|
|
426
|
+
sessionTemplate: template.sessionTemplate,
|
|
427
|
+
});
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (error instanceof PrReviewDispatchAuthorityError) {
|
|
430
|
+
throw new HTTPException(409, { message: error.message });
|
|
431
|
+
}
|
|
432
|
+
if (nestedPostgresSqlState(error) === "23505") {
|
|
433
|
+
throw new HTTPException(409, {
|
|
434
|
+
message:
|
|
435
|
+
"One of these repositories is already connected to OpenGeni Lens in another workspace",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
await recordAuditEvent(deps.db, {
|
|
441
|
+
accountId: grant.accountId,
|
|
442
|
+
workspaceId: grant.workspaceId,
|
|
443
|
+
subjectId: grant.subjectId,
|
|
444
|
+
action: "prReview.managed_github.connected",
|
|
445
|
+
targetType: "pr_review_app_registration",
|
|
446
|
+
targetId: synchronized.registration.id,
|
|
447
|
+
metadata: {
|
|
448
|
+
installationId,
|
|
449
|
+
providerAccountLogin: proof.installation.accountLogin,
|
|
450
|
+
repositoryCount: synchronized.repositories.length,
|
|
451
|
+
authorityKind: proof.authorityKind,
|
|
452
|
+
githubActorId: proof.actorId,
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
deleteCookie(c, stateCookie, { path: "/v1" });
|
|
456
|
+
return c.html(
|
|
457
|
+
setupSuccessHtml(
|
|
458
|
+
proof.installation.accountLogin ?? `installation ${installationId}`,
|
|
459
|
+
`${openGeniBaseUrl(deps, c)}/workspaces/${grant.workspaceId}/capabilities`,
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
app.get("/v1/workspaces/:workspaceId/pr-review/github/installations/select", async (c) => {
|
|
465
|
+
const workspaceId = c.req.param("workspaceId");
|
|
466
|
+
const state = requireStateQuery(c, "missing OpenGeni Lens selection state");
|
|
467
|
+
const payload = requireFreshState(state, deps, "pr_review_github_selection", workspaceId);
|
|
468
|
+
requireStateCookie(c, state);
|
|
469
|
+
await requirePrReviewManageGrant(c, deps, workspaceId, payload);
|
|
470
|
+
await requireActivePack(deps, workspaceId);
|
|
471
|
+
const selected = c.req.query("installation_id");
|
|
472
|
+
if (selected === "new") return redirectToInstallation(c, deps, state);
|
|
473
|
+
const installationId = positiveInteger(selected);
|
|
474
|
+
if (
|
|
475
|
+
installationId === null ||
|
|
476
|
+
!Array.isArray(payload.allowedInstallationIds) ||
|
|
477
|
+
!payload.allowedInstallationIds.includes(installationId)
|
|
478
|
+
) {
|
|
479
|
+
throw new HTTPException(403, {
|
|
480
|
+
message: "GitHub installation was not in the owner-authorized selection",
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return redirectToExactAuthorization(c, deps, state, installationId);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function requireConfiguredApp(deps: ApiRouteDeps): void {
|
|
488
|
+
const missing = prReviewGitHubAppMissingSettings(deps.settings);
|
|
489
|
+
if (missing.length > 0) {
|
|
490
|
+
throw new HTTPException(409, {
|
|
491
|
+
message: JSON.stringify({ message: "OpenGeni Lens is not configured", missing }),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function requireActivePack(deps: ApiRouteDeps, workspaceId: string) {
|
|
497
|
+
const installation = await getPackInstallation(deps.db, workspaceId, OPENGENI_PR_REVIEW_PACK_ID);
|
|
498
|
+
if (installation?.status !== "active") {
|
|
499
|
+
throw new HTTPException(409, {
|
|
500
|
+
message: "Install and enable the OpenGeni Review Bot Pack first",
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return installation;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function assertManagedCompute(deps: ApiRouteDeps): void {
|
|
507
|
+
if (deps.settings.sandboxBackend === "selfhosted") {
|
|
508
|
+
throw new HTTPException(409, { message: "OpenGeni Lens requires managed compute" });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function requirePrReviewManageGrant(
|
|
513
|
+
c: Context,
|
|
514
|
+
deps: ApiRouteDeps,
|
|
515
|
+
workspaceId: string,
|
|
516
|
+
state: GitHubSignedStatePayload,
|
|
517
|
+
): Promise<AccessGrant> {
|
|
518
|
+
let grant: AccessGrant;
|
|
519
|
+
try {
|
|
520
|
+
grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (!(error instanceof HTTPException) || error.status !== 401) throw error;
|
|
523
|
+
const handedOff = prReviewBrowserGrantFromState(deps, state, workspaceId);
|
|
524
|
+
if (!handedOff) throw error;
|
|
525
|
+
grant = handedOff;
|
|
526
|
+
}
|
|
527
|
+
requirePermission(grant, "secrets:write");
|
|
528
|
+
if (grant.accountId !== state.accountId) {
|
|
529
|
+
throw new HTTPException(403, { message: "OpenGeni Lens state does not match this workspace" });
|
|
530
|
+
}
|
|
531
|
+
return grant;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function prReviewBrowserGrantClaims(deps: ApiRouteDeps, grant: AccessGrant) {
|
|
535
|
+
if (
|
|
536
|
+
deps.settings.productAccessMode !== "configured" ||
|
|
537
|
+
!hasPermission(grant.permissions, "workspace:admin") ||
|
|
538
|
+
!hasPermission(grant.permissions, "secrets:write")
|
|
539
|
+
) {
|
|
540
|
+
return {};
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
prReviewBrowserGrantSubjectId: grant.subjectId,
|
|
544
|
+
prReviewBrowserGrantExpiresAt: Math.floor(Date.now() / 1_000) + bindingStateMaxAgeSeconds,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function continuedBrowserGrantClaims(payload: GitHubSignedStatePayload) {
|
|
549
|
+
return typeof payload.prReviewBrowserGrantSubjectId === "string" &&
|
|
550
|
+
typeof payload.prReviewBrowserGrantExpiresAt === "number"
|
|
551
|
+
? {
|
|
552
|
+
prReviewBrowserGrantSubjectId: payload.prReviewBrowserGrantSubjectId,
|
|
553
|
+
prReviewBrowserGrantExpiresAt: payload.prReviewBrowserGrantExpiresAt,
|
|
554
|
+
}
|
|
555
|
+
: {};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function prReviewBrowserGrantFromState(
|
|
559
|
+
deps: ApiRouteDeps,
|
|
560
|
+
payload: GitHubSignedStatePayload,
|
|
561
|
+
workspaceId: string,
|
|
562
|
+
): AccessGrant | null {
|
|
563
|
+
const subjectId = payload.prReviewBrowserGrantSubjectId;
|
|
564
|
+
const expiresAt = payload.prReviewBrowserGrantExpiresAt;
|
|
565
|
+
const now = Math.floor(Date.now() / 1_000);
|
|
566
|
+
if (
|
|
567
|
+
deps.settings.productAccessMode !== "configured" ||
|
|
568
|
+
payload.workspaceId !== workspaceId ||
|
|
569
|
+
typeof payload.accountId !== "string" ||
|
|
570
|
+
typeof subjectId !== "string" ||
|
|
571
|
+
typeof expiresAt !== "number" ||
|
|
572
|
+
!Number.isInteger(expiresAt) ||
|
|
573
|
+
expiresAt < now ||
|
|
574
|
+
expiresAt > payload.iat + bindingStateMaxAgeSeconds
|
|
575
|
+
) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
accountId: payload.accountId,
|
|
580
|
+
workspaceId,
|
|
581
|
+
subjectId,
|
|
582
|
+
permissions: ["workspace:admin", "secrets:write"],
|
|
583
|
+
metadata: { prReviewGithubBrowserHandoff: true, expiresAt },
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function redirectToInstallation(c: Context, deps: ApiRouteDeps, sourceState: string): Response {
|
|
588
|
+
const payload = readSignedState(sourceState, deps.githubStateSecret);
|
|
589
|
+
const slug = deps.settings.prReviewGithubAppSlug?.trim();
|
|
590
|
+
if (!payload?.accountId || !payload.workspaceId || !slug) {
|
|
591
|
+
throw new HTTPException(409, { message: "OpenGeni Lens installation is unavailable" });
|
|
592
|
+
}
|
|
593
|
+
const installState = createSignedState(deps.githubStateSecret, {
|
|
594
|
+
accountId: payload.accountId,
|
|
595
|
+
workspaceId: payload.workspaceId,
|
|
596
|
+
intent: "pr_review_github_install",
|
|
597
|
+
...continuedBrowserGrantClaims(payload),
|
|
598
|
+
});
|
|
599
|
+
setStateCookie(c, deps, installState);
|
|
600
|
+
return c.redirect(
|
|
601
|
+
`https://github.com/apps/${encodeURIComponent(slug)}/installations/new?state=${encodeURIComponent(installState)}`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function redirectToExactAuthorization(
|
|
606
|
+
c: Context,
|
|
607
|
+
deps: ApiRouteDeps,
|
|
608
|
+
sourceState: string,
|
|
609
|
+
installationId: number,
|
|
610
|
+
): Response {
|
|
611
|
+
const payload = readSignedState(sourceState, deps.githubStateSecret);
|
|
612
|
+
const clientId = deps.settings.prReviewGithubClientId?.trim();
|
|
613
|
+
if (!payload?.accountId || !payload.workspaceId || !clientId) {
|
|
614
|
+
throw new HTTPException(409, { message: "OpenGeni Lens authorization is unavailable" });
|
|
615
|
+
}
|
|
616
|
+
const oauthState = createSignedState(deps.githubStateSecret, {
|
|
617
|
+
accountId: payload.accountId,
|
|
618
|
+
workspaceId: payload.workspaceId,
|
|
619
|
+
installationId,
|
|
620
|
+
intent: "pr_review_github_oauth",
|
|
621
|
+
...continuedBrowserGrantClaims(payload),
|
|
622
|
+
});
|
|
623
|
+
setStateCookie(c, deps, oauthState);
|
|
624
|
+
return c.redirect(
|
|
625
|
+
githubOAuthAuthorizeUrl({
|
|
626
|
+
clientId,
|
|
627
|
+
state: oauthState,
|
|
628
|
+
redirectUri: `${openGeniBaseUrl(deps, c)}/v1/pr-review/github/oauth/callback`,
|
|
629
|
+
}),
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function requireFreshState(
|
|
634
|
+
state: string,
|
|
635
|
+
deps: ApiRouteDeps,
|
|
636
|
+
intent?: string,
|
|
637
|
+
workspaceId?: string,
|
|
638
|
+
): GitHubSignedStatePayload {
|
|
639
|
+
const payload = readSignedState(state, deps.githubStateSecret);
|
|
640
|
+
if (
|
|
641
|
+
!payload ||
|
|
642
|
+
!isFreshState(payload) ||
|
|
643
|
+
typeof payload.accountId !== "string" ||
|
|
644
|
+
typeof payload.workspaceId !== "string" ||
|
|
645
|
+
(intent !== undefined && payload.intent !== intent) ||
|
|
646
|
+
(workspaceId !== undefined && payload.workspaceId !== workspaceId)
|
|
647
|
+
) {
|
|
648
|
+
throw new HTTPException(400, { message: "invalid or expired OpenGeni Lens state" });
|
|
649
|
+
}
|
|
650
|
+
return payload;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function isFreshState(payload: GitHubSignedStatePayload): boolean {
|
|
654
|
+
const age = Math.floor(Date.now() / 1_000) - payload.iat;
|
|
655
|
+
return age >= 0 && age < bindingStateMaxAgeSeconds;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function requireStateQuery(c: Context, message: string): string {
|
|
659
|
+
const state = c.req.query("state");
|
|
660
|
+
if (!state) throw new HTTPException(400, { message });
|
|
661
|
+
return state;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function setStateCookie(c: Context, deps: ApiRouteDeps, state: string): void {
|
|
665
|
+
setCookie(c, stateCookie, state, {
|
|
666
|
+
httpOnly: true,
|
|
667
|
+
sameSite: "Lax",
|
|
668
|
+
secure:
|
|
669
|
+
deps.settings.publicBaseUrl?.startsWith("https://") ||
|
|
670
|
+
c.req.header("x-forwarded-proto") === "https" ||
|
|
671
|
+
new URL(c.req.url).protocol === "https:",
|
|
672
|
+
path: "/v1",
|
|
673
|
+
maxAge: stateMaxAgeSeconds,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function requireStateCookie(c: Context, state: string): void {
|
|
678
|
+
if (!allCookieValues(c, stateCookie).includes(state)) {
|
|
679
|
+
throw new HTTPException(400, { message: "invalid OpenGeni Lens browser state" });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function allCookieValues(c: Context, name: string): string[] {
|
|
684
|
+
const prefix = `${name}=`;
|
|
685
|
+
return (c.req.header("cookie") ?? "")
|
|
686
|
+
.split(";")
|
|
687
|
+
.map((part) => part.trim())
|
|
688
|
+
.filter((part) => part.startsWith(prefix))
|
|
689
|
+
.map((part) => {
|
|
690
|
+
try {
|
|
691
|
+
return decodeURIComponent(part.slice(prefix.length));
|
|
692
|
+
} catch {
|
|
693
|
+
return part.slice(prefix.length);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function consistentCandidates(candidates: GitHubInstallationBindingCandidate[]): boolean {
|
|
699
|
+
const ids = new Set<number>();
|
|
700
|
+
return candidates.every(({ installation, authorityKind }) => {
|
|
701
|
+
if (
|
|
702
|
+
!Number.isSafeInteger(installation.installationId) ||
|
|
703
|
+
installation.installationId <= 0 ||
|
|
704
|
+
!Number.isSafeInteger(installation.accountId) ||
|
|
705
|
+
installation.accountId <= 0 ||
|
|
706
|
+
!installation.accountLogin?.trim() ||
|
|
707
|
+
installation.suspended ||
|
|
708
|
+
ids.has(installation.installationId)
|
|
709
|
+
) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
ids.add(installation.installationId);
|
|
713
|
+
return authorityKind === "personal_owner"
|
|
714
|
+
? installation.accountType === "User"
|
|
715
|
+
: installation.accountType === "Organization";
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function consistentProof(proof: GitHubInstallationBindingProof, installationId: number): boolean {
|
|
720
|
+
const installation = proof.installation;
|
|
721
|
+
if (
|
|
722
|
+
installation.installationId !== installationId ||
|
|
723
|
+
!Number.isSafeInteger(installation.accountId) ||
|
|
724
|
+
installation.accountId <= 0 ||
|
|
725
|
+
!installation.accountLogin?.trim() ||
|
|
726
|
+
installation.suspended ||
|
|
727
|
+
!Number.isSafeInteger(proof.actorId) ||
|
|
728
|
+
proof.actorId <= 0 ||
|
|
729
|
+
!proof.actorLogin.trim() ||
|
|
730
|
+
proof.repositories.length === 0
|
|
731
|
+
) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
if (
|
|
735
|
+
proof.authorityKind === "personal_owner"
|
|
736
|
+
? installation.accountType !== "User" || proof.actorId !== installation.accountId
|
|
737
|
+
: installation.accountType !== "Organization"
|
|
738
|
+
) {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
return proof.repositories.every(
|
|
742
|
+
(repository) =>
|
|
743
|
+
Number.isSafeInteger(repository.id) &&
|
|
744
|
+
repository.id > 0 &&
|
|
745
|
+
repository.installationId === installationId &&
|
|
746
|
+
repository.accountLogin === installation.accountLogin &&
|
|
747
|
+
repository.accountType === installation.accountType,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function authorityHttpError(error: unknown): HTTPException {
|
|
752
|
+
if (error instanceof HTTPException) return error;
|
|
753
|
+
if (error instanceof GitHubInstallationAuthorityError) {
|
|
754
|
+
if (error.reason === "authority_denied") {
|
|
755
|
+
return new HTTPException(403, { message: error.message });
|
|
756
|
+
}
|
|
757
|
+
if (error.reason === "installation_missing") {
|
|
758
|
+
return new HTTPException(404, { message: error.message });
|
|
759
|
+
}
|
|
760
|
+
return new HTTPException(409, { message: error.message });
|
|
761
|
+
}
|
|
762
|
+
if (error instanceof GitHubAppConfigurationError) {
|
|
763
|
+
return new HTTPException(409, {
|
|
764
|
+
message: JSON.stringify({ message: error.message, missing: error.missing }),
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
if (error instanceof GitHubAppApiError) {
|
|
768
|
+
return new HTTPException(502, { message: error.message });
|
|
769
|
+
}
|
|
770
|
+
return new HTTPException(502, { message: "OpenGeni Lens authority verification failed" });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function positiveInteger(value: unknown): number | null {
|
|
774
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value;
|
|
775
|
+
if (typeof value === "string" && /^[1-9][0-9]*$/.test(value)) {
|
|
776
|
+
const parsed = Number(value);
|
|
777
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
783
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
784
|
+
? (value as Record<string, unknown>)
|
|
785
|
+
: null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function ignoredWebhook(reason: string) {
|
|
789
|
+
return {
|
|
790
|
+
accepted: true,
|
|
791
|
+
duplicate: false,
|
|
792
|
+
ignoredReason: reason,
|
|
793
|
+
eventId: null,
|
|
794
|
+
runIds: [],
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function githubInstallationSettingsUrl(
|
|
799
|
+
installationId: number,
|
|
800
|
+
accountLogin: string | null,
|
|
801
|
+
accountType: "User" | "Organization" | null,
|
|
802
|
+
): URL {
|
|
803
|
+
return accountType === "Organization" && accountLogin
|
|
804
|
+
? new URL(
|
|
805
|
+
`https://github.com/organizations/${encodeURIComponent(accountLogin)}/settings/installations/${installationId}`,
|
|
806
|
+
)
|
|
807
|
+
: new URL(`https://github.com/settings/installations/${installationId}`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function openGeniBaseUrl(deps: ApiRouteDeps, c: Context): string {
|
|
811
|
+
return githubBrowserBaseUrl(deps.settings, new URL(c.req.url).origin);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function escapeHtml(value: string): string {
|
|
815
|
+
return value.replace(
|
|
816
|
+
/[&<>"']/g,
|
|
817
|
+
(char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]!,
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function installationChooserHtml(
|
|
822
|
+
candidates: GitHubInstallationBindingCandidate[],
|
|
823
|
+
state: string,
|
|
824
|
+
workspaceId: string,
|
|
825
|
+
deps: ApiRouteDeps,
|
|
826
|
+
c: Context,
|
|
827
|
+
): string {
|
|
828
|
+
const action = `${openGeniBaseUrl(deps, c)}/v1/workspaces/${workspaceId}/pr-review/github/installations/select`;
|
|
829
|
+
const options = candidates
|
|
830
|
+
.map(
|
|
831
|
+
({ installation, authorityKind }) =>
|
|
832
|
+
`<label class="option"><input type="radio" name="installation_id" value="${installation.installationId}" required><span><strong>${escapeHtml(installation.accountLogin!)}</strong><small>${authorityKind === "personal_owner" ? "Personal account" : "Organization owner"}</small></span></label>`,
|
|
833
|
+
)
|
|
834
|
+
.join("");
|
|
835
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Choose GitHub account</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:12px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px}p{color:#d4d4d8}.options{display:grid;gap:8px;margin-bottom:18px}.option{display:flex;gap:12px;border:1px solid #3f3f46;border-radius:8px;padding:12px}.option span{display:grid}.option small{color:#a1a1aa}button{min-height:38px;border-radius:7px;border:1px solid #3f3f46;padding:0 14px;font-weight:600}.secondary{margin-left:8px;background:transparent;color:#f4f4f5}</style></head><body><main><h1>Connect OpenGeni Lens</h1><p>Choose an account where GitHub proved you are the owner.</p><form method="get" action="${escapeHtml(action)}"><input type="hidden" name="state" value="${escapeHtml(state)}"><div class="options">${options}</div><button type="submit">Connect selected</button><button class="secondary" type="submit" name="installation_id" value="new" formnovalidate>Install on another account</button></form></main></body></html>`;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function setupSuccessHtml(account: string, returnUrl: string): string {
|
|
839
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>OpenGeni Lens Connected</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:8px;padding:28px;background:#111114}p{color:#d4d4d8}.button{display:inline-flex;min-height:36px;align-items:center;border-radius:6px;padding:0 12px;background:#f4f4f5;color:#09090b;font-weight:600;text-decoration:none}</style></head><body><main><h1>OpenGeni Lens connected</h1><p>${escapeHtml(account)} and its selected repositories are ready for pull-request review.</p><a class="button" href="${escapeHtml(returnUrl)}">Back to OpenGeni</a></main></body></html>`;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function setupPendingHtml(): string {
|
|
843
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>OpenGeni Lens Requested</title></head><body><main><h1>Installation requested</h1><p>A GitHub organization owner must approve OpenGeni Lens. No repository was connected yet.</p></main></body></html>`;
|
|
844
|
+
}
|