@opengeni/api-router 2.6.4 → 2.10.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/access-grant-rls.d.ts +3 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/organization-user-setup.d.ts +23 -0
- package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
- package/dist/chunk-XXH7DW34.js.map +1 -0
- package/dist/codemode.d.ts +6 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/http/request-source.d.ts +14 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +2 -11
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +158 -1
- package/dist/mcp/session-view.d.ts +56 -2
- package/dist/mcp/session-wait.d.ts +1 -1
- package/dist/mcp-oauth.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +27 -2
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +1 -1
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +118 -0
- package/package.json +19 -18
- package/src/access-grant-rls.ts +40 -0
- package/src/app.ts +296 -53
- package/src/auth/organization-user-setup.ts +95 -5
- package/src/codemode.ts +33 -4
- package/src/github-access.ts +117 -1
- package/src/http/auth.ts +11 -0
- package/src/http/request-source.ts +37 -0
- package/src/http/sse.ts +15 -7
- package/src/index.ts +18 -2
- package/src/integrations/oauth-client.ts +168 -203
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +504 -133
- package/src/mcp/session-view.ts +20 -1
- package/src/mcp/session-wait.ts +3 -0
- package/src/mcp-oauth.ts +539 -0
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +242 -73
- package/src/routes/connections.ts +271 -16
- package/src/routes/documents.ts +67 -19
- package/src/routes/files.ts +54 -6
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +59 -16
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +153 -65
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspace-artifacts.ts +176 -67
- package/src/routes/workspaces.ts +405 -62
- package/src/sandbox/channel-a.ts +17 -4
- package/src/work-discovery-observability.ts +3 -3
- package/src/workspace-artifact-content.ts +163 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-deletion.ts +64 -0
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +691 -0
- 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/documents.ts
CHANGED
|
@@ -70,6 +70,12 @@ import {
|
|
|
70
70
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
71
71
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
72
72
|
import { buildDocumentsMcpServer } from "../mcp/documents";
|
|
73
|
+
import { mcpOAuthAuthenticateHeader, resolveMcpOAuthRouteAccess } from "../mcp-oauth";
|
|
74
|
+
import { withAccessGrantSessionRlsContext } from "../access-grant-rls";
|
|
75
|
+
import {
|
|
76
|
+
buildWorkspaceToolGatewayMcpServer,
|
|
77
|
+
prepareMcpOAuthWorkspaceToolGateway,
|
|
78
|
+
} from "../workspace-tool-gateway";
|
|
73
79
|
import { sanitizeFilename } from "./files";
|
|
74
80
|
|
|
75
81
|
export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
@@ -949,25 +955,67 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
949
955
|
|
|
950
956
|
app.all("/v1/workspaces/:workspaceId/mcp/docs", async (c) => {
|
|
951
957
|
const workspaceId = c.req.param("workspaceId");
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
958
|
+
let oauthAccess: Awaited<ReturnType<typeof resolveMcpOAuthRouteAccess>>;
|
|
959
|
+
try {
|
|
960
|
+
oauthAccess = await resolveMcpOAuthRouteAccess(deps, c.req.raw, workspaceId);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
if (error instanceof HTTPException && error.status === 401) {
|
|
963
|
+
c.header("www-authenticate", mcpOAuthAuthenticateHeader(deps, new URL(c.req.url).pathname));
|
|
964
|
+
}
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
if (oauthAccess) {
|
|
968
|
+
return await withAccessGrantSessionRlsContext(deps, oauthAccess.grant, async () => {
|
|
969
|
+
const prepared = await prepareMcpOAuthWorkspaceToolGateway(
|
|
970
|
+
deps,
|
|
971
|
+
oauthAccess.grant,
|
|
972
|
+
oauthAccess.allowedToolIdentities,
|
|
973
|
+
);
|
|
974
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
975
|
+
enableJsonResponse: true,
|
|
976
|
+
});
|
|
977
|
+
const server = buildWorkspaceToolGatewayMcpServer(
|
|
978
|
+
prepared,
|
|
979
|
+
oauthAccess.grant,
|
|
980
|
+
deps.observability,
|
|
981
|
+
);
|
|
982
|
+
try {
|
|
983
|
+
await server.connect(transport);
|
|
984
|
+
return await transport.handleRequest(c.req.raw);
|
|
985
|
+
} finally {
|
|
986
|
+
await Promise.allSettled([server.close(), prepared.close()]);
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
let grant: Awaited<ReturnType<typeof requireAccessGrant>>;
|
|
991
|
+
try {
|
|
992
|
+
grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
993
|
+
} catch (error) {
|
|
994
|
+
if (deps.settings.mcpOauthEnabled && error instanceof HTTPException && error.status === 401) {
|
|
995
|
+
c.header("www-authenticate", mcpOAuthAuthenticateHeader(deps, new URL(c.req.url).pathname));
|
|
996
|
+
}
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
return await withAccessGrantSessionRlsContext(deps, grant, async () => {
|
|
1000
|
+
const sessionId =
|
|
1001
|
+
typeof grant.metadata?.sessionId === "string" ? grant.metadata.sessionId : undefined;
|
|
1002
|
+
const attemptId =
|
|
1003
|
+
typeof grant.metadata?.attemptId === "string" ? grant.metadata.attemptId : undefined;
|
|
1004
|
+
const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
1005
|
+
const server = buildDocumentsMcpServer(
|
|
1006
|
+
db,
|
|
1007
|
+
grant.accountId,
|
|
1008
|
+
workspaceId,
|
|
1009
|
+
getDocumentServices(),
|
|
1010
|
+
{
|
|
1011
|
+
createdBySessionId: sessionId,
|
|
1012
|
+
attemptId,
|
|
1013
|
+
initiatingSubjectId: grant.subjectId,
|
|
1014
|
+
},
|
|
1015
|
+
);
|
|
1016
|
+
await server.connect(transport);
|
|
1017
|
+
return await transport.handleRequest(c.req.raw);
|
|
1018
|
+
});
|
|
971
1019
|
});
|
|
972
1020
|
}
|
|
973
1021
|
|
package/src/routes/files.ts
CHANGED
|
@@ -46,19 +46,67 @@ import type { ApiRouteDeps } from "@opengeni/core";
|
|
|
46
46
|
import { retryWhileMissing } from "@opengeni/storage";
|
|
47
47
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
48
48
|
import { buildFilesMcpServer } from "../mcp/files";
|
|
49
|
+
import { mcpOAuthAuthenticateHeader, resolveMcpOAuthRouteAccess } from "../mcp-oauth";
|
|
50
|
+
import { withAccessGrantSessionRlsContext } from "../access-grant-rls";
|
|
51
|
+
import {
|
|
52
|
+
buildWorkspaceToolGatewayMcpServer,
|
|
53
|
+
prepareMcpOAuthWorkspaceToolGateway,
|
|
54
|
+
} from "../workspace-tool-gateway";
|
|
49
55
|
|
|
50
56
|
export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
51
57
|
const { db, objectStorage } = deps;
|
|
52
58
|
|
|
53
59
|
app.all("/v1/workspaces/:workspaceId/mcp/files", async (c) => {
|
|
54
60
|
const workspaceId = c.req.param("workspaceId");
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
let oauthAccess: Awaited<ReturnType<typeof resolveMcpOAuthRouteAccess>>;
|
|
62
|
+
try {
|
|
63
|
+
oauthAccess = await resolveMcpOAuthRouteAccess(deps, c.req.raw, workspaceId);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof HTTPException && error.status === 401) {
|
|
66
|
+
c.header("www-authenticate", mcpOAuthAuthenticateHeader(deps, new URL(c.req.url).pathname));
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
if (oauthAccess) {
|
|
71
|
+
return await withAccessGrantSessionRlsContext(deps, oauthAccess.grant, async () => {
|
|
72
|
+
const prepared = await prepareMcpOAuthWorkspaceToolGateway(
|
|
73
|
+
deps,
|
|
74
|
+
oauthAccess.grant,
|
|
75
|
+
oauthAccess.allowedToolIdentities,
|
|
76
|
+
);
|
|
77
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
78
|
+
enableJsonResponse: true,
|
|
79
|
+
});
|
|
80
|
+
const server = buildWorkspaceToolGatewayMcpServer(
|
|
81
|
+
prepared,
|
|
82
|
+
oauthAccess.grant,
|
|
83
|
+
deps.observability,
|
|
84
|
+
);
|
|
85
|
+
try {
|
|
86
|
+
await server.connect(transport);
|
|
87
|
+
return await transport.handleRequest(c.req.raw);
|
|
88
|
+
} finally {
|
|
89
|
+
await Promise.allSettled([server.close(), prepared.close()]);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
let grant: Awaited<ReturnType<typeof requireAccessGrant>>;
|
|
94
|
+
try {
|
|
95
|
+
grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (deps.settings.mcpOauthEnabled && error instanceof HTTPException && error.status === 401) {
|
|
98
|
+
c.header("www-authenticate", mcpOAuthAuthenticateHeader(deps, new URL(c.req.url).pathname));
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
return await withAccessGrantSessionRlsContext(deps, grant, async () => {
|
|
103
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
104
|
+
enableJsonResponse: true,
|
|
105
|
+
});
|
|
106
|
+
const server = buildFilesMcpServer(deps, grant);
|
|
107
|
+
await server.connect(transport);
|
|
108
|
+
return await transport.handleRequest(c.req.raw);
|
|
58
109
|
});
|
|
59
|
-
const server = buildFilesMcpServer(deps, grant);
|
|
60
|
-
await server.connect(transport);
|
|
61
|
-
return await transport.handleRequest(c.req.raw);
|
|
62
110
|
});
|
|
63
111
|
|
|
64
112
|
app.post("/v1/workspaces/:workspaceId/files/uploads", async (c) => {
|