@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,7 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withOrganizationIntegrationAcquisition,
|
|
3
|
+
withOrganizationIntegrationPolicyFence,
|
|
4
|
+
} from "@opengeni/db/organization-integration-policy";
|
|
5
|
+
import {
|
|
6
|
+
claimOAuthAcquisition,
|
|
7
|
+
finishOAuthAcquisition,
|
|
8
|
+
integrationSourceSelectionRequiresAcquisition,
|
|
9
|
+
} from "./oauth-client";
|
|
1
10
|
import {
|
|
2
11
|
scheduledTaskKnowledgeSource,
|
|
3
12
|
requireScheduledTaskKnowledgeSource,
|
|
4
13
|
knowledgeSourceAgentConfig,
|
|
14
|
+
assertOrganizationIntegrationAllowed,
|
|
5
15
|
} from "@opengeni/contracts";
|
|
6
16
|
import type { Settings } from "@opengeni/config";
|
|
7
17
|
import { createHash } from "node:crypto";
|
|
@@ -44,7 +54,6 @@ import {
|
|
|
44
54
|
ConnectionDisconnectGenerationError,
|
|
45
55
|
ConnectionDisconnectIdempotencyError,
|
|
46
56
|
consumeIntegrationOAuthStateNonce,
|
|
47
|
-
claimConnectOperation,
|
|
48
57
|
finishConnectOperation,
|
|
49
58
|
getConnectAttempt,
|
|
50
59
|
decryptEnvironmentValue,
|
|
@@ -132,6 +141,7 @@ export async function startAtlassianOAuth(
|
|
|
132
141
|
externalContinuation?: ExternalActorContinuation;
|
|
133
142
|
},
|
|
134
143
|
): Promise<AtlassianOAuthStartResponse> {
|
|
144
|
+
await withOrganizationIntegrationAcquisition(deps.db, input, ["atlassian"], async () => {});
|
|
135
145
|
const oauth = requireAtlassianSettings(deps.settings);
|
|
136
146
|
const existing = input.payload.connectionId
|
|
137
147
|
? await getConnectionMetadata(
|
|
@@ -208,12 +218,18 @@ export async function completeAtlassianOAuthCallback(
|
|
|
208
218
|
operationId: `oauth:${state.nonce}`,
|
|
209
219
|
inputDigest: createHash("sha256").update(input.state!).digest("hex"),
|
|
210
220
|
};
|
|
211
|
-
const claim = await
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
const claim = await claimOAuthAcquisition(
|
|
222
|
+
deps.db,
|
|
223
|
+
state,
|
|
224
|
+
{
|
|
225
|
+
...operation,
|
|
226
|
+
expectedRevision: stored.attempt.revision,
|
|
227
|
+
authorize: (tx, _attempt, origin) =>
|
|
228
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
229
|
+
},
|
|
230
|
+
"atlassian",
|
|
231
|
+
Boolean(input.code && !input.error),
|
|
232
|
+
);
|
|
217
233
|
if (claim.status === "replayed") return { redirectTo: exactReturnUrl, exactReturn: true };
|
|
218
234
|
}
|
|
219
235
|
await requireCallbackGrant(deps, state);
|
|
@@ -247,6 +263,7 @@ export async function completeAtlassianOAuthCallback(
|
|
|
247
263
|
}
|
|
248
264
|
if (input.error) throw new AtlassianCallbackError("provider_denied");
|
|
249
265
|
if (!input.code) throw new AtlassianCallbackError("missing_code");
|
|
266
|
+
await withOrganizationIntegrationAcquisition(deps.db, state, ["atlassian"], async () => {});
|
|
250
267
|
|
|
251
268
|
const oauth = requireAtlassianSettings(deps.settings);
|
|
252
269
|
const redirectUri = `${baseUrl}/v1/integrations/atlassian/callback`;
|
|
@@ -360,36 +377,46 @@ export async function completeAtlassianOAuthCallback(
|
|
|
360
377
|
});
|
|
361
378
|
};
|
|
362
379
|
if (operation) {
|
|
363
|
-
await
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
380
|
+
await finishOAuthAcquisition(
|
|
381
|
+
deps.db,
|
|
382
|
+
acceptedState,
|
|
383
|
+
{
|
|
384
|
+
...operation,
|
|
385
|
+
authorize: (tx, _attempt, origin) =>
|
|
386
|
+
requireConnectOwnerAuthority(tx, acceptedState, "connections:write", origin),
|
|
387
|
+
commit: async (tx, current) => {
|
|
388
|
+
const connection = await persist(tx);
|
|
389
|
+
if (!connection) throw new AtlassianCallbackError("connection_conflict");
|
|
390
|
+
return {
|
|
391
|
+
...current,
|
|
392
|
+
revision: current.revision + 1,
|
|
393
|
+
state: "complete",
|
|
394
|
+
credentialsCommitted: true,
|
|
395
|
+
nextAction: { type: "none" },
|
|
396
|
+
account: {
|
|
397
|
+
id: connection.id,
|
|
398
|
+
version: connection.version,
|
|
399
|
+
providerId: "atlassian",
|
|
400
|
+
label: profile.displayName ?? "Atlassian",
|
|
401
|
+
ownership: "personal",
|
|
402
|
+
status: "connected",
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
},
|
|
385
406
|
},
|
|
386
|
-
|
|
407
|
+
"atlassian",
|
|
408
|
+
);
|
|
387
409
|
return { redirectTo: exactReturnUrl!, exactReturn: true };
|
|
388
410
|
}
|
|
389
|
-
const connection = await
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
411
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
412
|
+
deps.db,
|
|
413
|
+
acceptedState,
|
|
414
|
+
["atlassian"],
|
|
415
|
+
async (tx) => {
|
|
416
|
+
await requireCallbackGrant({ ...deps, db: tx }, acceptedState);
|
|
417
|
+
return persist(tx);
|
|
418
|
+
},
|
|
419
|
+
);
|
|
393
420
|
if (!connection) throw new AtlassianCallbackError("connection_conflict");
|
|
394
421
|
return {
|
|
395
422
|
redirectTo: returnUrl(returnBaseUrl, state.returnPath, "connected", connection.id),
|
|
@@ -705,9 +732,28 @@ export async function saveAtlassianSources(
|
|
|
705
732
|
initiatingSubjectId: input.subjectId,
|
|
706
733
|
});
|
|
707
734
|
|
|
708
|
-
const
|
|
735
|
+
const requestedSelection = parsed.data.sources.map((source) => ({
|
|
736
|
+
...source,
|
|
737
|
+
destination,
|
|
738
|
+
syncCadence: parsed.data.syncCadence,
|
|
739
|
+
syncEnabled: parsed.data.syncEnabled,
|
|
740
|
+
readPolicy: parsed.data.readPolicy,
|
|
741
|
+
}));
|
|
742
|
+
const acquiring = integrationSourceSelectionRequiresAcquisition(
|
|
743
|
+
metadata.selectedSources,
|
|
744
|
+
requestedSelection,
|
|
745
|
+
);
|
|
746
|
+
if (acquiring) {
|
|
747
|
+
await withOrganizationIntegrationAcquisition(deps.db, input, ["atlassian"], async () => {});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const available =
|
|
751
|
+
acquiring && parsed.data.sources.length
|
|
752
|
+
? (await browseAtlassianSources(deps, input)).items
|
|
753
|
+
: [];
|
|
709
754
|
const availableById = new Map(available.map((source) => [source.id, source]));
|
|
710
755
|
const verified = parsed.data.sources.map((source) => {
|
|
756
|
+
if (!acquiring) return source;
|
|
711
757
|
const current = availableById.get(source.id);
|
|
712
758
|
if (
|
|
713
759
|
!current ||
|
|
@@ -724,28 +770,39 @@ export async function saveAtlassianSources(
|
|
|
724
770
|
return current;
|
|
725
771
|
});
|
|
726
772
|
const previousSources = metadata.selectedSources;
|
|
727
|
-
const updated = await
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
773
|
+
const updated = await withOrganizationIntegrationPolicyFence(
|
|
774
|
+
deps.db,
|
|
775
|
+
input,
|
|
776
|
+
async (tx, policy) => {
|
|
777
|
+
// The exact generation checked by transitionConnectionState binds this
|
|
778
|
+
// comparison to the row being changed. Provider verification already ended.
|
|
779
|
+
if (integrationSourceSelectionRequiresAcquisition(previousSources, requestedSelection)) {
|
|
780
|
+
assertOrganizationIntegrationAllowed(policy, "atlassian");
|
|
781
|
+
}
|
|
782
|
+
return transitionConnectionState(tx, {
|
|
783
|
+
workspaceId: input.workspaceId,
|
|
784
|
+
connectionId: existing.id,
|
|
785
|
+
visibleToSubjectId: input.subjectId,
|
|
786
|
+
expectedVersion: existing.version,
|
|
787
|
+
metadata: AtlassianConnectionMetadata.parse({
|
|
788
|
+
...metadata,
|
|
789
|
+
documentDestination: destination,
|
|
790
|
+
selectedSources: verified.map((source) => ({
|
|
791
|
+
...source,
|
|
792
|
+
destination,
|
|
793
|
+
syncCadence: parsed.data.syncCadence,
|
|
794
|
+
syncEnabled: parsed.data.syncEnabled,
|
|
795
|
+
configGeneration:
|
|
796
|
+
(previousSources.find((previous) => previous.id === source.id)?.configGeneration ??
|
|
797
|
+
0) + 1,
|
|
798
|
+
readPolicy: parsed.data.readPolicy,
|
|
799
|
+
selectedAt: new Date().toISOString(),
|
|
800
|
+
})),
|
|
801
|
+
}),
|
|
802
|
+
updatedBySubjectId: input.subjectId,
|
|
803
|
+
});
|
|
804
|
+
},
|
|
805
|
+
);
|
|
749
806
|
if (!updated) throw new HTTPException(409, { message: "Atlassian connection changed" });
|
|
750
807
|
await materializeSchedules(deps, {
|
|
751
808
|
...input,
|
|
@@ -783,14 +840,27 @@ export async function transitionAtlassianLifecycle(
|
|
|
783
840
|
if (target === "paused") {
|
|
784
841
|
await deauthorizeConnectionSources(deps, connection, input.subjectId, "connection_paused");
|
|
785
842
|
}
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
843
|
+
const persist = (tx: Database) =>
|
|
844
|
+
transitionConnectionState(tx, {
|
|
845
|
+
workspaceId: input.workspaceId,
|
|
846
|
+
connectionId: input.connectionId,
|
|
847
|
+
visibleToSubjectId: input.subjectId,
|
|
848
|
+
expectedVersion: connection.version,
|
|
849
|
+
metadata: AtlassianConnectionMetadata.parse({ ...metadata, lifecycle: lifecycle(target) }),
|
|
850
|
+
updatedBySubjectId: input.subjectId,
|
|
851
|
+
});
|
|
852
|
+
const updated =
|
|
853
|
+
target === "active"
|
|
854
|
+
? await withOrganizationIntegrationPolicyFence(
|
|
855
|
+
deps.db,
|
|
856
|
+
{ accountId: connection.accountId, workspaceId: input.workspaceId },
|
|
857
|
+
(tx, policy) => {
|
|
858
|
+
if ((metadata.lifecycle?.state ?? "active") !== "active")
|
|
859
|
+
assertOrganizationIntegrationAllowed(policy, "atlassian");
|
|
860
|
+
return persist(tx);
|
|
861
|
+
},
|
|
862
|
+
)
|
|
863
|
+
: await persist(deps.db);
|
|
794
864
|
if (!updated) throw new HTTPException(409, { message: "Atlassian connection changed" });
|
|
795
865
|
await setSchedulePause(deps, input.workspaceId, input.connectionId, target === "paused");
|
|
796
866
|
return updated;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
|
|
2
|
+
import { claimOAuthAcquisition, finishOAuthAcquisition } from "./oauth-client";
|
|
1
3
|
import { safeReturnPath } from "./oauth-return-path";
|
|
2
4
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
3
5
|
import type { Settings } from "@opengeni/config";
|
|
@@ -30,7 +32,6 @@ import {
|
|
|
30
32
|
encryptEnvironmentValue,
|
|
31
33
|
decryptEnvironmentValue,
|
|
32
34
|
getConnectAttempt,
|
|
33
|
-
claimConnectOperation,
|
|
34
35
|
finishConnectOperation,
|
|
35
36
|
getConnectionMetadata,
|
|
36
37
|
listConnectionsMetadata,
|
|
@@ -63,7 +64,9 @@ const MAX_DRAFT_LINES = 100;
|
|
|
63
64
|
const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/;
|
|
64
65
|
|
|
65
66
|
/** Shared native/embedded token adapter. Provider verification happens before
|
|
66
|
-
* persistence; the caller reauthorizes inside its durable commit transaction.
|
|
67
|
+
* persistence; the caller reauthorizes inside its durable commit transaction.
|
|
68
|
+
* That caller must acquire the organization policy fence for `fiken` BEFORE
|
|
69
|
+
* owner/tenancy locks and invoke the returned writer with that exact tx. */
|
|
67
70
|
export async function prepareFikenTokenInstall(
|
|
68
71
|
deps: ApiRouteDeps,
|
|
69
72
|
grant: Pick<AccessGrant, "accountId" | "workspaceId" | "subjectId">,
|
|
@@ -71,6 +74,7 @@ export async function prepareFikenTokenInstall(
|
|
|
71
74
|
expectedVersion?: number,
|
|
72
75
|
): Promise<(tx: Database) => Promise<ConnectionMetadata>> {
|
|
73
76
|
const payload = FikenInstallRequest.parse(raw);
|
|
77
|
+
await withOrganizationIntegrationAcquisition(deps.db, grant, ["fiken"], async () => {});
|
|
74
78
|
const key = requireEnvironmentEncryption(deps.settings);
|
|
75
79
|
const existing = payload.connectionId
|
|
76
80
|
? await getConnectionMetadata(deps.db, grant.workspaceId, payload.connectionId, null)
|
|
@@ -1102,6 +1106,7 @@ export async function startFikenOAuth(
|
|
|
1102
1106
|
connectAttemptId?: string;
|
|
1103
1107
|
},
|
|
1104
1108
|
): Promise<FikenOAuthStartResponse> {
|
|
1109
|
+
await withOrganizationIntegrationAcquisition(deps.db, input, ["fiken"], async () => {});
|
|
1105
1110
|
const fiken = requireFikenOAuthSettings(deps.settings);
|
|
1106
1111
|
const existing = input.payload.connectionId
|
|
1107
1112
|
? await getConnectionMetadata(deps.db, input.workspaceId, input.payload.connectionId, null)
|
|
@@ -1168,12 +1173,18 @@ export async function completeFikenOAuthCallback(
|
|
|
1168
1173
|
operationId: `oauth:${state.nonce}`,
|
|
1169
1174
|
inputDigest: createHash("sha256").update(input.state!).digest("hex"),
|
|
1170
1175
|
};
|
|
1171
|
-
const claim = await
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1176
|
+
const claim = await claimOAuthAcquisition(
|
|
1177
|
+
deps.db,
|
|
1178
|
+
state,
|
|
1179
|
+
{
|
|
1180
|
+
...operation,
|
|
1181
|
+
expectedRevision: stored.attempt.revision,
|
|
1182
|
+
authorize: (tx, _attempt, origin) =>
|
|
1183
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
1184
|
+
},
|
|
1185
|
+
"fiken",
|
|
1186
|
+
Boolean(input.code && !input.error),
|
|
1187
|
+
);
|
|
1177
1188
|
if (claim.status === "replayed") return { redirectTo: exactReturnUrl, exactReturn: true };
|
|
1178
1189
|
}
|
|
1179
1190
|
await requireFikenCallbackGrant(deps.db, state);
|
|
@@ -1217,6 +1228,7 @@ export async function completeFikenOAuthCallback(
|
|
|
1217
1228
|
if (!input.code) {
|
|
1218
1229
|
throw new FikenOAuthCallbackError("missing_code");
|
|
1219
1230
|
}
|
|
1231
|
+
await withOrganizationIntegrationAcquisition(deps.db, state, ["fiken"], async () => {});
|
|
1220
1232
|
const fiken = requireFikenOAuthSettings(deps.settings);
|
|
1221
1233
|
const key = requireEnvironmentEncryption(deps.settings);
|
|
1222
1234
|
const fetchImpl = deps.fikenFetch ?? fetch;
|
|
@@ -1303,36 +1315,46 @@ export async function completeFikenOAuthCallback(
|
|
|
1303
1315
|
createdBySubjectId: state!.subjectId,
|
|
1304
1316
|
});
|
|
1305
1317
|
if (operation) {
|
|
1306
|
-
await
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1318
|
+
await finishOAuthAcquisition(
|
|
1319
|
+
deps.db,
|
|
1320
|
+
state,
|
|
1321
|
+
{
|
|
1322
|
+
...operation,
|
|
1323
|
+
authorize: (tx, _attempt, origin) =>
|
|
1324
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
1325
|
+
commit: async (tx, current) => {
|
|
1326
|
+
const connection = await persist(tx);
|
|
1327
|
+
if (!connection) throw new FikenOAuthCallbackError("connection_conflict");
|
|
1328
|
+
return {
|
|
1329
|
+
...current,
|
|
1330
|
+
revision: current.revision + 1,
|
|
1331
|
+
state: "complete",
|
|
1332
|
+
credentialsCommitted: true,
|
|
1333
|
+
nextAction: { type: "none" },
|
|
1334
|
+
account: {
|
|
1335
|
+
id: connection.id,
|
|
1336
|
+
version: connection.version,
|
|
1337
|
+
providerId: "fiken-oauth",
|
|
1338
|
+
label: "Fiken",
|
|
1339
|
+
ownership: "workspace",
|
|
1340
|
+
status: "connected",
|
|
1341
|
+
},
|
|
1342
|
+
};
|
|
1343
|
+
},
|
|
1328
1344
|
},
|
|
1329
|
-
|
|
1345
|
+
"fiken",
|
|
1346
|
+
);
|
|
1330
1347
|
return { redirectTo: exactReturnUrl!, exactReturn: true };
|
|
1331
1348
|
}
|
|
1332
|
-
const connection = await
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1349
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
1350
|
+
deps.db,
|
|
1351
|
+
state,
|
|
1352
|
+
["fiken"],
|
|
1353
|
+
async (tx) => {
|
|
1354
|
+
await requireFikenCallbackGrant(tx, state!);
|
|
1355
|
+
return persist(tx);
|
|
1356
|
+
},
|
|
1357
|
+
);
|
|
1336
1358
|
if (!connection) {
|
|
1337
1359
|
throw new FikenOAuthCallbackError("connection_conflict");
|
|
1338
1360
|
}
|
|
@@ -4,7 +4,6 @@ import { HTTPException } from "hono/http-exception";
|
|
|
4
4
|
import type { ConnectAdvance, ConnectAttempt } from "@opengeni/contracts/connect";
|
|
5
5
|
import {
|
|
6
6
|
bindAuthorizedGitHubInstallationRepositories,
|
|
7
|
-
claimConnectOperation,
|
|
8
7
|
finishConnectOperation,
|
|
9
8
|
getConnectAttempt,
|
|
10
9
|
type ConnectActorScope,
|
|
@@ -23,7 +22,7 @@ import {
|
|
|
23
22
|
import { commitGitHubLensConnect, requireGitHubLensConnect } from "./github-lens-connect";
|
|
24
23
|
import type { ApiRouteDeps, PreparedConnectOperation } from "@opengeni/core";
|
|
25
24
|
import { requireConnectOwnerAuthority } from "./connect-authority";
|
|
26
|
-
import { integrationBaseUrl } from "./oauth-client";
|
|
25
|
+
import { claimOAuthAcquisition, finishOAuthAcquisition, integrationBaseUrl } from "./oauth-client";
|
|
27
26
|
import {
|
|
28
27
|
isConsistentGitHubBindingCandidates,
|
|
29
28
|
isConsistentGitHubBindingProof,
|
|
@@ -177,27 +176,40 @@ export async function completeGitHubAppConnect(
|
|
|
177
176
|
const stored = await getConnectAttempt(deps.db, state, state.connectAttemptId);
|
|
178
177
|
if (stored.attempt.providerId !== state.providerId || stored.attempt.ownership !== "workspace")
|
|
179
178
|
throw new Error("GitHub attempt mismatch");
|
|
180
|
-
destination = stored.returnUrl;
|
|
181
179
|
// Reject obsolete browser stages before committing an operation claim.
|
|
182
180
|
// Otherwise a valid older callback could strand the current stage in-flight.
|
|
183
181
|
// Completed callbacks still navigate home without repeating provider work.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const authorize = callbackAuthority(state);
|
|
182
|
+
const authorize: ConnectOperationAuthorization = async (tx, attempt, origin) => {
|
|
183
|
+
await callbackAuthority(state)(tx, attempt, origin);
|
|
184
|
+
destination = stored.returnUrl;
|
|
185
|
+
};
|
|
186
|
+
const acquiring =
|
|
187
|
+
!input.error && (state.phase === "install" ? input.setupAction !== "request" : !!input.code);
|
|
191
188
|
const operation = {
|
|
192
189
|
attemptId: state.connectAttemptId,
|
|
193
190
|
operationId: `github:${state.nonce}`,
|
|
194
191
|
inputDigest: createHash("sha256").update(input.state!).digest("hex"),
|
|
195
192
|
authorize,
|
|
196
193
|
};
|
|
197
|
-
const claim = await
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
const claim = await claimOAuthAcquisition(
|
|
195
|
+
deps.db,
|
|
196
|
+
state,
|
|
197
|
+
{
|
|
198
|
+
...operation,
|
|
199
|
+
expectedRevision: stored.attempt.revision,
|
|
200
|
+
authorizeAcquisition: async (tx, current) => {
|
|
201
|
+
if (
|
|
202
|
+
current.nextAction.type !== "authorize" ||
|
|
203
|
+
new URL(current.nextAction.url).searchParams.get("state") !== input.state
|
|
204
|
+
)
|
|
205
|
+
throw new Error("Stale GitHub setup stage");
|
|
206
|
+
if (acquiring && state.providerId === "github-lens")
|
|
207
|
+
await requireGitHubLensConnect({ ...deps, db: tx }, state.workspaceId);
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
state.providerId,
|
|
211
|
+
acquiring,
|
|
212
|
+
);
|
|
201
213
|
if (claim.status !== "replayed") {
|
|
202
214
|
const provider =
|
|
203
215
|
state.providerId === "github-lens" ? deps.prReviewGithubAppApi : deps.githubAppApi;
|
|
@@ -378,7 +390,14 @@ export async function completeGitHubAppConnect(
|
|
|
378
390
|
},
|
|
379
391
|
};
|
|
380
392
|
}
|
|
381
|
-
|
|
393
|
+
if (acquiring)
|
|
394
|
+
await finishOAuthAcquisition(
|
|
395
|
+
deps.db,
|
|
396
|
+
state,
|
|
397
|
+
{ ...operation, commit: prepared.commit },
|
|
398
|
+
state.providerId,
|
|
399
|
+
);
|
|
400
|
+
else await finishConnectOperation(deps.db, state, { ...operation, commit: prepared.commit });
|
|
382
401
|
}
|
|
383
402
|
} catch {
|
|
384
403
|
// Preserve unknown outcomes; never replay provider authorization to recover.
|