@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.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- 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
|
+
}
|