@opengeni/api-router 2.6.3-canary.0 → 2.6.4-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.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/{chunk-XTLI3CBH.js → chunk-UOI7KAP3.js} +1010 -410
- package/dist/chunk-UOI7KAP3.js.map +1 -0
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/model-catalog.d.ts +5 -31
- package/package.json +18 -18
- package/src/app.ts +34 -9
- package/src/index.ts +13 -1
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/server.ts +11 -2
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +186 -19
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +0 -8
- package/src/routes/pr-review.ts +80 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +72 -47
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +373 -3
- package/dist/chunk-XTLI3CBH.js.map +0 -1
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
|
|
4
|
+
WORKSPACE_OPENROUTER_CONNECTION_ROLE,
|
|
5
|
+
VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
|
|
6
|
+
VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
7
|
+
} from "@opengeni/config";
|
|
2
8
|
import {
|
|
3
9
|
ATLASSIAN_PROVIDER_DOMAIN,
|
|
4
10
|
AtlassianConnectionMetadata,
|
|
@@ -21,11 +27,16 @@ import {
|
|
|
21
27
|
IntegrationClientMetadata,
|
|
22
28
|
ListSlackInstallationBindingsResponse,
|
|
23
29
|
ListConnectionsResponse,
|
|
30
|
+
OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY,
|
|
31
|
+
OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY,
|
|
24
32
|
OpenGeniSlackBotInstallRequest,
|
|
25
33
|
OpenGeniSlackBotInstallStart,
|
|
26
34
|
OAuthStartRequest,
|
|
27
35
|
OAuthStartResponse,
|
|
28
36
|
UpdateConnectionRequest,
|
|
37
|
+
VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY,
|
|
38
|
+
VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY,
|
|
39
|
+
stableJson,
|
|
29
40
|
} from "@opengeni/contracts";
|
|
30
41
|
import {
|
|
31
42
|
bindConnectorDocumentDestination,
|
|
@@ -69,11 +80,17 @@ import {
|
|
|
69
80
|
persistSlackBotInstallationWithSuccessAudit,
|
|
70
81
|
recordSlackBotInstallCallbackFailure,
|
|
71
82
|
revokeConnection,
|
|
83
|
+
revokeWorkspaceOpenRouterConnections,
|
|
84
|
+
revokeWorkspaceVercelAiGatewayConnections,
|
|
72
85
|
revokeConnectionWithSlackBotSuccessAudit,
|
|
86
|
+
rotateWorkspaceVercelAiGatewayConnection,
|
|
87
|
+
rotateWorkspaceOpenRouterConnection,
|
|
73
88
|
SlackBotLifecycleSuccessAuditError,
|
|
74
89
|
SlackInstallationBindingConflictError,
|
|
75
90
|
updateConnection,
|
|
76
91
|
updateSlackBotDocumentDestination,
|
|
92
|
+
upsertWorkspaceVercelAiGatewayConnection,
|
|
93
|
+
upsertWorkspaceOpenRouterConnection,
|
|
77
94
|
type SlackBotInstallCallbackFailureReason,
|
|
78
95
|
type SlackBotInstallCallbackFailureStage,
|
|
79
96
|
} from "@opengeni/db";
|
|
@@ -193,18 +210,69 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
193
210
|
assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
|
|
194
211
|
assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
|
|
195
212
|
assertNotDirectPersonalGitHubOAuth(providerDomain, payload.kind, payload.metadata);
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
workspaceId,
|
|
213
|
+
const credentialEncrypted = encryptCredentialBundle(key, payload.credential);
|
|
214
|
+
const workspaceProviderKind = workspaceProviderApiKeyConnectionKind({
|
|
199
215
|
subjectId,
|
|
200
216
|
providerDomain,
|
|
201
217
|
kind: payload.kind,
|
|
202
|
-
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
203
|
-
grantedScopes: payload.grantedScopes,
|
|
204
|
-
expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
|
|
205
218
|
metadata: payload.metadata,
|
|
206
|
-
createdBySubjectId: grant.subjectId,
|
|
207
219
|
});
|
|
220
|
+
const connection = workspaceProviderKind
|
|
221
|
+
? await (async () => {
|
|
222
|
+
const provider = workspaceProviderApiKeyConnectionSpec(workspaceProviderKind);
|
|
223
|
+
if (!payload.operationId) {
|
|
224
|
+
throw new HTTPException(400, {
|
|
225
|
+
message: `connecting ${provider.label} requires an operationId`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const expiresAt = payload.expiresAt ? new Date(payload.expiresAt) : null;
|
|
229
|
+
const metadata = workspaceProviderCredentialMetadata(
|
|
230
|
+
workspaceProviderKind,
|
|
231
|
+
payload.metadata,
|
|
232
|
+
);
|
|
233
|
+
const input = {
|
|
234
|
+
accountId: grant.accountId,
|
|
235
|
+
workspaceId,
|
|
236
|
+
operationId: payload.operationId,
|
|
237
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
238
|
+
action: "create",
|
|
239
|
+
providerDomain,
|
|
240
|
+
kind: payload.kind,
|
|
241
|
+
subjectId,
|
|
242
|
+
credential: payload.credential,
|
|
243
|
+
grantedScopes: payload.grantedScopes,
|
|
244
|
+
expiresAt: expiresAt?.toISOString() ?? null,
|
|
245
|
+
metadata,
|
|
246
|
+
}),
|
|
247
|
+
credentialEncrypted,
|
|
248
|
+
grantedScopes: payload.grantedScopes,
|
|
249
|
+
expiresAt,
|
|
250
|
+
metadata,
|
|
251
|
+
updatedBySubjectId: grant.subjectId,
|
|
252
|
+
};
|
|
253
|
+
const created =
|
|
254
|
+
workspaceProviderKind === "vercel_gateway"
|
|
255
|
+
? await upsertWorkspaceVercelAiGatewayConnection(db, input)
|
|
256
|
+
: await upsertWorkspaceOpenRouterConnection(db, input);
|
|
257
|
+
if (!created || created.status === "revoked") {
|
|
258
|
+
throw new HTTPException(409, {
|
|
259
|
+
message: `${provider.label} is already connected; reload before replacing its key`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return created;
|
|
263
|
+
})()
|
|
264
|
+
: await createConnection(db, {
|
|
265
|
+
accountId: grant.accountId,
|
|
266
|
+
workspaceId,
|
|
267
|
+
subjectId,
|
|
268
|
+
providerDomain,
|
|
269
|
+
kind: payload.kind,
|
|
270
|
+
credentialEncrypted,
|
|
271
|
+
grantedScopes: payload.grantedScopes,
|
|
272
|
+
expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
|
|
273
|
+
metadata: payload.metadata,
|
|
274
|
+
createdBySubjectId: grant.subjectId,
|
|
275
|
+
});
|
|
208
276
|
return c.json(ConnectionResponse.parse({ connection }), 201);
|
|
209
277
|
});
|
|
210
278
|
|
|
@@ -794,6 +862,24 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
794
862
|
payload.providerDomain ?? existing.providerDomain,
|
|
795
863
|
);
|
|
796
864
|
const kind = payload.kind ?? existing.kind;
|
|
865
|
+
const subjectId =
|
|
866
|
+
payload.subjectId === undefined
|
|
867
|
+
? existing.subjectId
|
|
868
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
869
|
+
const targetWorkspaceProviderKind = workspaceProviderApiKeyConnectionKind({
|
|
870
|
+
subjectId,
|
|
871
|
+
providerDomain,
|
|
872
|
+
kind,
|
|
873
|
+
metadata: payload.metadata ?? existing.metadata,
|
|
874
|
+
});
|
|
875
|
+
if (!workspaceProviderApiKeyConnectionKind(existing) && targetWorkspaceProviderKind) {
|
|
876
|
+
throw new HTTPException(422, {
|
|
877
|
+
message:
|
|
878
|
+
targetWorkspaceProviderKind === "vercel_gateway"
|
|
879
|
+
? "use the Vercel AI Gateway connect flow to create this connection"
|
|
880
|
+
: "use the OpenRouter connect flow to create this connection",
|
|
881
|
+
});
|
|
882
|
+
}
|
|
797
883
|
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
798
884
|
assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
799
885
|
assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
@@ -819,6 +905,86 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
819
905
|
});
|
|
820
906
|
}
|
|
821
907
|
}
|
|
908
|
+
const existingWorkspaceProviderKind = existing
|
|
909
|
+
? workspaceProviderApiKeyConnectionKind(existing)
|
|
910
|
+
: null;
|
|
911
|
+
if (existing && existingWorkspaceProviderKind) {
|
|
912
|
+
const provider = workspaceProviderApiKeyConnectionSpec(existingWorkspaceProviderKind);
|
|
913
|
+
const providerDomain = canonicalProviderDomain(
|
|
914
|
+
payload.providerDomain ?? existing.providerDomain,
|
|
915
|
+
);
|
|
916
|
+
const subjectId =
|
|
917
|
+
payload.subjectId === undefined
|
|
918
|
+
? existing.subjectId
|
|
919
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
920
|
+
const kind = payload.kind ?? existing.kind;
|
|
921
|
+
if (
|
|
922
|
+
subjectId !== null ||
|
|
923
|
+
providerDomain !== provider.providerDomain ||
|
|
924
|
+
kind !== "api_key" ||
|
|
925
|
+
(payload.metadata?.credentialRole !== undefined &&
|
|
926
|
+
payload.metadata.credentialRole !== provider.credentialRole)
|
|
927
|
+
) {
|
|
928
|
+
throw new HTTPException(422, {
|
|
929
|
+
message: `${provider.label} connection identity cannot be changed`,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
if (payload.credential === undefined) {
|
|
933
|
+
throw new HTTPException(400, {
|
|
934
|
+
message: `updating a ${provider.label} connection requires a new credential`,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (payload.expectedVersion === undefined || payload.operationId === undefined) {
|
|
938
|
+
throw new HTTPException(400, {
|
|
939
|
+
message: `updating a ${provider.label} connection requires expectedVersion and operationId`,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const key = requireEnvironmentEncryption(settings);
|
|
943
|
+
const grantedScopes = payload.grantedScopes ?? existing.grantedScopes;
|
|
944
|
+
const expiresAt =
|
|
945
|
+
payload.expiresAt !== undefined
|
|
946
|
+
? payload.expiresAt
|
|
947
|
+
? new Date(payload.expiresAt)
|
|
948
|
+
: null
|
|
949
|
+
: existing.expiresAt
|
|
950
|
+
? new Date(existing.expiresAt)
|
|
951
|
+
: null;
|
|
952
|
+
const metadata = workspaceProviderCredentialMetadata(existingWorkspaceProviderKind, {
|
|
953
|
+
...existing.metadata,
|
|
954
|
+
...(payload.metadata ?? {}),
|
|
955
|
+
});
|
|
956
|
+
const input = {
|
|
957
|
+
accountId: grant.accountId,
|
|
958
|
+
workspaceId,
|
|
959
|
+
connectionId: existing.id,
|
|
960
|
+
expectedVersion: payload.expectedVersion,
|
|
961
|
+
operationId: payload.operationId,
|
|
962
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
963
|
+
action: "rotate",
|
|
964
|
+
connectionId: existing.id,
|
|
965
|
+
expectedVersion: payload.expectedVersion,
|
|
966
|
+
credential: payload.credential,
|
|
967
|
+
grantedScopes,
|
|
968
|
+
expiresAt: expiresAt?.toISOString() ?? null,
|
|
969
|
+
metadata,
|
|
970
|
+
}),
|
|
971
|
+
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
972
|
+
grantedScopes,
|
|
973
|
+
expiresAt,
|
|
974
|
+
metadata,
|
|
975
|
+
updatedBySubjectId: grant.subjectId,
|
|
976
|
+
};
|
|
977
|
+
const connection =
|
|
978
|
+
existingWorkspaceProviderKind === "vercel_gateway"
|
|
979
|
+
? await rotateWorkspaceVercelAiGatewayConnection(db, input)
|
|
980
|
+
: await rotateWorkspaceOpenRouterConnection(db, input);
|
|
981
|
+
if (!connection) {
|
|
982
|
+
throw new HTTPException(409, {
|
|
983
|
+
message: `${provider.label} connection changed; reload before replacing its key`,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
987
|
+
}
|
|
822
988
|
const key = payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
|
|
823
989
|
const subjectId =
|
|
824
990
|
payload.subjectId === undefined
|
|
@@ -879,6 +1045,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
879
1045
|
existing.providerDomain === ATLASSIAN_PROVIDER_DOMAIN &&
|
|
880
1046
|
existing.kind === "oauth2" &&
|
|
881
1047
|
AtlassianConnectionMetadata.safeParse(existing.metadata).success;
|
|
1048
|
+
const workspaceProviderKind = workspaceProviderApiKeyConnectionKind(existing);
|
|
882
1049
|
const disconnectPayload = await c.req.json().catch(() => null);
|
|
883
1050
|
const googleDriveDisconnect = isGoogleDrive
|
|
884
1051
|
? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
|
|
@@ -909,7 +1076,13 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
909
1076
|
"invalid personal GitHub disconnect request",
|
|
910
1077
|
});
|
|
911
1078
|
}
|
|
912
|
-
if (
|
|
1079
|
+
if (
|
|
1080
|
+
existing.status === "revoked" &&
|
|
1081
|
+
!isGoogleDrive &&
|
|
1082
|
+
!isAtlassian &&
|
|
1083
|
+
!isPersonalGitHub &&
|
|
1084
|
+
!workspaceProviderKind
|
|
1085
|
+
) {
|
|
913
1086
|
return c.json(ConnectionResponse.parse({ connection: existing }));
|
|
914
1087
|
}
|
|
915
1088
|
const connection = isGoogleDrive
|
|
@@ -944,13 +1117,29 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
944
1117
|
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
945
1118
|
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
946
1119
|
})
|
|
947
|
-
:
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1120
|
+
: workspaceProviderKind
|
|
1121
|
+
? workspaceProviderKind === "vercel_gateway"
|
|
1122
|
+
? await revokeWorkspaceVercelAiGatewayConnections(db, {
|
|
1123
|
+
accountId: grant.accountId,
|
|
1124
|
+
workspaceId,
|
|
1125
|
+
connectionId,
|
|
1126
|
+
expectedVersion: existing.version,
|
|
1127
|
+
updatedBySubjectId: grant.subjectId,
|
|
1128
|
+
})
|
|
1129
|
+
: await revokeWorkspaceOpenRouterConnections(db, {
|
|
1130
|
+
accountId: grant.accountId,
|
|
1131
|
+
workspaceId,
|
|
1132
|
+
connectionId,
|
|
1133
|
+
expectedVersion: existing.version,
|
|
1134
|
+
updatedBySubjectId: grant.subjectId,
|
|
1135
|
+
})
|
|
1136
|
+
: await revokeConnection(
|
|
1137
|
+
db,
|
|
1138
|
+
workspaceId,
|
|
1139
|
+
connectionId,
|
|
1140
|
+
grant.subjectId,
|
|
1141
|
+
existing.version,
|
|
1142
|
+
);
|
|
954
1143
|
if (!connection) {
|
|
955
1144
|
throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
|
|
956
1145
|
}
|
|
@@ -1367,6 +1556,51 @@ function assertNotReservedPersonalGitHubMetadata(
|
|
|
1367
1556
|
}
|
|
1368
1557
|
}
|
|
1369
1558
|
|
|
1559
|
+
type WorkspaceProviderApiKeyConnectionKind = "vercel_gateway" | "openrouter";
|
|
1560
|
+
|
|
1561
|
+
function workspaceProviderApiKeyConnectionSpec(
|
|
1562
|
+
providerKind: WorkspaceProviderApiKeyConnectionKind,
|
|
1563
|
+
): {
|
|
1564
|
+
providerDomain: string;
|
|
1565
|
+
credentialRole: string;
|
|
1566
|
+
label: string;
|
|
1567
|
+
} {
|
|
1568
|
+
return providerKind === "vercel_gateway"
|
|
1569
|
+
? {
|
|
1570
|
+
providerDomain: VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
|
|
1571
|
+
credentialRole: VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
1572
|
+
label: "Vercel AI Gateway",
|
|
1573
|
+
}
|
|
1574
|
+
: {
|
|
1575
|
+
providerDomain: WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
|
|
1576
|
+
credentialRole: WORKSPACE_OPENROUTER_CONNECTION_ROLE,
|
|
1577
|
+
label: "OpenRouter",
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function workspaceProviderApiKeyConnectionKind(input: {
|
|
1582
|
+
subjectId?: string | null;
|
|
1583
|
+
providerDomain: string;
|
|
1584
|
+
kind: string;
|
|
1585
|
+
metadata?: Record<string, unknown>;
|
|
1586
|
+
}): WorkspaceProviderApiKeyConnectionKind | null {
|
|
1587
|
+
if (input.subjectId != null || input.kind !== "api_key") return null;
|
|
1588
|
+
const providerDomain = input.providerDomain.toLowerCase();
|
|
1589
|
+
if (
|
|
1590
|
+
providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
|
|
1591
|
+
input.metadata?.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE
|
|
1592
|
+
) {
|
|
1593
|
+
return "vercel_gateway";
|
|
1594
|
+
}
|
|
1595
|
+
if (
|
|
1596
|
+
providerDomain === WORKSPACE_OPENROUTER_CONNECTION_DOMAIN &&
|
|
1597
|
+
input.metadata?.credentialRole === WORKSPACE_OPENROUTER_CONNECTION_ROLE
|
|
1598
|
+
) {
|
|
1599
|
+
return "openrouter";
|
|
1600
|
+
}
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1370
1604
|
function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
|
|
1371
1605
|
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
1372
1606
|
throw new HTTPException(422, {
|
|
@@ -1426,6 +1660,27 @@ function encryptCredentialBundle(key: Uint8Array, credential: Record<string, unk
|
|
|
1426
1660
|
return encryptEnvironmentValue(key, JSON.stringify(credential));
|
|
1427
1661
|
}
|
|
1428
1662
|
|
|
1663
|
+
function workspaceProviderCredentialMetadata(
|
|
1664
|
+
providerKind: WorkspaceProviderApiKeyConnectionKind,
|
|
1665
|
+
metadata: Record<string, unknown> | undefined,
|
|
1666
|
+
): Record<string, unknown> {
|
|
1667
|
+
const {
|
|
1668
|
+
[OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _openRouterOperationId,
|
|
1669
|
+
[OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _openRouterOperationDigest,
|
|
1670
|
+
[VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _operationId,
|
|
1671
|
+
[VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _operationDigest,
|
|
1672
|
+
...effectiveMetadata
|
|
1673
|
+
} = metadata ?? {};
|
|
1674
|
+
return {
|
|
1675
|
+
...effectiveMetadata,
|
|
1676
|
+
credentialRole: workspaceProviderApiKeyConnectionSpec(providerKind).credentialRole,
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function workspaceProviderCredentialRequestDigest(key: Uint8Array, value: unknown): string {
|
|
1681
|
+
return createHmac("sha256", key).update(stableJson(value), "utf8").digest("hex");
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1429
1684
|
function slackBotCredentialBundle(token: string): Record<string, unknown> {
|
|
1430
1685
|
const headerName = ["author", "ization"].join("");
|
|
1431
1686
|
const scheme = ["Bear", "er"].join("");
|
package/src/routes/github.ts
CHANGED
|
@@ -428,14 +428,6 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
428
428
|
if (candidates.length === 0) {
|
|
429
429
|
return redirectToGitHubInstallation(c, deps, selectionState);
|
|
430
430
|
}
|
|
431
|
-
if (candidates.length === 1) {
|
|
432
|
-
return redirectToExactGitHubAuthorization(
|
|
433
|
-
c,
|
|
434
|
-
deps,
|
|
435
|
-
selectionState,
|
|
436
|
-
candidates[0]!.installation.installationId,
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
431
|
setGitHubStateCookie(c, deps, selectionState);
|
|
440
432
|
return c.html(
|
|
441
433
|
githubInstallationChooserHtml(
|
package/src/routes/pr-review.ts
CHANGED
|
@@ -13,12 +13,14 @@ import {
|
|
|
13
13
|
canonicalConfiguredModel,
|
|
14
14
|
defaultPrReviewProviderBaseUrl,
|
|
15
15
|
getCapabilityPack,
|
|
16
|
+
workspaceCustomModelReference,
|
|
16
17
|
prReviewWebhookAuthKind,
|
|
17
18
|
normalizePrReviewProviderBaseUrl,
|
|
18
19
|
prReviewPackConnectorId,
|
|
19
20
|
PR_REVIEW_AUTOMATION_TEMPLATE_ID,
|
|
20
21
|
requireAccessGrant,
|
|
21
22
|
requirePermission,
|
|
23
|
+
resolveWorkspaceCatalogSettings,
|
|
22
24
|
type ApiRouteDeps,
|
|
23
25
|
} from "@opengeni/core";
|
|
24
26
|
import {
|
|
@@ -32,6 +34,8 @@ import {
|
|
|
32
34
|
getPackInstallation,
|
|
33
35
|
listPrReviewAppRegistrations,
|
|
34
36
|
listPrReviewRepositoryBindings,
|
|
37
|
+
lockActiveWorkspaceGatewayCustomModelForAdmission,
|
|
38
|
+
lockActiveWorkspaceOpenRouterCustomModelForAdmission,
|
|
35
39
|
nestedPostgresSqlState,
|
|
36
40
|
recordAuditEvent,
|
|
37
41
|
updatePrReviewAppRegistration,
|
|
@@ -385,10 +389,24 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
385
389
|
throw error;
|
|
386
390
|
}
|
|
387
391
|
}
|
|
392
|
+
const catalogSettings = (
|
|
393
|
+
await resolveWorkspaceCatalogSettings(db, deps.settings, {
|
|
394
|
+
accountId: grant.accountId,
|
|
395
|
+
workspaceId,
|
|
396
|
+
})
|
|
397
|
+
).settings;
|
|
388
398
|
const model = payload.model
|
|
389
|
-
? (canonicalConfiguredModel(
|
|
399
|
+
? (canonicalConfiguredModel(catalogSettings, payload.model) ?? null)
|
|
390
400
|
: null;
|
|
391
|
-
if (model) await assertWorkspaceModelPolicyAllows(db,
|
|
401
|
+
if (model) await assertWorkspaceModelPolicyAllows(db, catalogSettings, workspaceId, model);
|
|
402
|
+
const beforeCreateCommit = model
|
|
403
|
+
? prReviewCustomModelCommitGuard({
|
|
404
|
+
settings: catalogSettings,
|
|
405
|
+
accountId: grant.accountId,
|
|
406
|
+
workspaceId,
|
|
407
|
+
modelId: model,
|
|
408
|
+
})
|
|
409
|
+
: undefined;
|
|
392
410
|
const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
|
|
393
411
|
(candidate) => candidate.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
|
|
394
412
|
);
|
|
@@ -417,6 +435,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
417
435
|
eventTypes: template.eventTypes,
|
|
418
436
|
configuration: template.configuration,
|
|
419
437
|
sessionTemplate: template.sessionTemplate,
|
|
438
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
420
439
|
}),
|
|
421
440
|
"This repository is already bound to the selected PR Review registration",
|
|
422
441
|
);
|
|
@@ -441,13 +460,40 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
441
460
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
442
461
|
await requirePrReviewPackActive(db, workspaceId);
|
|
443
462
|
const payload = UpdatePrReviewRepositoryBindingRequest.parse(await c.req.json());
|
|
463
|
+
const catalogSettings = (
|
|
464
|
+
await resolveWorkspaceCatalogSettings(db, deps.settings, {
|
|
465
|
+
accountId: grant.accountId,
|
|
466
|
+
workspaceId,
|
|
467
|
+
})
|
|
468
|
+
).settings;
|
|
444
469
|
const model =
|
|
445
470
|
payload.model === undefined
|
|
446
471
|
? undefined
|
|
447
472
|
: payload.model === null
|
|
448
473
|
? null
|
|
449
|
-
: (canonicalConfiguredModel(
|
|
450
|
-
if (model) await assertWorkspaceModelPolicyAllows(db,
|
|
474
|
+
: (canonicalConfiguredModel(catalogSettings, payload.model) ?? null);
|
|
475
|
+
if (model) await assertWorkspaceModelPolicyAllows(db, catalogSettings, workspaceId, model);
|
|
476
|
+
const beforeUpdateCommit = async (
|
|
477
|
+
tx: ApiRouteDeps["db"],
|
|
478
|
+
context: {
|
|
479
|
+
currentModel: string | null;
|
|
480
|
+
currentStatus: "active" | "disabled";
|
|
481
|
+
nextModel: string | null;
|
|
482
|
+
nextStatus: "active" | "disabled";
|
|
483
|
+
},
|
|
484
|
+
): Promise<void> => {
|
|
485
|
+
const materialExecutionChange =
|
|
486
|
+
payload.model !== undefined ||
|
|
487
|
+
payload.additionalInstructions !== undefined ||
|
|
488
|
+
(context.currentStatus === "disabled" && context.nextStatus === "active");
|
|
489
|
+
if (!materialExecutionChange || !context.nextModel) return;
|
|
490
|
+
await prReviewCustomModelCommitGuard({
|
|
491
|
+
settings: catalogSettings,
|
|
492
|
+
accountId: grant.accountId,
|
|
493
|
+
workspaceId,
|
|
494
|
+
modelId: context.nextModel,
|
|
495
|
+
})?.(tx);
|
|
496
|
+
};
|
|
451
497
|
const binding = await updatePrReviewRepositoryBinding(db, {
|
|
452
498
|
accountId: grant.accountId,
|
|
453
499
|
workspaceId,
|
|
@@ -458,6 +504,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
458
504
|
? { additionalInstructions: payload.additionalInstructions }
|
|
459
505
|
: {}),
|
|
460
506
|
...(payload.status !== undefined ? { status: payload.status } : {}),
|
|
507
|
+
beforeUpdateCommit,
|
|
461
508
|
});
|
|
462
509
|
if (!binding)
|
|
463
510
|
throw new HTTPException(404, {
|
|
@@ -501,6 +548,35 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
501
548
|
});
|
|
502
549
|
}
|
|
503
550
|
|
|
551
|
+
function prReviewCustomModelCommitGuard(input: {
|
|
552
|
+
settings: ApiRouteDeps["settings"];
|
|
553
|
+
accountId: string;
|
|
554
|
+
workspaceId: string;
|
|
555
|
+
modelId: string;
|
|
556
|
+
}): ((tx: ApiRouteDeps["db"]) => Promise<void>) | undefined {
|
|
557
|
+
const reference = workspaceCustomModelReference(input.settings, input.modelId);
|
|
558
|
+
if (!reference) return undefined;
|
|
559
|
+
return async (tx): Promise<void> => {
|
|
560
|
+
const active =
|
|
561
|
+
reference.providerKind === "openrouter"
|
|
562
|
+
? await lockActiveWorkspaceOpenRouterCustomModelForAdmission(tx, {
|
|
563
|
+
accountId: input.accountId,
|
|
564
|
+
workspaceId: input.workspaceId,
|
|
565
|
+
upstreamModelId: reference.upstreamModelId,
|
|
566
|
+
})
|
|
567
|
+
: await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
|
|
568
|
+
accountId: input.accountId,
|
|
569
|
+
workspaceId: input.workspaceId,
|
|
570
|
+
upstreamModelId: reference.upstreamModelId,
|
|
571
|
+
});
|
|
572
|
+
if (!active) {
|
|
573
|
+
throw new HTTPException(422, {
|
|
574
|
+
message: `model is not available: ${input.modelId}`,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
504
580
|
function assertPrReviewSandboxBackend(deps: ApiRouteDeps): void {
|
|
505
581
|
if (deps.settings.sandboxBackend === "selfhosted") {
|
|
506
582
|
throw new HTTPException(409, {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { listScheduledTaskRuns, listScheduledTasks } from "@opengeni/db";
|
|
7
7
|
import type { Hono } from "hono";
|
|
8
8
|
import { HTTPException } from "hono/http-exception";
|
|
9
|
-
import { requireAccessGrant } from "@opengeni/core";
|
|
9
|
+
import { requireAccessGrant, resolveWorkspaceCatalogSettings } from "@opengeni/core";
|
|
10
10
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
11
11
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
12
12
|
import {
|
|
@@ -30,7 +30,7 @@ import { boundedLimit } from "../http/common";
|
|
|
30
30
|
import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
|
|
31
31
|
|
|
32
32
|
export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
33
|
-
const {
|
|
33
|
+
const { db, workflowClient, objectStorage } = deps;
|
|
34
34
|
|
|
35
35
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
|
|
36
36
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -43,6 +43,12 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
const payload = parsedPayload.data;
|
|
46
|
+
const catalogSettings = (
|
|
47
|
+
await resolveWorkspaceCatalogSettings(db, deps.settings, {
|
|
48
|
+
accountId: grant.accountId,
|
|
49
|
+
workspaceId,
|
|
50
|
+
})
|
|
51
|
+
).settings;
|
|
46
52
|
await requireLimit(deps, {
|
|
47
53
|
accountId: grant.accountId,
|
|
48
54
|
workspaceId,
|
|
@@ -50,7 +56,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
50
56
|
quantity: 1,
|
|
51
57
|
});
|
|
52
58
|
const task = await createValidatedScheduledTask({
|
|
53
|
-
settings,
|
|
59
|
+
settings: catalogSettings,
|
|
54
60
|
db,
|
|
55
61
|
objectStorage,
|
|
56
62
|
grant,
|
|
@@ -91,8 +97,14 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
91
97
|
});
|
|
92
98
|
}
|
|
93
99
|
const payload = parsedPayload.data;
|
|
100
|
+
const catalogSettings = (
|
|
101
|
+
await resolveWorkspaceCatalogSettings(db, deps.settings, {
|
|
102
|
+
accountId: grant.accountId,
|
|
103
|
+
workspaceId,
|
|
104
|
+
})
|
|
105
|
+
).settings;
|
|
94
106
|
const update = await validatedScheduledTaskUpdate({
|
|
95
|
-
settings,
|
|
107
|
+
settings: catalogSettings,
|
|
96
108
|
db,
|
|
97
109
|
objectStorage,
|
|
98
110
|
grant,
|
|
@@ -124,8 +136,14 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
124
136
|
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
125
137
|
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
126
138
|
const previous = await captureScheduledTaskRestoreState(db, existing);
|
|
139
|
+
const catalogSettings = (
|
|
140
|
+
await resolveWorkspaceCatalogSettings(db, deps.settings, {
|
|
141
|
+
accountId: grant.accountId,
|
|
142
|
+
workspaceId,
|
|
143
|
+
})
|
|
144
|
+
).settings;
|
|
127
145
|
const update = await validatedScheduledTaskUpdate({
|
|
128
|
-
settings,
|
|
146
|
+
settings: catalogSettings,
|
|
129
147
|
db,
|
|
130
148
|
objectStorage,
|
|
131
149
|
grant,
|
|
@@ -146,6 +164,12 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
146
164
|
// recognised as codex-billed and skip the credit/cost gates at the edge.
|
|
147
165
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
148
166
|
if (task.action.kind === "agent_turn") {
|
|
167
|
+
const catalogSettings = (
|
|
168
|
+
await resolveWorkspaceCatalogSettings(db, deps.settings, {
|
|
169
|
+
accountId: grant.accountId,
|
|
170
|
+
workspaceId,
|
|
171
|
+
})
|
|
172
|
+
).settings;
|
|
149
173
|
await validateScheduledTaskTarget({
|
|
150
174
|
db,
|
|
151
175
|
sessionAuthorization: deps.sessionAuthorization,
|
|
@@ -159,20 +183,23 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
159
183
|
missingTargetStatus: 404,
|
|
160
184
|
});
|
|
161
185
|
await validateScheduledTaskMachineTarget({
|
|
162
|
-
settings,
|
|
186
|
+
settings: catalogSettings,
|
|
163
187
|
db,
|
|
164
188
|
grant,
|
|
165
189
|
runMode: task.runMode,
|
|
166
190
|
agentConfig: task.agentConfig,
|
|
167
191
|
requireOnline: true,
|
|
168
192
|
});
|
|
169
|
-
await requireLimit(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
await requireLimit(
|
|
194
|
+
{ ...deps, settings: catalogSettings },
|
|
195
|
+
{
|
|
196
|
+
accountId: grant.accountId,
|
|
197
|
+
workspaceId,
|
|
198
|
+
action: "agent_run:create",
|
|
199
|
+
quantity: 1,
|
|
200
|
+
model: task.agentConfig.model ?? catalogSettings.openaiModel,
|
|
201
|
+
},
|
|
202
|
+
);
|
|
176
203
|
}
|
|
177
204
|
// Body is optional (a bare POST is still a valid trigger); only a present,
|
|
178
205
|
// non-empty body must parse against the contract.
|