@opengeni/api-router 2.11.3 → 2.12.3-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.
Files changed (66) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-5X3XM6CE.js → chunk-4AQMKVQM.js} +17727 -14427
  3. package/dist/chunk-4AQMKVQM.js.map +1 -0
  4. package/dist/connection-ownership.d.ts +5 -1
  5. package/dist/index.js +3 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/integrations/atlassian.d.ts +4 -0
  8. package/dist/integrations/connect-authority.d.ts +1 -0
  9. package/dist/integrations/connect-callback-return.d.ts +6 -0
  10. package/dist/integrations/fiken.d.ts +7 -0
  11. package/dist/integrations/github-app-connect.d.ts +47 -0
  12. package/dist/integrations/github-installation-proof.d.ts +3 -0
  13. package/dist/integrations/github-lens-connect.d.ts +153 -0
  14. package/dist/integrations/google-drive.d.ts +4 -0
  15. package/dist/integrations/oauth-client.d.ts +4 -0
  16. package/dist/integrations/oauth-return-path.d.ts +1 -0
  17. package/dist/integrations/personal-github.d.ts +2 -0
  18. package/dist/integrations/provider-oauth.d.ts +11 -1
  19. package/dist/integrations/slack-install.d.ts +20 -0
  20. package/dist/integrations/social-oauth.d.ts +2 -0
  21. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  22. package/dist/routes/api-integrations.d.ts +14 -0
  23. package/dist/routes/connect.d.ts +5 -0
  24. package/dist/routes/external-identity-links.d.ts +6 -0
  25. package/dist/routes/host-mcp-bindings.d.ts +5 -0
  26. package/dist/sandbox/rematerialize.d.ts +1 -1
  27. package/dist/workspace-tool-gateway.d.ts +1 -0
  28. package/package.json +19 -19
  29. package/src/app.ts +22 -2
  30. package/src/connection-ownership.ts +14 -1
  31. package/src/index.ts +2 -0
  32. package/src/integrations/atlassian.ts +143 -39
  33. package/src/integrations/connect-authority.ts +2 -0
  34. package/src/integrations/connect-callback-return.ts +86 -0
  35. package/src/integrations/fiken.ts +222 -40
  36. package/src/integrations/github-app-connect.ts +389 -0
  37. package/src/integrations/github-installation-proof.ts +60 -0
  38. package/src/integrations/github-lens-connect.ts +97 -0
  39. package/src/integrations/google-drive.ts +152 -47
  40. package/src/integrations/oauth-client.ts +180 -84
  41. package/src/integrations/oauth-return-path.ts +18 -0
  42. package/src/integrations/personal-github.ts +146 -52
  43. package/src/integrations/provider-oauth.ts +132 -21
  44. package/src/integrations/slack-install.ts +83 -0
  45. package/src/integrations/social-oauth.ts +142 -1
  46. package/src/mcp/documents.ts +1 -1
  47. package/src/mcp/server.ts +64 -17
  48. package/src/routes/api-integrations.ts +74 -64
  49. package/src/routes/connect.ts +1869 -0
  50. package/src/routes/connections.ts +263 -221
  51. package/src/routes/documents.ts +2 -2
  52. package/src/routes/external-identity-links.ts +200 -0
  53. package/src/routes/github.ts +39 -64
  54. package/src/routes/host-mcp-bindings.ts +122 -0
  55. package/src/routes/organization-memberships.ts +23 -0
  56. package/src/routes/organization-sessions.ts +2 -2
  57. package/src/routes/personal-github.ts +8 -1
  58. package/src/routes/pr-review-github.ts +30 -3
  59. package/src/routes/scheduled-tasks.ts +29 -4
  60. package/src/routes/sessions.ts +61 -32
  61. package/src/routes/social.ts +5 -1
  62. package/src/routes/supergrok.ts +90 -3
  63. package/src/routes/workspaces.ts +16 -0
  64. package/src/sandbox/rematerialize.ts +33 -1
  65. package/src/workspace-tool-gateway.ts +136 -24
  66. package/dist/chunk-5X3XM6CE.js.map +0 -1
