@opengeni/api-router 3.0.0-canary.0 → 3.0.1-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.js +1 -1
- package/dist/{chunk-7IAWEYBH.js → chunk-XZGTKYOA.js} +3626 -2525
- package/dist/{chunk-7IAWEYBH.js.map → chunk-XZGTKYOA.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +2 -2
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +118 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/package.json +19 -19
- package/src/app.ts +7 -2
- package/src/integrations/atlassian.ts +135 -65
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +34 -15
- package/src/integrations/google-drive.ts +295 -124
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +220 -65
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +54 -31
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +23 -6
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/billing.ts +52 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/connect.ts +161 -57
- package/src/routes/connections.ts +430 -305
- package/src/routes/github.ts +48 -19
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/plugins.ts +12 -2
- package/src/routes/pr-review-github.ts +60 -30
- package/src/routes/pr-review.ts +69 -45
- package/src/routes/social.ts +24 -13
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { safeReturnPath } from "./oauth-return-path";
|
|
2
|
+
import { assertOrganizationIntegrationAllowed, stableJson } from "@opengeni/contracts";
|
|
3
|
+
import {
|
|
4
|
+
withOrganizationIntegrationAcquisition,
|
|
5
|
+
withOrganizationIntegrationPolicyFence,
|
|
6
|
+
} from "@opengeni/db/organization-integration-policy";
|
|
2
7
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
8
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
9
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
@@ -9,7 +14,7 @@ import {
|
|
|
9
14
|
type ConnectionOwnership,
|
|
10
15
|
type OAuthStartRequest,
|
|
11
16
|
} from "@opengeni/contracts";
|
|
12
|
-
import { requireEnvironmentEncryption } from "@opengeni/core";
|
|
17
|
+
import { requireEnvironmentEncryption, integrationKeyForConnectProvider } from "@opengeni/core";
|
|
13
18
|
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
14
19
|
import type { Observability } from "@opengeni/observability";
|
|
15
20
|
import {
|
|
@@ -63,6 +68,7 @@ import { ApiHttpError } from "../http/api-error";
|
|
|
63
68
|
import { requireConnectOwnerAuthority } from "./connect-authority";
|
|
64
69
|
import {
|
|
65
70
|
DEFAULT_OAUTH_PROFILE,
|
|
71
|
+
OFFICIAL_GMAIL_MCP_URL,
|
|
66
72
|
DEPLOYMENT_MANAGED_CLIENTS,
|
|
67
73
|
builtInOAuthProfileByKey,
|
|
68
74
|
assertAuthorizationServerNotReserved,
|
|
@@ -95,6 +101,8 @@ type OAuthClientDeps = {
|
|
|
95
101
|
};
|
|
96
102
|
|
|
97
103
|
export type OAuthStartContext = {
|
|
104
|
+
/** Server-selected dedicated adapter identity; never copied from request metadata. */
|
|
105
|
+
integrationKey?: "gmail" | "slack-personal";
|
|
98
106
|
connectAttemptId?: string;
|
|
99
107
|
externalContinuation?: ExternalActorContinuation;
|
|
100
108
|
accountId: string;
|
|
@@ -129,6 +137,7 @@ type OAuthClientRegistration = {
|
|
|
129
137
|
};
|
|
130
138
|
|
|
131
139
|
type OAuthStatePayload = {
|
|
140
|
+
integrationKey?: "gmail" | "slack-personal";
|
|
132
141
|
connectAttemptId?: string;
|
|
133
142
|
externalContinuation?: ExternalActorContinuation;
|
|
134
143
|
returnUrl?: string;
|
|
@@ -312,10 +321,90 @@ class OAuthCallbackDeadline {
|
|
|
312
321
|
}
|
|
313
322
|
}
|
|
314
323
|
|
|
324
|
+
/** Policy is the outermost lock. Receipt replay still runs normal authorization,
|
|
325
|
+
* but the DB calls authorizeAcquisition only for uncommitted progress. Provider
|
|
326
|
+
* requests must happen after this transaction, never inside either wrapper. */
|
|
327
|
+
export async function claimOAuthAcquisition(
|
|
328
|
+
db: Database,
|
|
329
|
+
scope: Parameters<typeof claimConnectOperation>[1],
|
|
330
|
+
input: Parameters<typeof claimConnectOperation>[2],
|
|
331
|
+
integrationKey: string | null,
|
|
332
|
+
acquiring = true,
|
|
333
|
+
) {
|
|
334
|
+
if (!acquiring) return claimConnectOperation(db, scope, input);
|
|
335
|
+
return withOrganizationIntegrationPolicyFence(db, scope, (tx, policy) =>
|
|
336
|
+
claimConnectOperation(tx, scope, {
|
|
337
|
+
...input,
|
|
338
|
+
authorizeAcquisition: async (locked, attempt) => {
|
|
339
|
+
assertOrganizationIntegrationAllowed(policy, integrationKey);
|
|
340
|
+
await input.authorizeAcquisition?.(locked, attempt);
|
|
341
|
+
},
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
type IntegrationSourceSelection = Record<string, unknown> & {
|
|
347
|
+
id: string;
|
|
348
|
+
syncEnabled: boolean;
|
|
349
|
+
readPolicy: "allow" | "ask" | "block";
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/** Compare server-bound selections, not provider classification. Removing a
|
|
353
|
+
* source, disabling sync, or narrowing its read permission acquires no authority.
|
|
354
|
+
* Generation/timestamp bookkeeping does not turn an unchanged save into setup. */
|
|
355
|
+
export function integrationSourceSelectionRequiresAcquisition(
|
|
356
|
+
previous: readonly IntegrationSourceSelection[],
|
|
357
|
+
requested: readonly IntegrationSourceSelection[],
|
|
358
|
+
): boolean {
|
|
359
|
+
const rank = { block: 0, ask: 1, allow: 2 };
|
|
360
|
+
const config = (source: IntegrationSourceSelection) => {
|
|
361
|
+
const {
|
|
362
|
+
selectedAt: _selectedAt,
|
|
363
|
+
configGeneration: _generation,
|
|
364
|
+
syncEnabled: _enabled,
|
|
365
|
+
readPolicy: _readPolicy,
|
|
366
|
+
...binding
|
|
367
|
+
} = source;
|
|
368
|
+
return stableJson(binding);
|
|
369
|
+
};
|
|
370
|
+
return requested.some((source) => {
|
|
371
|
+
const existing = previous.find((candidate) => candidate.id === source.id);
|
|
372
|
+
return (
|
|
373
|
+
!existing ||
|
|
374
|
+
(!existing.syncEnabled && source.syncEnabled) ||
|
|
375
|
+
rank[source.readPolicy] > rank[existing.readPolicy] ||
|
|
376
|
+
config(existing) !== config(source)
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function finishOAuthAcquisition(
|
|
382
|
+
db: Database,
|
|
383
|
+
scope: Parameters<typeof finishConnectOperation>[1],
|
|
384
|
+
input: Parameters<typeof finishConnectOperation>[2],
|
|
385
|
+
integrationKey: string | null,
|
|
386
|
+
) {
|
|
387
|
+
return withOrganizationIntegrationPolicyFence(db, scope, (tx, policy) =>
|
|
388
|
+
finishConnectOperation(tx, scope, {
|
|
389
|
+
...input,
|
|
390
|
+
authorizeAcquisition: async (locked, attempt) => {
|
|
391
|
+
assertOrganizationIntegrationAllowed(policy, integrationKey);
|
|
392
|
+
await input.authorizeAcquisition?.(locked, attempt);
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
315
398
|
export async function startMcpOAuth(
|
|
316
399
|
deps: OAuthClientDeps,
|
|
317
400
|
context: OAuthStartContext,
|
|
318
401
|
): Promise<OAuthStartResponse> {
|
|
402
|
+
await withOrganizationIntegrationAcquisition(
|
|
403
|
+
deps.db,
|
|
404
|
+
context,
|
|
405
|
+
[context.integrationKey ?? "custom:mcp"],
|
|
406
|
+
async () => {},
|
|
407
|
+
);
|
|
319
408
|
const deadline = new OAuthStartDeadline(deps.oauthStartDeadlineMs ?? OAUTH_START_DEADLINE_MS);
|
|
320
409
|
try {
|
|
321
410
|
return await startMcpOAuthWithinDeadline(deps, context, deadline);
|
|
@@ -439,10 +528,12 @@ async function startMcpOAuthWithinDeadline(
|
|
|
439
528
|
context.payload.oauthClient,
|
|
440
529
|
profile,
|
|
441
530
|
signal,
|
|
531
|
+
context,
|
|
442
532
|
),
|
|
443
533
|
);
|
|
444
534
|
const key = requireEnvironmentEncryption(settings);
|
|
445
535
|
const state = createSignedState(requireIntegrationsStateSecret(settings), {
|
|
536
|
+
...(context.integrationKey ? { integrationKey: context.integrationKey } : {}),
|
|
446
537
|
...(context.connectAttemptId ? { connectAttemptId: context.connectAttemptId } : {}),
|
|
447
538
|
accountId: context.accountId,
|
|
448
539
|
workspaceId: context.workspaceId,
|
|
@@ -627,6 +718,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
627
718
|
): Promise<OAuthCallbackResult> {
|
|
628
719
|
const { db, settings, observability } = deps;
|
|
629
720
|
let state: OAuthStatePayload | null = null;
|
|
721
|
+
let integrationKey: string | null = "custom:mcp";
|
|
630
722
|
let connectOperation: { attemptId: string; operationId: string; inputDigest: string } | undefined;
|
|
631
723
|
if (!input.state) {
|
|
632
724
|
const error = new OAuthCallbackStageError(
|
|
@@ -644,10 +736,22 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
644
736
|
}
|
|
645
737
|
try {
|
|
646
738
|
state = readOAuthState(input.state, settings);
|
|
739
|
+
integrationKey = state.integrationKey ?? "custom:mcp";
|
|
647
740
|
if (state.connectAttemptId) {
|
|
648
741
|
const stored = await getConnectAttempt(db, state, state.connectAttemptId);
|
|
742
|
+
// Native Gmail setup uses its own provider id, but shares this callback.
|
|
743
|
+
// Bind that id to the exact reviewed personal Gmail destination before
|
|
744
|
+
// claiming an operation (including receipt replay), not to arbitrary MCP.
|
|
745
|
+
const gmailAttemptMatches =
|
|
746
|
+
stored.attempt.providerId === "gmail" &&
|
|
747
|
+
state.mcpUrl === OFFICIAL_GMAIL_MCP_URL &&
|
|
748
|
+
state.providerDomain === "gmailmcp.googleapis.com" &&
|
|
749
|
+
builtInOAuthProfileFor(state)?.key === "official-gmail" &&
|
|
750
|
+
state.ownership === "personal" &&
|
|
751
|
+
personalOwnerStateAccepted(state);
|
|
649
752
|
if (
|
|
650
|
-
!["mcp-oauth", "slack-personal"].includes(stored.attempt.providerId)
|
|
753
|
+
(!["mcp-oauth", "slack-personal"].includes(stored.attempt.providerId) &&
|
|
754
|
+
!gmailAttemptMatches) ||
|
|
651
755
|
stored.attempt.ownership !== state.ownership ||
|
|
652
756
|
stored.returnUrl !== state.returnUrl
|
|
653
757
|
)
|
|
@@ -657,12 +761,19 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
657
761
|
operationId: `oauth:${state.nonce}`,
|
|
658
762
|
inputDigest: createHash("sha256").update(input.state).digest("hex"),
|
|
659
763
|
};
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
764
|
+
integrationKey = integrationKeyForConnectProvider(stored.attempt.providerId);
|
|
765
|
+
const claim = await claimOAuthAcquisition(
|
|
766
|
+
db,
|
|
767
|
+
state,
|
|
768
|
+
{
|
|
769
|
+
...connectOperation,
|
|
770
|
+
expectedRevision: stored.attempt.revision,
|
|
771
|
+
authorize: (tx, _attempt, origin) =>
|
|
772
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
773
|
+
},
|
|
774
|
+
integrationKey,
|
|
775
|
+
Boolean(input.code),
|
|
776
|
+
);
|
|
666
777
|
if (claim.status === "replayed") return { redirectTo: stored.returnUrl, exactReturn: true };
|
|
667
778
|
}
|
|
668
779
|
// Fence state minted by an older deployment too: a rolling update must not
|
|
@@ -711,6 +822,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
711
822
|
reason: "missing_code",
|
|
712
823
|
});
|
|
713
824
|
}
|
|
825
|
+
await withOrganizationIntegrationAcquisition(db, state, [integrationKey], async () => {});
|
|
714
826
|
const consumed = await runCallbackDatabaseStage(
|
|
715
827
|
deadline,
|
|
716
828
|
"state_verify",
|
|
@@ -843,38 +955,45 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
843
955
|
};
|
|
844
956
|
if (connectOperation) {
|
|
845
957
|
await runCallbackDatabaseStage(deadline, "persist", db, (scopedDb) =>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
958
|
+
finishOAuthAcquisition(
|
|
959
|
+
scopedDb,
|
|
960
|
+
state!,
|
|
961
|
+
{
|
|
962
|
+
...connectOperation!,
|
|
963
|
+
authorize: (tx, _attempt, origin) =>
|
|
964
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
965
|
+
commit: async (tx, current) => {
|
|
966
|
+
const connection = await persist(tx);
|
|
967
|
+
if (!connection)
|
|
968
|
+
throw new HTTPException(409, {
|
|
969
|
+
message: "connection changed during OAuth reconnect",
|
|
970
|
+
});
|
|
971
|
+
return {
|
|
972
|
+
...current,
|
|
973
|
+
revision: current.revision + 1,
|
|
974
|
+
state: "complete",
|
|
975
|
+
completionRequirement: "connection",
|
|
976
|
+
credentialsCommitted: true,
|
|
977
|
+
nextAction: { type: "none" },
|
|
978
|
+
account: {
|
|
979
|
+
id: connection.id,
|
|
980
|
+
version: connection.version,
|
|
981
|
+
providerId: current.providerId,
|
|
982
|
+
label: state!.providerDomain,
|
|
983
|
+
ownership: current.ownership,
|
|
984
|
+
status: "connected",
|
|
985
|
+
},
|
|
986
|
+
};
|
|
987
|
+
},
|
|
872
988
|
},
|
|
873
|
-
|
|
989
|
+
integrationKey,
|
|
990
|
+
),
|
|
874
991
|
);
|
|
875
992
|
return { redirectTo: state.returnUrl!, exactReturn: true };
|
|
876
993
|
}
|
|
877
|
-
const connection = await runCallbackDatabaseStage(deadline, "persist", db,
|
|
994
|
+
const connection = await runCallbackDatabaseStage(deadline, "persist", db, (scopedDb) =>
|
|
995
|
+
withOrganizationIntegrationAcquisition(scopedDb, state!, [integrationKey], persist),
|
|
996
|
+
);
|
|
878
997
|
if (!connection) {
|
|
879
998
|
throw new HTTPException(409, {
|
|
880
999
|
message: "connection changed during OAuth reconnect; start again",
|
|
@@ -1206,6 +1325,7 @@ async function registerOAuthClient(
|
|
|
1206
1325
|
manual: OAuthStartRequest["oauthClient"],
|
|
1207
1326
|
profile: OAuthProviderProfile,
|
|
1208
1327
|
signal: AbortSignal,
|
|
1328
|
+
acquisition: Pick<OAuthStartContext, "accountId" | "workspaceId" | "integrationKey">,
|
|
1209
1329
|
): Promise<OAuthClientRegistration> {
|
|
1210
1330
|
const operator = operatorClientForAs(settings, as);
|
|
1211
1331
|
if (operator) {
|
|
@@ -1220,6 +1340,7 @@ async function registerOAuthClient(
|
|
|
1220
1340
|
redirectUri,
|
|
1221
1341
|
scopes,
|
|
1222
1342
|
signal,
|
|
1343
|
+
acquisition,
|
|
1223
1344
|
);
|
|
1224
1345
|
}
|
|
1225
1346
|
if (selfRegistration === "cimd") {
|
|
@@ -1244,7 +1365,15 @@ async function registerOAuthClient(
|
|
|
1244
1365
|
),
|
|
1245
1366
|
};
|
|
1246
1367
|
}
|
|
1247
|
-
return await getOrCreateDynamicClientRegistration(
|
|
1368
|
+
return await getOrCreateDynamicClientRegistration(
|
|
1369
|
+
db,
|
|
1370
|
+
settings,
|
|
1371
|
+
as,
|
|
1372
|
+
redirectUri,
|
|
1373
|
+
scopes,
|
|
1374
|
+
signal,
|
|
1375
|
+
acquisition,
|
|
1376
|
+
);
|
|
1248
1377
|
}
|
|
1249
1378
|
|
|
1250
1379
|
export function preferredOAuthSelfRegistration(
|
|
@@ -1272,6 +1401,7 @@ async function getOrCreateDynamicClientRegistration(
|
|
|
1272
1401
|
redirectUri: string,
|
|
1273
1402
|
scopes: string[],
|
|
1274
1403
|
signal: AbortSignal,
|
|
1404
|
+
acquisition: Pick<OAuthStartContext, "accountId" | "workspaceId" | "integrationKey">,
|
|
1275
1405
|
): Promise<OAuthClientRegistration> {
|
|
1276
1406
|
const storedClient = await loadIntegrationOAuthClient(db, settings, as.issuer);
|
|
1277
1407
|
if (storedClient && storedDcrClientSatisfiesPolicy(storedClient, as, redirectUri, scopes)) {
|
|
@@ -1292,6 +1422,12 @@ async function getOrCreateDynamicClientRegistration(
|
|
|
1292
1422
|
message: "manual OAuth client credentials are required for this authorization server",
|
|
1293
1423
|
});
|
|
1294
1424
|
}
|
|
1425
|
+
await withOrganizationIntegrationAcquisition(
|
|
1426
|
+
db,
|
|
1427
|
+
acquisition,
|
|
1428
|
+
[acquisition.integrationKey ?? "custom:mcp"],
|
|
1429
|
+
async () => {},
|
|
1430
|
+
);
|
|
1295
1431
|
const dcr = await dynamicClientRegistration(settings, as, redirectUri, scopes, signal);
|
|
1296
1432
|
const key = dcr.clientSecret ? requireEnvironmentEncryption(settings) : null;
|
|
1297
1433
|
const storeInput = {
|
|
@@ -1303,34 +1439,43 @@ async function getOrCreateDynamicClientRegistration(
|
|
|
1303
1439
|
tokenEndpointAuthMethod: dcr.tokenEndpointAuthMethod,
|
|
1304
1440
|
metadata: registrationMetadata(as, redirectUri, scopes),
|
|
1305
1441
|
};
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1442
|
+
// Registration has already happened outside this transaction. Fence only the
|
|
1443
|
+
// durable client credential; a restriction racing DCR must not persist it.
|
|
1444
|
+
return withOrganizationIntegrationAcquisition(
|
|
1445
|
+
db,
|
|
1446
|
+
acquisition,
|
|
1447
|
+
[acquisition.integrationKey ?? "custom:mcp"],
|
|
1448
|
+
async (tx) => {
|
|
1449
|
+
if (storedClient) {
|
|
1450
|
+
const replaced = await replaceIntegrationOAuthClientIfCurrent(tx, {
|
|
1451
|
+
...storeInput,
|
|
1452
|
+
expectedClientId: storedClient.clientId,
|
|
1453
|
+
});
|
|
1454
|
+
if (replaced?.clientId === dcr.clientId) {
|
|
1455
|
+
return dcr;
|
|
1456
|
+
}
|
|
1457
|
+
return await loadCompatibleDcrWinner(tx, settings, as, redirectUri, scopes);
|
|
1458
|
+
}
|
|
1459
|
+
const storedWinner = await storeIntegrationOAuthClient(tx, storeInput);
|
|
1460
|
+
if (storedWinner.clientId === dcr.clientId) {
|
|
1461
|
+
return dcr;
|
|
1462
|
+
}
|
|
1463
|
+
const winner = await loadIntegrationOAuthClient(tx, settings, as.issuer);
|
|
1464
|
+
if (winner && storedDcrClientSatisfiesPolicy(winner, as, redirectUri, scopes)) {
|
|
1465
|
+
return dcrRegistrationFromStored(winner);
|
|
1466
|
+
}
|
|
1467
|
+
if (winner) {
|
|
1468
|
+
const replaced = await replaceIntegrationOAuthClientIfCurrent(tx, {
|
|
1469
|
+
...storeInput,
|
|
1470
|
+
expectedClientId: winner.clientId,
|
|
1471
|
+
});
|
|
1472
|
+
if (replaced?.clientId === dcr.clientId) {
|
|
1473
|
+
return dcr;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return await loadCompatibleDcrWinner(tx, settings, as, redirectUri, scopes);
|
|
1477
|
+
},
|
|
1478
|
+
);
|
|
1334
1479
|
}
|
|
1335
1480
|
|
|
1336
1481
|
function storedDcrClientSatisfiesPolicy(
|
|
@@ -1641,6 +1786,13 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1641
1786
|
if (!payload) {
|
|
1642
1787
|
throw new HTTPException(400, { message: "invalid or expired OAuth state" });
|
|
1643
1788
|
}
|
|
1789
|
+
if (
|
|
1790
|
+
payload.integrationKey !== undefined &&
|
|
1791
|
+
payload.integrationKey !== "gmail" &&
|
|
1792
|
+
payload.integrationKey !== "slack-personal"
|
|
1793
|
+
) {
|
|
1794
|
+
throw new HTTPException(400, { message: "invalid OAuth integration identity" });
|
|
1795
|
+
}
|
|
1644
1796
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
1645
1797
|
const iat = numberValue(payload.iat);
|
|
1646
1798
|
if (iat === undefined || nowSeconds - iat > oauthStateTtlMs / 1000 || nowSeconds < iat) {
|
|
@@ -1697,6 +1849,9 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1697
1849
|
ownership: connectionOwnership(payload.ownership) ?? "personal",
|
|
1698
1850
|
// Absent on a legacy state, which therefore cannot land a personal owner.
|
|
1699
1851
|
personalOwnerVerified: personalOwnerVerifiedInState(payload),
|
|
1852
|
+
...(payload.integrationKey
|
|
1853
|
+
? { integrationKey: payload.integrationKey as "gmail" | "slack-personal" }
|
|
1854
|
+
: {}),
|
|
1700
1855
|
providerDomain: requiredString(payload.providerDomain, "state.providerDomain"),
|
|
1701
1856
|
mcpUrl: stringValue(payload.mcpUrl) ?? resource,
|
|
1702
1857
|
resource,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
|
|
2
|
+
import { claimOAuthAcquisition, finishOAuthAcquisition } from "./oauth-client";
|
|
1
3
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
4
|
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
3
5
|
import { requireConnectOwnerAuthority } from "./connect-authority";
|
|
@@ -23,7 +25,6 @@ import {
|
|
|
23
25
|
} from "@opengeni/core";
|
|
24
26
|
import {
|
|
25
27
|
consumeIntegrationOAuthStateNonce,
|
|
26
|
-
claimConnectOperation,
|
|
27
28
|
finishConnectOperation,
|
|
28
29
|
getConnectAttempt,
|
|
29
30
|
type Database,
|
|
@@ -216,6 +217,7 @@ export async function startPersonalGitHubOAuth(
|
|
|
216
217
|
) {
|
|
217
218
|
const oauth = requirePersonalGitHubOAuthSettings(deps.settings);
|
|
218
219
|
const { grant } = input.access;
|
|
220
|
+
await withOrganizationIntegrationAcquisition(deps.db, grant, ["github-personal"], async () => {});
|
|
219
221
|
const existing = input.connectionId
|
|
220
222
|
? await getConnectionMetadata(deps.db, input.workspaceId, input.connectionId, grant.subjectId)
|
|
221
223
|
: null;
|
|
@@ -301,12 +303,18 @@ export async function completePersonalGitHubOAuthCallback(
|
|
|
301
303
|
operationId: `oauth:${state.nonce}`,
|
|
302
304
|
inputDigest: createHash("sha256").update(input.state!).digest("hex"),
|
|
303
305
|
};
|
|
304
|
-
const claim = await
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
306
|
+
const claim = await claimOAuthAcquisition(
|
|
307
|
+
deps.db,
|
|
308
|
+
state,
|
|
309
|
+
{
|
|
310
|
+
...operation,
|
|
311
|
+
expectedRevision: stored.attempt.revision,
|
|
312
|
+
authorize: (tx, _attempt, origin) =>
|
|
313
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
314
|
+
},
|
|
315
|
+
"github-personal",
|
|
316
|
+
Boolean(input.code && !input.error),
|
|
317
|
+
);
|
|
310
318
|
if (claim.status === "replayed") return { redirectTo: exactReturnUrl, exactReturn: true };
|
|
311
319
|
}
|
|
312
320
|
await requirePersonalGitHubCallbackGrant(deps, state);
|
|
@@ -340,6 +348,12 @@ export async function completePersonalGitHubOAuthCallback(
|
|
|
340
348
|
}
|
|
341
349
|
if (input.error) throw new PersonalGitHubCallbackError("provider_denied");
|
|
342
350
|
if (!input.code) throw new PersonalGitHubCallbackError("missing_code");
|
|
351
|
+
await withOrganizationIntegrationAcquisition(
|
|
352
|
+
deps.db,
|
|
353
|
+
state,
|
|
354
|
+
["github-personal"],
|
|
355
|
+
async () => {},
|
|
356
|
+
);
|
|
343
357
|
|
|
344
358
|
const oauth = requirePersonalGitHubOAuthSettings(deps.settings);
|
|
345
359
|
if (
|
|
@@ -474,33 +488,43 @@ export async function completePersonalGitHubOAuthCallback(
|
|
|
474
488
|
});
|
|
475
489
|
};
|
|
476
490
|
if (operation) {
|
|
477
|
-
await
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
491
|
+
await finishOAuthAcquisition(
|
|
492
|
+
deps.db,
|
|
493
|
+
acceptedState,
|
|
494
|
+
{
|
|
495
|
+
...operation,
|
|
496
|
+
authorize: (tx, _attempt, origin) =>
|
|
497
|
+
requireConnectOwnerAuthority(tx, acceptedState, "connections:write", origin),
|
|
498
|
+
commit: async (tx, current) => {
|
|
499
|
+
const connection = await persist(tx);
|
|
500
|
+
if (!connection) throw new PersonalGitHubCallbackError("connection_conflict");
|
|
501
|
+
return {
|
|
502
|
+
...current,
|
|
503
|
+
revision: current.revision + 1,
|
|
504
|
+
state: "complete",
|
|
505
|
+
credentialsCommitted: true,
|
|
506
|
+
nextAction: { type: "none" },
|
|
507
|
+
account: {
|
|
508
|
+
id: connection.id,
|
|
509
|
+
version: connection.version,
|
|
510
|
+
providerId: "github-personal",
|
|
511
|
+
label: identity.login,
|
|
512
|
+
ownership: "personal",
|
|
513
|
+
status: "connected",
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
},
|
|
499
517
|
},
|
|
500
|
-
|
|
518
|
+
"github-personal",
|
|
519
|
+
);
|
|
501
520
|
return { redirectTo: exactReturnUrl!, exactReturn: true };
|
|
502
521
|
}
|
|
503
|
-
const connection = await
|
|
522
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
523
|
+
deps.db,
|
|
524
|
+
acceptedState,
|
|
525
|
+
["github-personal"],
|
|
526
|
+
persist,
|
|
527
|
+
);
|
|
504
528
|
if (!connection) throw new PersonalGitHubCallbackError("connection_conflict");
|
|
505
529
|
return {
|
|
506
530
|
redirectTo: personalGitHubCallbackReturnUrl(returnBaseUrl, state.returnPath, "success", {
|