@opengeni/api-router 0.30.3 → 2.2.0-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 (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -0,0 +1,531 @@
1
+ import { environmentsEncryptionKeyBytes } from "@opengeni/config";
2
+ import {
3
+ CreatePrReviewAppRegistrationRequest,
4
+ CreatePrReviewRepositoryBindingRequest,
5
+ PrReviewAppRegistration,
6
+ PrReviewRepositoryBinding,
7
+ OPENGENI_PR_REVIEW_PACK_ID,
8
+ UpdatePrReviewAppRegistrationRequest,
9
+ UpdatePrReviewRepositoryBindingRequest,
10
+ } from "@opengeni/contracts";
11
+ import {
12
+ assertWorkspaceModelPolicyAllows,
13
+ canonicalConfiguredModel,
14
+ defaultPrReviewProviderBaseUrl,
15
+ getCapabilityPack,
16
+ prReviewWebhookAuthKind,
17
+ normalizePrReviewProviderBaseUrl,
18
+ prReviewPackConnectorId,
19
+ PR_REVIEW_AUTOMATION_TEMPLATE_ID,
20
+ requireAccessGrant,
21
+ requirePermission,
22
+ type ApiRouteDeps,
23
+ } from "@opengeni/core";
24
+ import {
25
+ createPrReviewAppRegistration,
26
+ createPrReviewRepositoryBinding,
27
+ deletePrReviewAppRegistration,
28
+ deletePrReviewRepositoryBinding,
29
+ decryptVariableSetValue,
30
+ encryptVariableSetValue,
31
+ getPrReviewAppRegistrationSecret,
32
+ getPackInstallation,
33
+ listPrReviewAppRegistrations,
34
+ listPrReviewRepositoryBindings,
35
+ nestedPostgresSqlState,
36
+ recordAuditEvent,
37
+ updatePrReviewAppRegistration,
38
+ updatePrReviewRepositoryBinding,
39
+ } from "@opengeni/db";
40
+ import { listGitHubAppRepositoriesWithSigningSettings } from "@opengeni/github";
41
+ import type { Hono } from "hono";
42
+ import { HTTPException } from "hono/http-exception";
43
+ import {
44
+ PrReviewProviderRepositoryError,
45
+ verifyPrReviewProviderRepository,
46
+ } from "../integrations/pr-review-provider";
47
+
48
+ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
49
+ const { db, settings } = deps;
50
+
51
+ app.get("/v1/workspaces/:workspaceId/pr-review/registrations", async (c) => {
52
+ const workspaceId = c.req.param("workspaceId");
53
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
54
+ await requirePrReviewPackActive(db, workspaceId);
55
+ return c.json({
56
+ registrations: await listPrReviewAppRegistrations(db, grant.accountId, workspaceId),
57
+ repositories: await listPrReviewRepositoryBindings(db, grant.accountId, workspaceId),
58
+ });
59
+ });
60
+
61
+ app.post("/v1/workspaces/:workspaceId/pr-review/registrations", async (c) => {
62
+ const workspaceId = c.req.param("workspaceId");
63
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
64
+ requirePermission(grant, "secrets:write");
65
+ const packInstallation = await requirePrReviewPackActive(db, workspaceId);
66
+ assertPrReviewSandboxBackend(deps);
67
+ const payload = CreatePrReviewAppRegistrationRequest.parse(await c.req.json());
68
+ const encryptionKey = requirePrReviewEncryptionKey(deps);
69
+ let providerBaseUrl: string;
70
+ try {
71
+ providerBaseUrl = normalizePrReviewProviderBaseUrl(
72
+ payload.provider,
73
+ payload.providerBaseUrl ?? defaultPrReviewProviderBaseUrl(payload.provider),
74
+ );
75
+ } catch (error) {
76
+ throw new HTTPException(422, {
77
+ message:
78
+ error instanceof Error
79
+ ? error.message
80
+ : "PR Review providerBaseUrl must be a credential-free HTTPS origin or path",
81
+ });
82
+ }
83
+ if (
84
+ payload.provider === "azure_devops" &&
85
+ new URL(providerBaseUrl).pathname.split("/").filter(Boolean).length === 0
86
+ ) {
87
+ throw new HTTPException(422, {
88
+ message: "Azure DevOps PR Review providerBaseUrl must include the organization path",
89
+ });
90
+ }
91
+ const registration = await mapPrReviewUniqueConflict(
92
+ createPrReviewAppRegistration(db, {
93
+ accountId: grant.accountId,
94
+ workspaceId,
95
+ name: payload.name,
96
+ provider: payload.provider,
97
+ providerBaseUrl,
98
+ appId: payload.appId ?? null,
99
+ credentialKind: payload.credentialKind,
100
+ credentialEncrypted: encryptVariableSetValue(
101
+ encryptionKey,
102
+ payload.privateKey ?? payload.accessToken!,
103
+ ),
104
+ accessTokenExpiresAt: payload.accessTokenExpiresAt
105
+ ? new Date(payload.accessTokenExpiresAt)
106
+ : null,
107
+ webhookAuthKind: prReviewWebhookAuthKind(payload.provider),
108
+ webhookSecretEncrypted: encryptVariableSetValue(encryptionKey, payload.webhookSecret),
109
+ webhookUsername: payload.webhookUsername ?? null,
110
+ createdBySubjectId: grant.subjectId,
111
+ packInstallationId: packInstallation.id,
112
+ packConnectorId: prReviewPackConnectorId(payload.provider),
113
+ }),
114
+ "A PR Review registration with this provider and name already exists",
115
+ );
116
+ await recordAuditEvent(db, {
117
+ accountId: grant.accountId,
118
+ workspaceId,
119
+ subjectId: grant.subjectId,
120
+ action: "prReview.app_registration.created",
121
+ targetType: "pr_review_app_registration",
122
+ targetId: registration.id,
123
+ metadata: {
124
+ provider: registration.provider,
125
+ credentialKind: registration.credentialKind,
126
+ webhookAuthKind: registration.webhookAuthKind,
127
+ },
128
+ });
129
+ return c.json(PrReviewAppRegistration.parse(registration), 201);
130
+ });
131
+
132
+ app.patch("/v1/workspaces/:workspaceId/pr-review/registrations/:registrationId", async (c) => {
133
+ const workspaceId = c.req.param("workspaceId");
134
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
135
+ requirePermission(grant, "secrets:write");
136
+ await requirePrReviewPackActive(db, workspaceId);
137
+ const payload = UpdatePrReviewAppRegistrationRequest.parse(await c.req.json());
138
+ const existing = await getPrReviewAppRegistrationSecret(db, {
139
+ accountId: grant.accountId,
140
+ workspaceId,
141
+ registrationId: c.req.param("registrationId"),
142
+ });
143
+ if (!existing)
144
+ throw new HTTPException(404, {
145
+ message: "PR Review app registration not found",
146
+ });
147
+ if (payload.accessToken && existing.credentialKind !== "provider_token") {
148
+ throw new HTTPException(422, {
149
+ message: "GitHub App registrations accept private keys, not access tokens",
150
+ });
151
+ }
152
+ if (payload.privateKey && existing.credentialKind !== "github_app") {
153
+ throw new HTTPException(422, {
154
+ message: "Provider-token registrations do not accept GitHub private keys",
155
+ });
156
+ }
157
+ if (
158
+ payload.accessTokenExpiresAt !== undefined &&
159
+ existing.credentialKind !== "provider_token"
160
+ ) {
161
+ throw new HTTPException(422, {
162
+ message: "GitHub App registrations do not accept access-token expiry",
163
+ });
164
+ }
165
+ const encryptionKey =
166
+ payload.privateKey || payload.accessToken || payload.webhookSecret
167
+ ? requirePrReviewEncryptionKey(deps)
168
+ : null;
169
+ const updated = await mapPrReviewUniqueConflict(
170
+ updatePrReviewAppRegistration(db, {
171
+ accountId: grant.accountId,
172
+ workspaceId,
173
+ registrationId: existing.id,
174
+ ...(payload.name !== undefined ? { name: payload.name } : {}),
175
+ ...(payload.accessToken !== undefined
176
+ ? {
177
+ credentialEncrypted: encryptVariableSetValue(encryptionKey!, payload.accessToken),
178
+ }
179
+ : {}),
180
+ ...(payload.privateKey !== undefined
181
+ ? {
182
+ credentialEncrypted: encryptVariableSetValue(encryptionKey!, payload.privateKey),
183
+ }
184
+ : {}),
185
+ ...(payload.accessTokenExpiresAt !== undefined
186
+ ? {
187
+ accessTokenExpiresAt: payload.accessTokenExpiresAt
188
+ ? new Date(payload.accessTokenExpiresAt)
189
+ : null,
190
+ }
191
+ : {}),
192
+ ...(payload.webhookSecret !== undefined
193
+ ? {
194
+ webhookSecretEncrypted: encryptVariableSetValue(
195
+ encryptionKey!,
196
+ payload.webhookSecret,
197
+ ),
198
+ }
199
+ : {}),
200
+ ...(payload.webhookUsername !== undefined
201
+ ? { webhookUsername: payload.webhookUsername }
202
+ : {}),
203
+ ...(payload.status !== undefined ? { status: payload.status } : {}),
204
+ }),
205
+ "A PR Review registration with this provider and name already exists",
206
+ );
207
+ await recordAuditEvent(db, {
208
+ accountId: grant.accountId,
209
+ workspaceId,
210
+ subjectId: grant.subjectId,
211
+ action: "prReview.app_registration.updated",
212
+ targetType: "pr_review_app_registration",
213
+ targetId: existing.id,
214
+ metadata: {
215
+ fields: Object.keys(payload).filter(
216
+ (field) => field !== "accessToken" && field !== "privateKey" && field !== "webhookSecret",
217
+ ),
218
+ credentialRotated: payload.accessToken !== undefined || payload.privateKey !== undefined,
219
+ webhookSecretRotated: payload.webhookSecret !== undefined,
220
+ },
221
+ });
222
+ return c.json(PrReviewAppRegistration.parse(updated));
223
+ });
224
+
225
+ app.delete("/v1/workspaces/:workspaceId/pr-review/registrations/:registrationId", async (c) => {
226
+ const workspaceId = c.req.param("workspaceId");
227
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
228
+ requirePermission(grant, "secrets:write");
229
+ const deleted = await deletePrReviewAppRegistration(db, {
230
+ accountId: grant.accountId,
231
+ workspaceId,
232
+ registrationId: c.req.param("registrationId"),
233
+ });
234
+ if (!deleted)
235
+ throw new HTTPException(404, {
236
+ message: "PR Review app registration not found",
237
+ });
238
+ await recordAuditEvent(db, {
239
+ accountId: grant.accountId,
240
+ workspaceId,
241
+ subjectId: grant.subjectId,
242
+ action: "prReview.app_registration.disabled",
243
+ targetType: "pr_review_app_registration",
244
+ targetId: c.req.param("registrationId"),
245
+ metadata: {},
246
+ });
247
+ return c.body(null, 204);
248
+ });
249
+
250
+ app.post("/v1/workspaces/:workspaceId/pr-review/repositories", async (c) => {
251
+ const workspaceId = c.req.param("workspaceId");
252
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
253
+ const packInstallation = await requirePrReviewPackActive(db, workspaceId);
254
+ const payload = CreatePrReviewRepositoryBindingRequest.parse(await c.req.json());
255
+ const registration = await getPrReviewAppRegistrationSecret(db, {
256
+ accountId: grant.accountId,
257
+ workspaceId,
258
+ registrationId: payload.registrationId,
259
+ });
260
+ if (!registration)
261
+ throw new HTTPException(404, {
262
+ message: "PR Review app registration not found",
263
+ });
264
+ const repositoryUrl = new URL(payload.repositoryUri);
265
+ let canonicalRepositoryUri = repositoryUrl.href;
266
+ let canonicalRepositoryFullName = payload.repositoryFullName;
267
+ let canonicalProviderRepositoryId = String(payload.providerRepositoryId);
268
+ let canonicalProjectId = payload.projectId === undefined ? null : String(payload.projectId);
269
+ const providerUrl = new URL(registration.providerBaseUrl);
270
+ const providerPath = providerUrl.pathname.replace(/\/$/, "");
271
+ if (
272
+ repositoryUrl.protocol !== "https:" ||
273
+ repositoryUrl.username ||
274
+ repositoryUrl.password ||
275
+ repositoryUrl.host.toLowerCase() !== providerUrl.host.toLowerCase() ||
276
+ (providerPath !== "" &&
277
+ providerPath !== "/" &&
278
+ !repositoryUrl.pathname.startsWith(`${providerPath}/`))
279
+ ) {
280
+ throw new HTTPException(422, {
281
+ message:
282
+ "repositoryUri must be a credential-free HTTPS URL on the registered provider host",
283
+ });
284
+ }
285
+ if (registration.provider === "github") {
286
+ const installationId = positiveInteger(payload.installationId);
287
+ const repositoryId = positiveInteger(payload.providerRepositoryId);
288
+ if (!installationId || !repositoryId || !registration.appId) {
289
+ throw new HTTPException(422, {
290
+ message: "GitHub repository bindings require App, installation, and repository IDs",
291
+ });
292
+ }
293
+ const encryptionKey = requirePrReviewEncryptionKey(deps);
294
+ if (!registration.credentialEncrypted) {
295
+ throw new HTTPException(409, {
296
+ message: "PR Review GitHub App credential is unavailable",
297
+ });
298
+ }
299
+ let allowedRepositories;
300
+ try {
301
+ allowedRepositories = await listGitHubAppRepositoriesWithSigningSettings(
302
+ {
303
+ githubAppId: registration.appId,
304
+ githubAppPrivateKey: decryptVariableSetValue(
305
+ encryptionKey,
306
+ registration.credentialEncrypted,
307
+ ),
308
+ },
309
+ { installationIds: [installationId] },
310
+ );
311
+ } catch {
312
+ throw new HTTPException(502, {
313
+ message: "Could not verify the repository against the registered GitHub App",
314
+ });
315
+ }
316
+ const allowedRepository = allowedRepositories.find(
317
+ (candidate) =>
318
+ candidate.id === repositoryId &&
319
+ candidate.fullName.toLowerCase() === payload.repositoryFullName.toLowerCase(),
320
+ );
321
+ if (!allowedRepository) {
322
+ throw new HTTPException(403, {
323
+ message: "GitHub repository is not installed for this PR Review App",
324
+ });
325
+ }
326
+ canonicalRepositoryUri = allowedRepository.cloneUrl;
327
+ canonicalRepositoryFullName = allowedRepository.fullName;
328
+ canonicalProviderRepositoryId = String(allowedRepository.id);
329
+ }
330
+ if (registration.provider !== "github") {
331
+ if (
332
+ registration.accessTokenExpiresAt &&
333
+ Date.parse(registration.accessTokenExpiresAt) <= Date.now()
334
+ ) {
335
+ throw new HTTPException(409, {
336
+ message: "PR Review provider credential has expired",
337
+ });
338
+ }
339
+ const encryptionKey = requirePrReviewEncryptionKey(deps);
340
+ if (!registration.credentialEncrypted) {
341
+ throw new HTTPException(409, {
342
+ message: "PR Review provider credential is unavailable",
343
+ });
344
+ }
345
+ try {
346
+ const verified = await verifyPrReviewProviderRepository({
347
+ provider: registration.provider,
348
+ providerBaseUrl: registration.providerBaseUrl,
349
+ providerRepositoryId: String(payload.providerRepositoryId),
350
+ ...(payload.projectId !== undefined ? { projectId: String(payload.projectId) } : {}),
351
+ token: decryptVariableSetValue(encryptionKey, registration.credentialEncrypted),
352
+ username: registration.webhookUsername,
353
+ settings,
354
+ });
355
+ canonicalRepositoryUri = verified.repositoryUri;
356
+ canonicalRepositoryFullName = verified.repositoryFullName;
357
+ canonicalProviderRepositoryId = verified.providerRepositoryId;
358
+ canonicalProjectId = verified.projectId;
359
+ } catch (error) {
360
+ if (error instanceof PrReviewProviderRepositoryError) {
361
+ const status =
362
+ error.reason === "denied" ? 403 : error.reason === "identity_mismatch" ? 422 : 502;
363
+ throw new HTTPException(status, { message: error.message });
364
+ }
365
+ throw error;
366
+ }
367
+ }
368
+ const model = payload.model
369
+ ? (canonicalConfiguredModel(settings, payload.model) ?? null)
370
+ : null;
371
+ if (model) await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
372
+ const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
373
+ (candidate) => candidate.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
374
+ );
375
+ if (!template) {
376
+ throw new HTTPException(503, { message: "PR Review automation template is unavailable" });
377
+ }
378
+ const binding = await mapPrReviewUniqueConflict(
379
+ createPrReviewRepositoryBinding(db, {
380
+ accountId: grant.accountId,
381
+ workspaceId,
382
+ registrationId: registration.id,
383
+ provider: registration.provider,
384
+ repositoryUri: canonicalRepositoryUri,
385
+ repositoryFullName: canonicalRepositoryFullName,
386
+ providerRepositoryId: canonicalProviderRepositoryId,
387
+ installationId:
388
+ payload.installationId === undefined ? null : String(payload.installationId),
389
+ projectId: canonicalProjectId,
390
+ model,
391
+ additionalInstructions: payload.additionalInstructions ?? null,
392
+ status: payload.status,
393
+ createdBySubjectId: grant.subjectId,
394
+ packInstallationId: packInstallation.id,
395
+ packTemplateId: template.id,
396
+ adapterId: template.adapterId,
397
+ eventTypes: template.eventTypes,
398
+ configuration: template.configuration,
399
+ sessionTemplate: template.sessionTemplate,
400
+ }),
401
+ "This repository is already bound to the selected PR Review registration",
402
+ );
403
+ await recordAuditEvent(db, {
404
+ accountId: grant.accountId,
405
+ workspaceId,
406
+ subjectId: grant.subjectId,
407
+ action: "prReview.repository_binding.created",
408
+ targetType: "pr_review_repository_binding",
409
+ targetId: binding.id,
410
+ metadata: {
411
+ provider: binding.provider,
412
+ registrationId: binding.registrationId,
413
+ providerRepositoryId: binding.providerRepositoryId,
414
+ },
415
+ });
416
+ return c.json(PrReviewRepositoryBinding.parse(binding), 201);
417
+ });
418
+
419
+ app.patch("/v1/workspaces/:workspaceId/pr-review/repositories/:bindingId", async (c) => {
420
+ const workspaceId = c.req.param("workspaceId");
421
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
422
+ await requirePrReviewPackActive(db, workspaceId);
423
+ const payload = UpdatePrReviewRepositoryBindingRequest.parse(await c.req.json());
424
+ const model =
425
+ payload.model === undefined
426
+ ? undefined
427
+ : payload.model === null
428
+ ? null
429
+ : (canonicalConfiguredModel(settings, payload.model) ?? null);
430
+ if (model) await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
431
+ const binding = await updatePrReviewRepositoryBinding(db, {
432
+ accountId: grant.accountId,
433
+ workspaceId,
434
+ bindingId: c.req.param("bindingId"),
435
+ subjectId: grant.subjectId,
436
+ ...(model !== undefined ? { model } : {}),
437
+ ...(payload.additionalInstructions !== undefined
438
+ ? { additionalInstructions: payload.additionalInstructions }
439
+ : {}),
440
+ ...(payload.status !== undefined ? { status: payload.status } : {}),
441
+ });
442
+ if (!binding)
443
+ throw new HTTPException(404, {
444
+ message: "PR Review repository binding not found",
445
+ });
446
+ await recordAuditEvent(db, {
447
+ accountId: grant.accountId,
448
+ workspaceId,
449
+ subjectId: grant.subjectId,
450
+ action: "prReview.repository_binding.updated",
451
+ targetType: "pr_review_repository_binding",
452
+ targetId: binding.id,
453
+ metadata: { fields: Object.keys(payload) },
454
+ });
455
+ return c.json(PrReviewRepositoryBinding.parse(binding));
456
+ });
457
+
458
+ app.delete("/v1/workspaces/:workspaceId/pr-review/repositories/:bindingId", async (c) => {
459
+ const workspaceId = c.req.param("workspaceId");
460
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
461
+ const deleted = await deletePrReviewRepositoryBinding(db, {
462
+ accountId: grant.accountId,
463
+ workspaceId,
464
+ bindingId: c.req.param("bindingId"),
465
+ subjectId: grant.subjectId,
466
+ });
467
+ if (!deleted)
468
+ throw new HTTPException(404, {
469
+ message: "PR Review repository binding not found",
470
+ });
471
+ await recordAuditEvent(db, {
472
+ accountId: grant.accountId,
473
+ workspaceId,
474
+ subjectId: grant.subjectId,
475
+ action: "prReview.repository_binding.disabled",
476
+ targetType: "pr_review_repository_binding",
477
+ targetId: c.req.param("bindingId"),
478
+ metadata: {},
479
+ });
480
+ return c.body(null, 204);
481
+ });
482
+ }
483
+
484
+ function assertPrReviewSandboxBackend(deps: ApiRouteDeps): void {
485
+ if (deps.settings.sandboxBackend === "selfhosted") {
486
+ throw new HTTPException(409, {
487
+ message:
488
+ "OpenGeni Review Bot requires managed compute so it can materialize and verify the exact pull-request head",
489
+ });
490
+ }
491
+ }
492
+
493
+ async function requirePrReviewPackActive(db: ApiRouteDeps["db"], workspaceId: string) {
494
+ const installation = await getPackInstallation(db, workspaceId, OPENGENI_PR_REVIEW_PACK_ID);
495
+ if (installation?.status !== "active") {
496
+ throw new HTTPException(409, {
497
+ message: "Install and enable the OpenGeni Review Bot Pack first",
498
+ });
499
+ }
500
+ return installation;
501
+ }
502
+
503
+ function requirePrReviewEncryptionKey(deps: ApiRouteDeps): Uint8Array {
504
+ const key = environmentsEncryptionKeyBytes(deps.settings);
505
+ if (!key) {
506
+ throw new HTTPException(503, {
507
+ message: "OpenGeni Review Bot requires configured secret encryption",
508
+ });
509
+ }
510
+ return key;
511
+ }
512
+
513
+ async function mapPrReviewUniqueConflict<T>(operation: Promise<T>, message: string): Promise<T> {
514
+ try {
515
+ return await operation;
516
+ } catch (error) {
517
+ if (nestedPostgresSqlState(error) === "23505") {
518
+ throw new HTTPException(409, { message });
519
+ }
520
+ throw error;
521
+ }
522
+ }
523
+
524
+ function positiveInteger(value: unknown): number | null {
525
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value;
526
+ if (typeof value === "string" && /^\d+$/.test(value)) {
527
+ const parsed = Number(value);
528
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
529
+ }
530
+ return null;
531
+ }