@opengeni/api-router 0.26.1 → 0.30.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 +1 -0
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +19 -1
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/{chunk-JIKNR5YL.js → chunk-2PQMSRCB.js} +15518 -6863
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.js +1 -1
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/google-drive.d.ts +6 -6
- package/dist/integrations/provider-oauth.d.ts +0 -2
- package/dist/integrations/slack-bot.d.ts +171 -1
- package/dist/integrations/slack-interactions.d.ts +17 -5
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/server.d.ts +3 -2
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/sessions.d.ts +2 -1
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +8 -1
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/viewer.d.ts +3 -1
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +17 -16
- package/src/app.ts +352 -49
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +50 -1
- package/src/browser-network-route.ts +34 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/http/auth.ts +27 -9
- package/src/http/sse.ts +229 -5
- package/src/integrations/api-integrations.ts +57 -42
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/google-drive.ts +31 -26
- package/src/integrations/provider-oauth.ts +70 -91
- package/src/integrations/slack-bot.ts +638 -9
- package/src/integrations/slack-interactions.ts +1583 -73
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +115 -6
- package/src/mcp/server.ts +865 -242
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +32 -29
- package/src/routes/browser-identities.ts +24 -0
- package/src/routes/browser-sessions.ts +2122 -183
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/computer-sessions.ts +441 -86
- package/src/routes/connections.ts +141 -4
- package/src/routes/{integration-features.ts → integration-facets.ts} +63 -63
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/plugins.ts +2 -1
- package/src/routes/sessions.ts +419 -43
- package/src/routes/skills.ts +92 -10
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +34 -4
- package/src/routes/workspaces.ts +22 -4
- package/src/sandbox/channel-a.ts +29 -2
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/viewer.ts +10 -3
- package/src/slack-reaction-files.ts +181 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-JIKNR5YL.js.map +0 -1
- package/dist/routes/integration-features.d.ts +0 -3
|
@@ -27,9 +27,12 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
29
29
|
ApiIntegrationOAuthConnectionMetadata,
|
|
30
|
-
|
|
30
|
+
IntegrationFacetMutationResult,
|
|
31
31
|
} from "@opengeni/contracts";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
GOOGLE_DRIVE_INTEGRATION_DEFINITION,
|
|
34
|
+
integrationDefinitionProviderDomain,
|
|
35
|
+
} from "@opengeni/capabilities";
|
|
33
36
|
import {
|
|
34
37
|
captureScheduledTaskRestoreState,
|
|
35
38
|
createValidatedScheduledTask,
|
|
@@ -42,7 +45,7 @@ import {
|
|
|
42
45
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
43
46
|
import {
|
|
44
47
|
buildConnectionTokenResolver,
|
|
45
|
-
|
|
48
|
+
configureIntegrationFacet,
|
|
46
49
|
appendKnowledgeSourceAclVersion,
|
|
47
50
|
deauthorizeKnowledgeSourceRetrieval,
|
|
48
51
|
ConnectionDisconnectGenerationError,
|
|
@@ -58,7 +61,7 @@ import {
|
|
|
58
61
|
getWorkspaceGrant,
|
|
59
62
|
listKnowledgeSourceSyncTasksForConnection,
|
|
60
63
|
loadConnectionCredentialForBroker,
|
|
61
|
-
|
|
64
|
+
listIntegrationInstanceFacets,
|
|
62
65
|
transitionConnectionState,
|
|
63
66
|
updateConnection,
|
|
64
67
|
updateScheduledTask,
|
|
@@ -731,19 +734,19 @@ export async function browseGoogleDrive(
|
|
|
731
734
|
});
|
|
732
735
|
}
|
|
733
736
|
|
|
734
|
-
export async function
|
|
737
|
+
export async function browseGoogleDriveFacetSource(
|
|
735
738
|
deps: ApiRouteDeps,
|
|
736
739
|
input: {
|
|
737
740
|
workspaceId: string;
|
|
738
741
|
subjectId: string;
|
|
739
742
|
capabilityId: string;
|
|
740
743
|
instanceKey: string;
|
|
741
|
-
|
|
744
|
+
facetKey: string;
|
|
742
745
|
parentId: string;
|
|
743
746
|
pageToken?: string | undefined;
|
|
744
747
|
},
|
|
745
748
|
) {
|
|
746
|
-
const context = await
|
|
749
|
+
const context = await requireGoogleDriveIntegrationFacet(deps, input);
|
|
747
750
|
return await browseGoogleDrive(deps, {
|
|
748
751
|
workspaceId: input.workspaceId,
|
|
749
752
|
subjectId: input.subjectId,
|
|
@@ -753,7 +756,7 @@ export async function browseGoogleDriveIntegrationSource(
|
|
|
753
756
|
});
|
|
754
757
|
}
|
|
755
758
|
|
|
756
|
-
export async function
|
|
759
|
+
export async function saveGoogleDriveFacetSource(
|
|
757
760
|
deps: ApiRouteDeps,
|
|
758
761
|
input: {
|
|
759
762
|
accountId: string;
|
|
@@ -761,19 +764,19 @@ export async function saveGoogleDriveIntegrationSource(
|
|
|
761
764
|
subjectId: string;
|
|
762
765
|
capabilityId: string;
|
|
763
766
|
instanceKey: string;
|
|
764
|
-
|
|
767
|
+
facetKey: string;
|
|
765
768
|
payload: unknown;
|
|
766
769
|
canManageOrganizationDestination: boolean;
|
|
767
770
|
canManageWorkspaceDestination: boolean;
|
|
768
771
|
canManagePersonalDestination: boolean;
|
|
769
772
|
},
|
|
770
|
-
): Promise<
|
|
773
|
+
): Promise<IntegrationFacetMutationResult> {
|
|
771
774
|
const parsedPayload = SaveGoogleDriveIntegrationSourceRequest.safeParse(input.payload);
|
|
772
775
|
if (!parsedPayload.success) {
|
|
773
776
|
throw new HTTPException(400, { message: "invalid Google Drive source selection" });
|
|
774
777
|
}
|
|
775
778
|
const payload = parsedPayload.data;
|
|
776
|
-
const context = await
|
|
779
|
+
const context = await requireGoogleDriveIntegrationFacet(deps, input);
|
|
777
780
|
const documentDestination = googleDriveDocumentDestination(input, payload);
|
|
778
781
|
const verifiedSources = await verifyGoogleDriveSources(deps, {
|
|
779
782
|
workspaceId: input.workspaceId,
|
|
@@ -811,16 +814,16 @@ export async function saveGoogleDriveIntegrationSource(
|
|
|
811
814
|
syncCadence: payload.syncCadence,
|
|
812
815
|
readPolicy: payload.readPolicy,
|
|
813
816
|
});
|
|
814
|
-
return
|
|
815
|
-
await
|
|
817
|
+
return IntegrationFacetMutationResult.parse(
|
|
818
|
+
await configureIntegrationFacet(deps.db, {
|
|
816
819
|
accountId: input.accountId,
|
|
817
820
|
workspaceId: input.workspaceId,
|
|
818
821
|
subjectId: input.subjectId,
|
|
819
822
|
capabilityId: input.capabilityId,
|
|
820
823
|
instanceKey: input.instanceKey,
|
|
821
|
-
|
|
824
|
+
facetKey: input.facetKey,
|
|
822
825
|
displayName:
|
|
823
|
-
context.
|
|
826
|
+
context.facet.binding?.displayName ?? `${input.instanceKey} — Google Drive content`,
|
|
824
827
|
config,
|
|
825
828
|
...(payload.expectedVersion !== undefined
|
|
826
829
|
? { expectedVersion: payload.expectedVersion }
|
|
@@ -932,30 +935,30 @@ export async function saveGoogleDriveSource(
|
|
|
932
935
|
return updated;
|
|
933
936
|
}
|
|
934
937
|
|
|
935
|
-
async function
|
|
938
|
+
async function requireGoogleDriveIntegrationFacet(
|
|
936
939
|
deps: ApiRouteDeps,
|
|
937
940
|
input: {
|
|
938
941
|
workspaceId: string;
|
|
939
942
|
subjectId: string;
|
|
940
943
|
capabilityId: string;
|
|
941
944
|
instanceKey: string;
|
|
942
|
-
|
|
945
|
+
facetKey: string;
|
|
943
946
|
},
|
|
944
947
|
) {
|
|
945
|
-
const instance = await
|
|
948
|
+
const instance = await listIntegrationInstanceFacets(
|
|
946
949
|
deps.db,
|
|
947
950
|
input.workspaceId,
|
|
948
951
|
input.subjectId,
|
|
949
952
|
input.capabilityId,
|
|
950
953
|
input.instanceKey,
|
|
951
954
|
);
|
|
952
|
-
const
|
|
953
|
-
(candidate) => candidate.definition.
|
|
955
|
+
const facet = instance.facets.find(
|
|
956
|
+
(candidate) => candidate.definition.facetKey === input.facetKey,
|
|
954
957
|
);
|
|
955
958
|
if (
|
|
956
|
-
!
|
|
957
|
-
|
|
958
|
-
|
|
959
|
+
!facet ||
|
|
960
|
+
facet.definition.kind !== "knowledge_source" ||
|
|
961
|
+
facet.definition.capabilities.provider !== "google-drive"
|
|
959
962
|
) {
|
|
960
963
|
throw new HTTPException(404, { message: "Google Drive knowledge source not found" });
|
|
961
964
|
}
|
|
@@ -972,7 +975,7 @@ async function requireGoogleDriveIntegrationFeature(
|
|
|
972
975
|
throw new HTTPException(404, { message: "Google Drive Connection not found" });
|
|
973
976
|
}
|
|
974
977
|
await requireGoogleDriveApiConnection(deps, connection, input.subjectId);
|
|
975
|
-
return { instance,
|
|
978
|
+
return { instance, facet, connection };
|
|
976
979
|
}
|
|
977
980
|
|
|
978
981
|
function googleDriveDocumentDestination(
|
|
@@ -1531,7 +1534,9 @@ async function requireGoogleDriveApiConnection(
|
|
|
1531
1534
|
): Promise<GoogleDriveApiConnection> {
|
|
1532
1535
|
const legacy = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
|
|
1533
1536
|
const integration = ApiIntegrationOAuthConnectionMetadata.safeParse(connection.metadata);
|
|
1534
|
-
const genericProviderDomain =
|
|
1537
|
+
const genericProviderDomain = integrationDefinitionProviderDomain(
|
|
1538
|
+
GOOGLE_DRIVE_INTEGRATION_DEFINITION,
|
|
1539
|
+
);
|
|
1535
1540
|
const authority: GoogleDriveApiConnection | null =
|
|
1536
1541
|
connection.kind === "oauth2" &&
|
|
1537
1542
|
connection.subjectId === subjectId &&
|
|
@@ -1544,7 +1549,7 @@ async function requireGoogleDriveApiConnection(
|
|
|
1544
1549
|
integration.success &&
|
|
1545
1550
|
integration.data.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE &&
|
|
1546
1551
|
integration.data.providerFamily === "google" &&
|
|
1547
|
-
integration.data.
|
|
1552
|
+
integration.data.authorizedDefinitionIds.includes(GOOGLE_DRIVE_INTEGRATION_DEFINITION.id)
|
|
1548
1553
|
? { kind: "integration", connection, metadata: integration.data }
|
|
1549
1554
|
: null;
|
|
1550
1555
|
if (!authority) {
|
|
@@ -2,9 +2,9 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type
|
|
5
|
+
integrationDefinitionProviderDomain,
|
|
6
|
+
integrationDefinitionById,
|
|
7
|
+
type IntegrationDefinition,
|
|
8
8
|
} from "@opengeni/capabilities";
|
|
9
9
|
import {
|
|
10
10
|
parseIntegrationsOauthClientsJson,
|
|
@@ -60,8 +60,8 @@ type ProviderOAuthState = {
|
|
|
60
60
|
workspaceId: string;
|
|
61
61
|
subjectId: string;
|
|
62
62
|
ownership: ConnectionOwnership;
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
definitionId: string;
|
|
64
|
+
definitionFingerprint: string;
|
|
65
65
|
providerDomain: string;
|
|
66
66
|
authorizeScopes: string[];
|
|
67
67
|
encryptedPkceVerifier: string;
|
|
@@ -121,8 +121,8 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
121
121
|
payload: ApiIntegrationOAuthStartRequest;
|
|
122
122
|
},
|
|
123
123
|
): Promise<OAuthStartResponse> {
|
|
124
|
-
const
|
|
125
|
-
const providerDomain =
|
|
124
|
+
const definition = requiredDefinition(input.payload.definitionId);
|
|
125
|
+
const providerDomain = integrationDefinitionProviderDomain(definition);
|
|
126
126
|
const existing = input.payload.connectionId
|
|
127
127
|
? await getConnectionMetadata(
|
|
128
128
|
deps.db,
|
|
@@ -138,7 +138,7 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
138
138
|
? requireProviderOAuthConnection(existing, {
|
|
139
139
|
subjectId: input.subjectId,
|
|
140
140
|
providerDomain,
|
|
141
|
-
providerFamily:
|
|
141
|
+
providerFamily: definition.provider.id,
|
|
142
142
|
})
|
|
143
143
|
: null;
|
|
144
144
|
const existingOwnership: ConnectionOwnership | null = existing
|
|
@@ -158,13 +158,13 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
158
158
|
const ownership = existingOwnership ?? input.payload.ownership ?? "personal";
|
|
159
159
|
const authorizeScopes = uniqueStrings([
|
|
160
160
|
...(existing?.grantedScopes ?? []),
|
|
161
|
-
...
|
|
161
|
+
...definition.authentication.scopes,
|
|
162
162
|
]);
|
|
163
|
-
const client =
|
|
163
|
+
const client = providerClientForDefinition(deps.settings, definition);
|
|
164
164
|
const verifier = randomBytes(48).toString("base64url");
|
|
165
165
|
const key = requireEnvironmentEncryption(deps.settings);
|
|
166
166
|
const baseUrl = integrationBaseUrl(deps.settings.publicBaseUrl, input.requestUrl);
|
|
167
|
-
const redirectUri = `${baseUrl}${
|
|
167
|
+
const redirectUri = `${baseUrl}${PROVIDER_OAUTH_CALLBACK_PATH}`;
|
|
168
168
|
const returnPath = safeReturnPath(
|
|
169
169
|
input.payload.returnPath ?? `/workspaces/${input.workspaceId}/capabilities`,
|
|
170
170
|
);
|
|
@@ -173,8 +173,8 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
173
173
|
workspaceId: input.workspaceId,
|
|
174
174
|
subjectId: input.subjectId,
|
|
175
175
|
ownership,
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
definitionId: definition.id,
|
|
177
|
+
definitionFingerprint: providerDefinitionFingerprint(definition),
|
|
178
178
|
providerDomain,
|
|
179
179
|
authorizeScopes,
|
|
180
180
|
encryptedPkceVerifier: encryptEnvironmentValue(key, verifier),
|
|
@@ -189,7 +189,7 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
189
189
|
}
|
|
190
190
|
: {}),
|
|
191
191
|
});
|
|
192
|
-
const authorizationUrl = new URL(
|
|
192
|
+
const authorizationUrl = new URL(definition.authentication.authorizationUrl);
|
|
193
193
|
authorizationUrl.searchParams.set("response_type", "code");
|
|
194
194
|
authorizationUrl.searchParams.set("client_id", client.clientId);
|
|
195
195
|
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
|
@@ -201,7 +201,7 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
201
201
|
createHash("sha256").update(verifier).digest("base64url"),
|
|
202
202
|
);
|
|
203
203
|
authorizationUrl.searchParams.set("prompt", "select_account");
|
|
204
|
-
if (
|
|
204
|
+
if (definition.provider.id === "google") {
|
|
205
205
|
authorizationUrl.searchParams.set("access_type", "offline");
|
|
206
206
|
authorizationUrl.searchParams.set("include_granted_scopes", "true");
|
|
207
207
|
authorizationUrl.searchParams.set("prompt", "consent select_account");
|
|
@@ -240,16 +240,16 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
240
240
|
if (input.error) throw new ProviderOAuthCallbackError("provider_denied");
|
|
241
241
|
if (!input.code) throw new ProviderOAuthCallbackError("missing_code");
|
|
242
242
|
|
|
243
|
-
const
|
|
244
|
-
if (!
|
|
243
|
+
const definition = integrationDefinitionById(state.definitionId);
|
|
244
|
+
if (!definition) throw new ProviderOAuthCallbackError("state_invalid");
|
|
245
245
|
if (
|
|
246
|
-
state.
|
|
247
|
-
state.providerDomain !==
|
|
248
|
-
|
|
246
|
+
state.definitionFingerprint !== providerDefinitionFingerprint(definition) ||
|
|
247
|
+
state.providerDomain !== integrationDefinitionProviderDomain(definition) ||
|
|
248
|
+
definition.authentication.scopes.some((scope) => !state!.authorizeScopes.includes(scope))
|
|
249
249
|
) {
|
|
250
250
|
throw new ProviderOAuthCallbackError("state_invalid");
|
|
251
251
|
}
|
|
252
|
-
const client =
|
|
252
|
+
const client = providerClientForDefinition(deps.settings, definition);
|
|
253
253
|
if (
|
|
254
254
|
client.clientId !== state.clientId ||
|
|
255
255
|
client.tokenEndpointAuthMethod !== state.tokenEndpointAuthMethod
|
|
@@ -258,17 +258,17 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
258
258
|
}
|
|
259
259
|
const key = requireEnvironmentEncryption(deps.settings);
|
|
260
260
|
const verifier = decryptEnvironmentValue(key, state.encryptedPkceVerifier);
|
|
261
|
-
const redirectUri = `${apiBaseUrl}${
|
|
262
|
-
const token = await exchangeProviderAuthorizationCode(deps,
|
|
261
|
+
const redirectUri = `${apiBaseUrl}${PROVIDER_OAUTH_CALLBACK_PATH}`;
|
|
262
|
+
const token = await exchangeProviderAuthorizationCode(deps, definition, client, {
|
|
263
263
|
code: input.code,
|
|
264
264
|
verifier,
|
|
265
265
|
redirectUri,
|
|
266
266
|
});
|
|
267
|
-
if (!providerScopesInclude(
|
|
267
|
+
if (!providerScopesInclude(definition, token.scopes, state.authorizeScopes)) {
|
|
268
268
|
throw new ProviderOAuthCallbackError("scope_not_granted");
|
|
269
269
|
}
|
|
270
270
|
const grantedScopes = token.scopes.length > 0 ? token.scopes : state.authorizeScopes;
|
|
271
|
-
const identity = await verifyProviderIdentity(deps,
|
|
271
|
+
const identity = await verifyProviderIdentity(deps, definition, token);
|
|
272
272
|
if (
|
|
273
273
|
state.expectedProviderPrincipalId &&
|
|
274
274
|
state.expectedProviderPrincipalId !== identity.principalId
|
|
@@ -287,7 +287,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
287
287
|
? requireProviderOAuthConnection(existing, {
|
|
288
288
|
subjectId: state.subjectId,
|
|
289
289
|
providerDomain: state.providerDomain,
|
|
290
|
-
providerFamily:
|
|
290
|
+
providerFamily: definition.provider.id,
|
|
291
291
|
})
|
|
292
292
|
: null;
|
|
293
293
|
if (existingMetadata && existingMetadata.providerPrincipalId !== identity.principalId) {
|
|
@@ -315,7 +315,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
315
315
|
token_type: token.tokenType,
|
|
316
316
|
...(token.expiresAt ? { expires_at: token.expiresAt.toISOString() } : {}),
|
|
317
317
|
scope: grantedScopes.join(" "),
|
|
318
|
-
token_endpoint:
|
|
318
|
+
token_endpoint: definition.authentication.tokenUrl,
|
|
319
319
|
client_id: client.clientId,
|
|
320
320
|
...(client.clientSecret
|
|
321
321
|
? {
|
|
@@ -329,13 +329,13 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
329
329
|
const metadata = ApiIntegrationOAuthConnectionMetadata.parse({
|
|
330
330
|
...(existing?.metadata ?? {}),
|
|
331
331
|
credentialRole: API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
332
|
-
providerFamily:
|
|
332
|
+
providerFamily: definition.provider.id,
|
|
333
333
|
providerPrincipalId: identity.principalId,
|
|
334
334
|
providerEmail: identity.email,
|
|
335
335
|
providerDisplayName: identity.displayName,
|
|
336
|
-
|
|
337
|
-
...(existingMetadata?.
|
|
338
|
-
|
|
336
|
+
authorizedDefinitionIds: uniqueStrings([
|
|
337
|
+
...(existingMetadata?.authorizedDefinitionIds ?? []),
|
|
338
|
+
definition.id,
|
|
339
339
|
]),
|
|
340
340
|
verifiedAt: new Date().toISOString(),
|
|
341
341
|
});
|
|
@@ -354,7 +354,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
354
354
|
createdBySubjectId: state.subjectId,
|
|
355
355
|
updatedBySubjectId: state.subjectId,
|
|
356
356
|
credentialRole: API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
357
|
-
providerFamily:
|
|
357
|
+
providerFamily: definition.provider.id,
|
|
358
358
|
providerPrincipalId: identity.principalId,
|
|
359
359
|
...(state.connectionId
|
|
360
360
|
? {
|
|
@@ -366,7 +366,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
366
366
|
if (!connection) throw new ProviderOAuthCallbackError("connection_conflict");
|
|
367
367
|
return {
|
|
368
368
|
redirectTo: providerOAuthReturnUrl(returnBaseUrl, state.returnPath, "success", {
|
|
369
|
-
|
|
369
|
+
definitionId: definition.id,
|
|
370
370
|
connectionId: connection.id,
|
|
371
371
|
providerDomain: connection.providerDomain,
|
|
372
372
|
ownership: state.ownership,
|
|
@@ -379,7 +379,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
379
379
|
state?.returnPath ?? "/integrations",
|
|
380
380
|
"error",
|
|
381
381
|
{
|
|
382
|
-
...(state?.
|
|
382
|
+
...(state?.definitionId ? { definitionId: state.definitionId } : {}),
|
|
383
383
|
reason: providerOAuthFailureReason(error),
|
|
384
384
|
},
|
|
385
385
|
),
|
|
@@ -387,39 +387,18 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
function
|
|
391
|
-
const
|
|
392
|
-
if (!
|
|
393
|
-
return
|
|
390
|
+
function requiredDefinition(id: string): IntegrationDefinition {
|
|
391
|
+
const definition = integrationDefinitionById(id);
|
|
392
|
+
if (!definition) throw new HTTPException(404, { message: "Unknown Integration definition" });
|
|
393
|
+
return definition;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
|
|
397
|
-
value: string | undefined,
|
|
396
|
+
function providerClientForDefinition(
|
|
398
397
|
settings: Settings,
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
readProviderOAuthState(value, settings);
|
|
402
|
-
return true;
|
|
403
|
-
} catch {
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function providerOAuthCallbackPath(preset: OpenApiProviderPreset): string {
|
|
409
|
-
// The existing Google OAuth client is already registered against this
|
|
410
|
-
// loopback callback. Keep that stable while the handler below distinguishes
|
|
411
|
-
// legacy Drive state from the protocol-neutral provider state.
|
|
412
|
-
return preset.family === "google"
|
|
413
|
-
? "/v1/integrations/google-drive/callback"
|
|
414
|
-
: PROVIDER_OAUTH_CALLBACK_PATH;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function providerClientForPreset(
|
|
418
|
-
settings: Settings,
|
|
419
|
-
preset: OpenApiProviderPreset,
|
|
398
|
+
definition: IntegrationDefinition,
|
|
420
399
|
): ProviderOAuthClient {
|
|
421
400
|
const configured = parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
|
|
422
|
-
const candidates = providerClientKeys(
|
|
401
|
+
const candidates = providerClientKeys(definition);
|
|
423
402
|
let entry: IntegrationOAuthClientConfig | undefined;
|
|
424
403
|
for (const candidate of candidates) {
|
|
425
404
|
entry = configured[candidate] ?? configured[candidate.replace(/\/+$/, "")];
|
|
@@ -433,7 +412,7 @@ function providerClientForPreset(
|
|
|
433
412
|
}
|
|
434
413
|
if (
|
|
435
414
|
!entry &&
|
|
436
|
-
|
|
415
|
+
definition.provider.id === "google" &&
|
|
437
416
|
settings.googleDriveClientId?.trim() &&
|
|
438
417
|
settings.googleDriveClientSecret?.trim()
|
|
439
418
|
) {
|
|
@@ -445,7 +424,7 @@ function providerClientForPreset(
|
|
|
445
424
|
}
|
|
446
425
|
if (!entry) {
|
|
447
426
|
throw new HTTPException(503, {
|
|
448
|
-
message: `${
|
|
427
|
+
message: `${definition.name} requires an operator OAuth client configuration`,
|
|
449
428
|
});
|
|
450
429
|
}
|
|
451
430
|
return {
|
|
@@ -455,15 +434,15 @@ function providerClientForPreset(
|
|
|
455
434
|
};
|
|
456
435
|
}
|
|
457
436
|
|
|
458
|
-
function providerClientKeys(
|
|
459
|
-
const authorization = new URL(
|
|
460
|
-
const token = new URL(
|
|
437
|
+
function providerClientKeys(definition: IntegrationDefinition): string[] {
|
|
438
|
+
const authorization = new URL(definition.authentication.authorizationUrl);
|
|
439
|
+
const token = new URL(definition.authentication.tokenUrl);
|
|
461
440
|
return uniqueStrings([
|
|
462
|
-
|
|
441
|
+
definition.authentication.authorizationUrl,
|
|
463
442
|
authorization.origin,
|
|
464
|
-
|
|
443
|
+
definition.authentication.tokenUrl,
|
|
465
444
|
token.origin,
|
|
466
|
-
...(
|
|
445
|
+
...(definition.provider.id === "google"
|
|
467
446
|
? ["https://accounts.google.com"]
|
|
468
447
|
: [
|
|
469
448
|
"https://login.microsoftonline.com/common/v2.0",
|
|
@@ -473,16 +452,16 @@ function providerClientKeys(preset: OpenApiProviderPreset): string[] {
|
|
|
473
452
|
]);
|
|
474
453
|
}
|
|
475
454
|
|
|
476
|
-
function
|
|
455
|
+
function providerDefinitionFingerprint(definition: IntegrationDefinition): string {
|
|
477
456
|
return createHash("sha256")
|
|
478
457
|
.update(
|
|
479
458
|
JSON.stringify({
|
|
480
|
-
id:
|
|
481
|
-
|
|
482
|
-
baseUrl:
|
|
483
|
-
authorizationUrl:
|
|
484
|
-
tokenUrl:
|
|
485
|
-
scopes: [...
|
|
459
|
+
id: definition.id,
|
|
460
|
+
provider: definition.provider,
|
|
461
|
+
baseUrl: definition.baseUrl,
|
|
462
|
+
authorizationUrl: definition.authentication.authorizationUrl,
|
|
463
|
+
tokenUrl: definition.authentication.tokenUrl,
|
|
464
|
+
scopes: [...definition.authentication.scopes],
|
|
486
465
|
}),
|
|
487
466
|
)
|
|
488
467
|
.digest("hex");
|
|
@@ -524,8 +503,8 @@ function readProviderOAuthState(raw: string | undefined, settings: Settings): Pr
|
|
|
524
503
|
workspaceId: requiredStateString(payload.workspaceId),
|
|
525
504
|
subjectId: requiredStateString(payload.subjectId),
|
|
526
505
|
ownership,
|
|
527
|
-
|
|
528
|
-
|
|
506
|
+
definitionId: requiredStateString(payload.definitionId),
|
|
507
|
+
definitionFingerprint: requiredStateString(payload.definitionFingerprint),
|
|
529
508
|
providerDomain: requiredStateString(payload.providerDomain),
|
|
530
509
|
authorizeScopes,
|
|
531
510
|
encryptedPkceVerifier: requiredStateString(payload.encryptedPkceVerifier),
|
|
@@ -543,7 +522,7 @@ function readProviderOAuthState(raw: string | undefined, settings: Settings): Pr
|
|
|
543
522
|
|
|
544
523
|
async function exchangeProviderAuthorizationCode(
|
|
545
524
|
deps: ApiRouteDeps,
|
|
546
|
-
|
|
525
|
+
definition: IntegrationDefinition,
|
|
547
526
|
client: ProviderOAuthClient,
|
|
548
527
|
input: { code: string; verifier: string; redirectUri: string },
|
|
549
528
|
): Promise<ProviderToken> {
|
|
@@ -565,7 +544,7 @@ async function exchangeProviderAuthorizationCode(
|
|
|
565
544
|
} else {
|
|
566
545
|
body.set("client_id", client.clientId);
|
|
567
546
|
}
|
|
568
|
-
const response = await providerFetch(deps,
|
|
547
|
+
const response = await providerFetch(deps, definition.authentication.tokenUrl, {
|
|
569
548
|
method: "POST",
|
|
570
549
|
headers,
|
|
571
550
|
body,
|
|
@@ -577,7 +556,7 @@ async function exchangeProviderAuthorizationCode(
|
|
|
577
556
|
const payload = await readResponseJsonBounded<Record<string, unknown>>(
|
|
578
557
|
response,
|
|
579
558
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
580
|
-
`${
|
|
559
|
+
`${definition.name} OAuth token response`,
|
|
581
560
|
).catch(() => {
|
|
582
561
|
throw new ProviderOAuthCallbackError("token_exchange_failed");
|
|
583
562
|
});
|
|
@@ -597,10 +576,10 @@ async function exchangeProviderAuthorizationCode(
|
|
|
597
576
|
|
|
598
577
|
async function verifyProviderIdentity(
|
|
599
578
|
deps: ApiRouteDeps,
|
|
600
|
-
|
|
579
|
+
definition: IntegrationDefinition,
|
|
601
580
|
token: ProviderToken,
|
|
602
581
|
): Promise<ProviderIdentity> {
|
|
603
|
-
const url =
|
|
582
|
+
const url = definition.provider.id === "google" ? GOOGLE_USERINFO_URL : MICROSOFT_USERINFO_URL;
|
|
604
583
|
const response = await providerFetch(deps, url, {
|
|
605
584
|
headers: {
|
|
606
585
|
accept: "application/json",
|
|
@@ -614,11 +593,11 @@ async function verifyProviderIdentity(
|
|
|
614
593
|
const payload = await readResponseJsonBounded<Record<string, unknown>>(
|
|
615
594
|
response,
|
|
616
595
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
617
|
-
`${
|
|
596
|
+
`${definition.name} identity response`,
|
|
618
597
|
).catch(() => {
|
|
619
598
|
throw new ProviderOAuthCallbackError("identity_verification_failed");
|
|
620
599
|
});
|
|
621
|
-
if (
|
|
600
|
+
if (definition.provider.id === "google") {
|
|
622
601
|
const principalId = optionalString(payload.sub);
|
|
623
602
|
if (!principalId) throw new ProviderOAuthCallbackError("identity_verification_failed");
|
|
624
603
|
return {
|
|
@@ -683,7 +662,7 @@ function requireProviderOAuthConnection(
|
|
|
683
662
|
input: {
|
|
684
663
|
subjectId: string;
|
|
685
664
|
providerDomain: string;
|
|
686
|
-
providerFamily:
|
|
665
|
+
providerFamily: IntegrationDefinition["provider"]["id"];
|
|
687
666
|
},
|
|
688
667
|
) {
|
|
689
668
|
const metadata = ApiIntegrationOAuthConnectionMetadata.safeParse(connection.metadata);
|
|
@@ -695,22 +674,22 @@ function requireProviderOAuthConnection(
|
|
|
695
674
|
metadata.data.providerFamily !== input.providerFamily
|
|
696
675
|
) {
|
|
697
676
|
throw new HTTPException(422, {
|
|
698
|
-
message: "Connection is not compatible with this Integration
|
|
677
|
+
message: "Connection is not compatible with this Integration definition",
|
|
699
678
|
});
|
|
700
679
|
}
|
|
701
680
|
return metadata.data;
|
|
702
681
|
}
|
|
703
682
|
|
|
704
683
|
function providerScopesInclude(
|
|
705
|
-
|
|
684
|
+
definition: IntegrationDefinition,
|
|
706
685
|
returnedScopes: string[],
|
|
707
686
|
fallbackScopes: string[],
|
|
708
687
|
): boolean {
|
|
709
688
|
const effective = returnedScopes.length > 0 ? returnedScopes : fallbackScopes;
|
|
710
689
|
const normalize = (value: string) =>
|
|
711
|
-
|
|
690
|
+
definition.provider.id === "microsoft" ? value.toLowerCase() : value;
|
|
712
691
|
const granted = new Set(effective.map(normalize));
|
|
713
|
-
return
|
|
692
|
+
return definition.authentication.scopes.every((scope) => granted.has(normalize(scope)));
|
|
714
693
|
}
|
|
715
694
|
|
|
716
695
|
function providerOAuthReturnUrl(
|