@@ -0,0 +1,389 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { HTTPException } from "hono/http-exception";
4
+ import type { ConnectAdvance, ConnectAttempt } from "@opengeni/contracts/connect";
5
+ import {
6
+ bindAuthorizedGitHubInstallationRepositories,
7
+ claimConnectOperation,
8
+ finishConnectOperation,
9
+ getConnectAttempt,
10
+ type ConnectActorScope,
11
+ type ConnectOperationAuthorization,
12
+ } from "@opengeni/db";
13
+ import {
14
+ authorizeGitHubInstallationBinding,
15
+ createSignedState,
16
+ discoverGitHubInstallationBindingCandidates,
17
+ githubAppMissingSettings,
18
+ githubOAuthAuthorizeUrl,
19
+ readSignedState,
20
+ prReviewGitHubAppMissingSettings,
21
+ settingsForPrReviewGitHubApp,
22
+ } from "@opengeni/github";
23
+ import { commitGitHubLensConnect, requireGitHubLensConnect } from "./github-lens-connect";
24
+ import type { ApiRouteDeps, PreparedConnectOperation } from "@opengeni/core";
25
+ import { requireConnectOwnerAuthority } from "./connect-authority";
26
+ import { integrationBaseUrl } from "./oauth-client";
27
+ import {
28
+ isConsistentGitHubBindingCandidates,
29
+ isConsistentGitHubBindingProof,
30
+ } from "./github-installation-proof";
31
+
32
+ const stateSchema = z.object({
33
+ kind: z.literal("github_app_connect"),
34
+ accountId: z.string().uuid(),
35
+ workspaceId: z.string().uuid(),
36
+ subjectId: z.string().min(1),
37
+ personalOwnerVerified: z.boolean(),
38
+ connectAttemptId: z.string().uuid(),
39
+ phase: z.enum(["discover", "install", "bind"]),
40
+ installationId: z.number().int().positive().safe().optional(),
41
+ providerId: z.enum(["github-app", "github-lens"]).default("github-app"),
42
+ nonce: z.string().min(1),
43
+ iat: z.number().int(),
44
+ });
45
+ type State = z.infer<typeof stateSchema>;
46
+ const lifetimeMs = 10 * 60_000;
47
+
48
+ export function isGitHubAppConnectState(deps: ApiRouteDeps, raw: string | undefined): boolean {
49
+ return !!raw && readSignedState(raw, deps.githubStateSecret)?.kind === "github_app_connect";
50
+ }
51
+
52
+ type GitHubConnectScope = ConnectActorScope & {
53
+ personalOwnerVerified?: boolean;
54
+ providerId?: "github-app" | "github-lens";
55
+ };
56
+ function callbackAuthority(scope: GitHubConnectScope): ConnectOperationAuthorization {
57
+ return async (tx, _attempt, origin) => {
58
+ const actor = { ...scope, ...(origin ? { externalContinuation: origin } : {}) };
59
+ await requireConnectOwnerAuthority(
60
+ tx,
61
+ actor,
62
+ scope.providerId === "github-lens" ? "workspace:admin" : "github:manage",
63
+ origin,
64
+ );
65
+ if (scope.providerId === "github-lens")
66
+ await requireConnectOwnerAuthority(tx, actor, "secrets:write", origin);
67
+ };
68
+ }
69
+
70
+ export function githubAppConnectNavigation(
71
+ deps: ApiRouteDeps,
72
+ scope: GitHubConnectScope,
73
+ attemptId: string,
74
+ requestUrl: string,
75
+ phase: State["phase"] = "discover",
76
+ installationId?: number,
77
+ providerId: State["providerId"] = "github-app",
78
+ ) {
79
+ const settings =
80
+ providerId === "github-lens" ? settingsForPrReviewGitHubApp(deps.settings) : deps.settings;
81
+ const missing =
82
+ providerId === "github-lens"
83
+ ? prReviewGitHubAppMissingSettings(deps.settings)
84
+ : githubAppMissingSettings(settings);
85
+ if (missing.length || !settings.githubAppSlug?.trim())
86
+ throw new HTTPException(503, { message: "GitHub App is not configured" });
87
+ const state = createSignedState(deps.githubStateSecret, {
88
+ kind: "github_app_connect",
89
+ accountId: scope.accountId,
90
+ workspaceId: scope.workspaceId,
91
+ subjectId: scope.subjectId,
92
+ personalOwnerVerified: scope.personalOwnerVerified === true,
93
+ connectAttemptId: attemptId,
94
+ phase,
95
+ providerId,
96
+ ...(installationId ? { installationId } : {}),
97
+ });
98
+ const url =
99
+ phase === "install"
100
+ ? `https://github.com/apps/${encodeURIComponent(settings.githubAppSlug.trim())}/installations/new?state=${encodeURIComponent(state)}`
101
+ : githubOAuthAuthorizeUrl({
102
+ clientId: settings.githubClientId!.trim(),
103
+ state,
104
+ redirectUri: `${integrationBaseUrl(deps.settings.publicBaseUrl, requestUrl)}${providerId === "github-lens" ? "/v1/pr-review/github" : "/v1/github"}/oauth/callback`,
105
+ });
106
+ return { authorizationUrl: url, expiresAt: new Date(Date.now() + lifetimeMs).toISOString() };
107
+ }
108
+
109
+ export function prepareGitHubAppConnectAction(
110
+ deps: ApiRouteDeps,
111
+ scope: GitHubConnectScope,
112
+ requestUrl: string,
113
+ attempt: ConnectAttempt,
114
+ action: ConnectAdvance,
115
+ ): PreparedConnectOperation {
116
+ let phase: State["phase"] = "discover";
117
+ let installationId: number | undefined;
118
+ if (action.type === "account") {
119
+ if (
120
+ attempt.nextAction.type !== "select_account" ||
121
+ !attempt.nextAction.accounts.some((account) => account.id === action.accountId)
122
+ )
123
+ throw new HTTPException(409, {
124
+ message: "Choose an installation from the current discovery",
125
+ });
126
+ if (action.accountId === "new") phase = "install";
127
+ else {
128
+ phase = "bind";
129
+ installationId = z.coerce.number().int().positive().safe().parse(action.accountId);
130
+ }
131
+ } else if (action.type !== "retry" || attempt.state !== "connected_but_incomplete") {
132
+ throw new HTTPException(422, {
133
+ message: "Choose an installation or retry after owner approval",
134
+ });
135
+ }
136
+ const navigation = githubAppConnectNavigation(
137
+ deps,
138
+ scope,
139
+ attempt.id,
140
+ requestUrl,
141
+ phase,
142
+ installationId,
143
+ attempt.providerId === "github-lens" ? "github-lens" : "github-app",
144
+ );
145
+ return {
146
+ commit: async (_tx, current) => ({
147
+ ...current,
148
+ error: undefined,
149
+ revision: current.revision + 1,
150
+ state: "requires_user_action",
151
+ nextAction: { type: "authorize", url: navigation.authorizationUrl },
152
+ }),
153
+ };
154
+ }
155
+
156
+ /** No browser login/cookie required: signed attempt + current stored origin,
157
+ * then fresh GitHub owner proof, are the two separate authority boundaries. */
158
+ export async function completeGitHubAppConnect(
159
+ deps: ApiRouteDeps,
160
+ input: {
161
+ state?: string | undefined;
162
+ code?: string | undefined;
163
+ installationId?: string | undefined;
164
+ setupAction?: string | undefined;
165
+ error?: string | undefined;
166
+ requestUrl: string;
167
+ expectedProvider?: "github-app" | "github-lens";
168
+ },
169
+ ): Promise<Response> {
170
+ let destination: string | undefined;
171
+ try {
172
+ const state = stateSchema.parse(readSignedState(input.state ?? "", deps.githubStateSecret));
173
+ if (state.providerId !== (input.expectedProvider ?? "github-app"))
174
+ throw new Error("GitHub callback provider mismatch");
175
+ const age = Date.now() - state.iat * 1000;
176
+ if (age < 0 || age >= lifetimeMs) throw new Error("Expired GitHub connection state");
177
+ const stored = await getConnectAttempt(deps.db, state, state.connectAttemptId);
178
+ if (stored.attempt.providerId !== state.providerId || stored.attempt.ownership !== "workspace")
179
+ throw new Error("GitHub attempt mismatch");
180
+ destination = stored.returnUrl;
181
+ // Reject obsolete browser stages before committing an operation claim.
182
+ // Otherwise a valid older callback could strand the current stage in-flight.
183
+ // Completed callbacks still navigate home without repeating provider work.
184
+ if (
185
+ stored.attempt.nextAction.type !== "authorize" ||
186
+ new URL(stored.attempt.nextAction.url).searchParams.get("state") !== input.state
187
+ )
188
+ throw new Error("Stale GitHub setup stage");
189
+ if (state.providerId === "github-lens") await requireGitHubLensConnect(deps, state.workspaceId);
190
+ const authorize = callbackAuthority(state);
191
+ const operation = {
192
+ attemptId: state.connectAttemptId,
193
+ operationId: `github:${state.nonce}`,
194
+ inputDigest: createHash("sha256").update(input.state!).digest("hex"),
195
+ authorize,
196
+ };
197
+ const claim = await claimConnectOperation(deps.db, state, {
198
+ ...operation,
199
+ expectedRevision: stored.attempt.revision,
200
+ });
201
+ if (claim.status !== "replayed") {
202
+ const provider =
203
+ state.providerId === "github-lens" ? deps.prReviewGithubAppApi : deps.githubAppApi;
204
+ const settings =
205
+ state.providerId === "github-lens"
206
+ ? settingsForPrReviewGitHubApp(deps.settings)
207
+ : deps.settings;
208
+ let prepared: PreparedConnectOperation;
209
+ if (input.error || (state.phase !== "install" && !input.code)) {
210
+ prepared = {
211
+ commit: async (_tx, current) => ({
212
+ ...current,
213
+ revision: current.revision + 1,
214
+ state: input.error === "access_denied" ? "cancelled" : "failed",
215
+ nextAction: { type: "none" },
216
+ error: {
217
+ code: "authorization_not_completed",
218
+ message: "Authorization was not completed. Start a new attempt.",
219
+ retryable: false,
220
+ },
221
+ }),
222
+ };
223
+ } else if (state.phase === "install") {
224
+ if (input.setupAction === "request")
225
+ prepared = {
226
+ commit: async (_tx, current) => ({
227
+ ...current,
228
+ revision: current.revision + 1,
229
+ state: "connected_but_incomplete",
230
+ nextAction: { type: "none" },
231
+ error: {
232
+ code: "owner_approval_pending",
233
+ message:
234
+ "A GitHub organization owner must approve installation. Retry discovery after approval.",
235
+ retryable: true,
236
+ },
237
+ }),
238
+ };
239
+ else {
240
+ const installationId = z.coerce
241
+ .number()
242
+ .int()
243
+ .positive()
244
+ .safe()
245
+ .parse(input.installationId);
246
+ const navigation = githubAppConnectNavigation(
247
+ deps,
248
+ state,
249
+ state.connectAttemptId,
250
+ input.requestUrl,
251
+ "bind",
252
+ installationId,
253
+ state.providerId,
254
+ );
255
+ prepared = {
256
+ commit: async (_tx, current) => ({
257
+ ...current,
258
+ revision: current.revision + 1,
259
+ state: "requires_user_action",
260
+ nextAction: { type: "authorize", url: navigation.authorizationUrl },
261
+ }),
262
+ };
263
+ }
264
+ } else if (state.phase === "discover") {
265
+ const candidates = provider
266
+ ? await provider.discoverInstallationBindingCandidates?.({ code: input.code! })
267
+ : await discoverGitHubInstallationBindingCandidates(settings, { code: input.code! });
268
+ if (!candidates || !isConsistentGitHubBindingCandidates(candidates))
269
+ throw new Error("Provider cannot prove installation candidates");
270
+ prepared =
271
+ candidates.length > 99
272
+ ? {
273
+ commit: async (_tx, current) => ({
274
+ ...current,
275
+ revision: current.revision + 1,
276
+ state: "failed",
277
+ nextAction: { type: "none" },
278
+ error: {
279
+ code: "installation_selection_limit",
280
+ message:
281
+ "This account exceeds the 99-installation Connect chooser limit; no installations were omitted or bound.",
282
+ retryable: false,
283
+ },
284
+ }),
285
+ }
286
+ : {
287
+ commit: async (_tx, current) => ({
288
+ ...current,
289
+ revision: current.revision + 1,
290
+ state: "account_selection",
291
+ nextAction: {
292
+ type: "select_account",
293
+ accounts: [
294
+ ...candidates.map(({ installation }) => ({
295
+ id: String(installation.installationId),
296
+ providerId: state.providerId,
297
+ label: installation.accountLogin!,
298
+ ownership: "workspace" as const,
299
+ status: "connected" as const,
300
+ })),
301
+ {
302
+ id: "new",
303
+ providerId: state.providerId,
304
+ label: "Install on another GitHub account",
305
+ ownership: "workspace",
306
+ status: "connected",
307
+ },
308
+ ],
309
+ },
310
+ }),
311
+ };
312
+ } else {
313
+ const installationId = z.number().int().positive().safe().parse(state.installationId);
314
+ const proof = provider
315
+ ? await provider.authorizeInstallationBinding?.({ code: input.code!, installationId })
316
+ : await authorizeGitHubInstallationBinding(settings, {
317
+ code: input.code!,
318
+ installationId,
319
+ });
320
+ if (!proof || !isConsistentGitHubBindingProof(proof, installationId))
321
+ throw new Error("GitHub owner proof unavailable");
322
+ const checkedAt = new Date();
323
+ prepared = {
324
+ commit: async (tx, current) => {
325
+ if (state.providerId === "github-lens") {
326
+ const registration = await commitGitHubLensConnect(deps, tx, {
327
+ ...state,
328
+ installationId,
329
+ proof,
330
+ checkedAt,
331
+ expiresAt: new Date(state.iat * 1000 + lifetimeMs),
332
+ nonce: state.nonce,
333
+ });
334
+ return {
335
+ ...current,
336
+ revision: current.revision + 1,
337
+ state: "complete",
338
+ nextAction: { type: "none" },
339
+ account: {
340
+ id: `lens-registration:${registration.id}`,
341
+ providerId: "github-lens",
342
+ label: proof.installation.accountLogin!,
343
+ ownership: "workspace",
344
+ status: "connected",
345
+ },
346
+ };
347
+ }
348
+ const bound = await bindAuthorizedGitHubInstallationRepositories(tx, {
349
+ accountId: state.accountId,
350
+ workspaceId: state.workspaceId,
351
+ installationId,
352
+ githubAccountId: proof.installation.accountId,
353
+ accountLogin: proof.installation.accountLogin,
354
+ accountType: proof.installation.accountType,
355
+ linkedBySubjectId: state.subjectId,
356
+ githubActorId: proof.actorId,
357
+ githubActorLogin: proof.actorLogin,
358
+ authorityKind: proof.authorityKind,
359
+ authorityCheckedAt: checkedAt,
360
+ authorityExpiresAt: new Date(state.iat * 1000 + lifetimeMs),
361
+ authorityNonce: state.nonce,
362
+ repositoryIds: proof.repositories.map((repo) => repo.id),
363
+ });
364
+ if (!bound) throw new Error("GitHub binding proof already consumed");
365
+ return {
366
+ ...current,
367
+ revision: current.revision + 1,
368
+ state: "complete",
369
+ nextAction: { type: "none" },
370
+ account: {
371
+ id: `github-installation:${installationId}`,
372
+ providerId: "github-app",
373
+ label: proof.installation.accountLogin!,
374
+ ownership: "workspace",
375
+ status: "connected",
376
+ },
377
+ };
378
+ },
379
+ };
380
+ }
381
+ await finishConnectOperation(deps.db, state, { ...operation, commit: prepared.commit });
382
+ }
383
+ } catch {
384
+ // Preserve unknown outcomes; never replay provider authorization to recover.
385
+ if (!destination)
386
+ return Response.json({ error: "Connection callback is invalid or expired" }, { status: 400 });
387
+ }
388
+ return new Response(null, { status: 302, headers: { Location: destination! } });
389
+ }
@@ -0,0 +1,60 @@
1
+ import type {
2
+ GitHubInstallationBindingCandidate,
3
+ GitHubInstallationBindingProof,
4
+ } from "@opengeni/contracts";
5
+
6
+ export function isConsistentGitHubBindingCandidates(
7
+ candidates: GitHubInstallationBindingCandidate[],
8
+ ): boolean {
9
+ const ids = new Set<number>();
10
+ return candidates.every(({ installation, authorityKind }) => {
11
+ if (
12
+ !Number.isSafeInteger(installation.installationId) ||
13
+ installation.installationId <= 0 ||
14
+ !Number.isSafeInteger(installation.accountId) ||
15
+ installation.accountId <= 0 ||
16
+ !installation.accountLogin?.trim() ||
17
+ installation.suspended ||
18
+ ids.has(installation.installationId)
19
+ )
20
+ return false;
21
+ ids.add(installation.installationId);
22
+ return authorityKind === "personal_owner"
23
+ ? installation.accountType === "User"
24
+ : authorityKind === "organization_owner" && installation.accountType === "Organization";
25
+ });
26
+ }
27
+
28
+ export function isConsistentGitHubBindingProof(
29
+ proof: GitHubInstallationBindingProof,
30
+ installationId: number,
31
+ ): boolean {
32
+ const installation = proof.installation;
33
+ if (
34
+ installation.installationId !== installationId ||
35
+ !Number.isSafeInteger(installation.accountId) ||
36
+ installation.accountId <= 0 ||
37
+ !installation.accountLogin?.trim() ||
38
+ installation.suspended ||
39
+ !Number.isSafeInteger(proof.actorId) ||
40
+ proof.actorId <= 0 ||
41
+ !proof.actorLogin.trim() ||
42
+ proof.repositories.length === 0 ||
43
+ new Set(proof.repositories.map((repo) => repo.id)).size !== proof.repositories.length
44
+ )
45
+ return false;
46
+ if (
47
+ proof.authorityKind === "personal_owner"
48
+ ? installation.accountType !== "User" || proof.actorId !== installation.accountId
49
+ : proof.authorityKind !== "organization_owner" || installation.accountType !== "Organization"
50
+ )
51
+ return false;
52
+ return proof.repositories.every(
53
+ (repo) =>
54
+ Number.isSafeInteger(repo.id) &&
55
+ repo.id > 0 &&
56
+ repo.installationId === installationId &&
57
+ repo.accountLogin === installation.accountLogin &&
58
+ repo.accountType === installation.accountType,
59
+ );
60
+ }
@@ -0,0 +1,97 @@
1
+ import { environmentsEncryptionKeyBytes } from "@opengeni/config";
2
+ import {
3
+ OPENGENI_PR_REVIEW_PACK_ID,
4
+ type GitHubInstallationBindingProof,
5
+ } from "@opengeni/contracts";
6
+ import {
7
+ getCapabilityPack,
8
+ PR_REVIEW_AUTOMATION_TEMPLATE_ID,
9
+ prReviewPackConnectorId,
10
+ type ApiRouteDeps,
11
+ } from "@opengeni/core";
12
+ import {
13
+ encryptVariableSetValue,
14
+ getPackInstallation,
15
+ recordAuditEvent,
16
+ syncManagedGitHubPrReviewInstallation,
17
+ type Database,
18
+ } from "@opengeni/db";
19
+ import { HTTPException } from "hono/http-exception";
20
+
21
+ export async function requireGitHubLensConnect(deps: ApiRouteDeps, workspaceId: string) {
22
+ if (deps.settings.sandboxBackend === "selfhosted")
23
+ throw new HTTPException(409, { message: "OpenGeni Lens requires managed compute" });
24
+ const installation = await getPackInstallation(deps.db, workspaceId, OPENGENI_PR_REVIEW_PACK_ID);
25
+ if (installation?.status !== "active")
26
+ throw new HTTPException(409, { message: "Install and enable the Review Bot Pack first" });
27
+ const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
28
+ (value) => value.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
29
+ );
30
+ if (!template || !environmentsEncryptionKeyBytes(deps.settings))
31
+ throw new HTTPException(503, { message: "Lens template or secret encryption is unavailable" });
32
+ return { installation, template };
33
+ }
34
+
35
+ /** Separate Lens registration/source/automation domain, not a repository-access
36
+ * binding. Invoked inside the Connect receipt's authorized transaction. */
37
+ export async function commitGitHubLensConnect(
38
+ deps: ApiRouteDeps,
39
+ tx: Database,
40
+ input: {
41
+ accountId: string;
42
+ workspaceId: string;
43
+ subjectId: string;
44
+ installationId: number;
45
+ proof: GitHubInstallationBindingProof;
46
+ checkedAt: Date;
47
+ expiresAt: Date;
48
+ nonce: string;
49
+ },
50
+ ) {
51
+ const { installation, template } = await requireGitHubLensConnect(
52
+ { ...deps, db: tx },
53
+ input.workspaceId,
54
+ );
55
+ const synchronized = await syncManagedGitHubPrReviewInstallation(tx, {
56
+ accountId: input.accountId,
57
+ workspaceId: input.workspaceId,
58
+ installationId: input.installationId,
59
+ providerAccountLogin: input.proof.installation.accountLogin,
60
+ providerAccountType: input.proof.installation.accountType as "User" | "Organization",
61
+ githubActorId: input.proof.actorId,
62
+ authorityKind: input.proof.authorityKind,
63
+ authorityCheckedAt: input.checkedAt,
64
+ authorityExpiresAt: input.expiresAt,
65
+ authorityNonce: input.nonce,
66
+ appId: deps.settings.prReviewGithubAppId!,
67
+ webhookSecretEncrypted: encryptVariableSetValue(
68
+ environmentsEncryptionKeyBytes(deps.settings)!,
69
+ deps.settings.prReviewGithubWebhookSecret!,
70
+ ),
71
+ repositories: input.proof.repositories,
72
+ createdBySubjectId: input.subjectId,
73
+ packInstallationId: installation.id,
74
+ packConnectorId: prReviewPackConnectorId("github"),
75
+ packTemplateId: template.id,
76
+ adapterId: template.adapterId,
77
+ eventTypes: template.eventTypes,
78
+ configuration: template.configuration,
79
+ sessionTemplate: template.sessionTemplate,
80
+ });
81
+ await recordAuditEvent(tx, {
82
+ accountId: input.accountId,
83
+ workspaceId: input.workspaceId,
84
+ subjectId: input.subjectId,
85
+ action: "prReview.managed_github.connected",
86
+ targetType: "pr_review_app_registration",
87
+ targetId: synchronized.registration.id,
88
+ metadata: {
89
+ installationId: input.installationId,
90
+ providerAccountLogin: input.proof.installation.accountLogin,
91
+ repositoryCount: synchronized.repositories.length,
92
+ authorityKind: input.proof.authorityKind,
93
+ githubActorId: input.proof.actorId,
94
+ },
95
+ });
96
+ return synchronized.registration;
97
+ }