@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.
Files changed (39) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-7IAWEYBH.js → chunk-XZGTKYOA.js} +3626 -2525
  3. package/dist/{chunk-7IAWEYBH.js.map → chunk-XZGTKYOA.js.map} +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/integrations/fiken.d.ts +3 -1
  6. package/dist/integrations/github-lens-connect.d.ts +2 -2
  7. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  8. package/dist/integrations/oauth-client.d.ts +118 -1
  9. package/dist/integrations/slack-interactions.d.ts +1 -1
  10. package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
  11. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  12. package/dist/routes/api-integrations.d.ts +1 -0
  13. package/dist/routes/organization-integration-policy.d.ts +3 -0
  14. package/package.json +19 -19
  15. package/src/app.ts +7 -2
  16. package/src/integrations/atlassian.ts +135 -65
  17. package/src/integrations/fiken.ts +57 -35
  18. package/src/integrations/github-app-connect.ts +34 -15
  19. package/src/integrations/google-drive.ts +295 -124
  20. package/src/integrations/integration-commit-authority.ts +69 -0
  21. package/src/integrations/oauth-client.ts +220 -65
  22. package/src/integrations/personal-github.ts +55 -31
  23. package/src/integrations/provider-oauth.ts +54 -31
  24. package/src/integrations/social-oauth.ts +83 -56
  25. package/src/mcp/first-party-tool-permissions.ts +1 -0
  26. package/src/mcp/knowledge-entries.ts +23 -6
  27. package/src/routes/api-integrations.ts +116 -4
  28. package/src/routes/billing.ts +52 -0
  29. package/src/routes/capabilities.ts +51 -1
  30. package/src/routes/connect.ts +161 -57
  31. package/src/routes/connections.ts +430 -305
  32. package/src/routes/github.ts +48 -19
  33. package/src/routes/integration-facets.ts +66 -25
  34. package/src/routes/knowledge.ts +48 -1
  35. package/src/routes/organization-integration-policy.ts +113 -0
  36. package/src/routes/plugins.ts +12 -2
  37. package/src/routes/pr-review-github.ts +60 -30
  38. package/src/routes/pr-review.ts +69 -45
  39. 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 claimConnectOperation(deps.db, state, {
212
- ...operation,
213
- expectedRevision: stored.attempt.revision,
214
- authorize: (tx, _attempt, origin) =>
215
- requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
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 finishConnectOperation(deps.db, acceptedState, {
364
- ...operation,
365
- authorize: (tx, _attempt, origin) =>
366
- requireConnectOwnerAuthority(tx, acceptedState, "connections:write", origin),
367
- commit: async (tx, current) => {
368
- const connection = await persist(tx);
369
- if (!connection) throw new AtlassianCallbackError("connection_conflict");
370
- return {
371
- ...current,
372
- revision: current.revision + 1,
373
- state: "complete",
374
- credentialsCommitted: true,
375
- nextAction: { type: "none" },
376
- account: {
377
- id: connection.id,
378
- version: connection.version,
379
- providerId: "atlassian",
380
- label: profile.displayName ?? "Atlassian",
381
- ownership: "personal",
382
- status: "connected",
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 deps.db.transaction(async (tx) => {
390
- await requireCallbackGrant({ ...deps, db: tx }, acceptedState);
391
- return persist(tx);
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 available = (await browseAtlassianSources(deps, input)).items;
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 transitionConnectionState(deps.db, {
728
- workspaceId: input.workspaceId,
729
- connectionId: existing.id,
730
- visibleToSubjectId: input.subjectId,
731
- expectedVersion: existing.version,
732
- metadata: AtlassianConnectionMetadata.parse({
733
- ...metadata,
734
- documentDestination: destination,
735
- selectedSources: verified.map((source) => ({
736
- ...source,
737
- destination,
738
- syncCadence: parsed.data.syncCadence,
739
- syncEnabled: parsed.data.syncEnabled,
740
- configGeneration:
741
- (previousSources.find((previous) => previous.id === source.id)?.configGeneration ?? 0) +
742
- 1,
743
- readPolicy: parsed.data.readPolicy,
744
- selectedAt: new Date().toISOString(),
745
- })),
746
- }),
747
- updatedBySubjectId: input.subjectId,
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 updated = await transitionConnectionState(deps.db, {
787
- workspaceId: input.workspaceId,
788
- connectionId: input.connectionId,
789
- visibleToSubjectId: input.subjectId,
790
- expectedVersion: connection.version,
791
- metadata: AtlassianConnectionMetadata.parse({ ...metadata, lifecycle: lifecycle(target) }),
792
- updatedBySubjectId: input.subjectId,
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 claimConnectOperation(deps.db, state, {
1172
- ...operation,
1173
- expectedRevision: stored.attempt.revision,
1174
- authorize: (tx, _attempt, origin) =>
1175
- requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
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 finishConnectOperation(deps.db, state, {
1307
- ...operation,
1308
- authorize: (tx, _attempt, origin) =>
1309
- requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
1310
- commit: async (tx, current) => {
1311
- const connection = await persist(tx);
1312
- if (!connection) throw new FikenOAuthCallbackError("connection_conflict");
1313
- return {
1314
- ...current,
1315
- revision: current.revision + 1,
1316
- state: "complete",
1317
- credentialsCommitted: true,
1318
- nextAction: { type: "none" },
1319
- account: {
1320
- id: connection.id,
1321
- version: connection.version,
1322
- providerId: "fiken-oauth",
1323
- label: "Fiken",
1324
- ownership: "workspace",
1325
- status: "connected",
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 deps.db.transaction(async (tx) => {
1333
- await requireFikenCallbackGrant(tx, state!);
1334
- return persist(tx);
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
- if (
185
- stored.attempt.nextAction.type !== "authorize" ||
186
- new URL(stored.attempt.nextAction.url).searchParams.get("state") !== input.state
187
- )
188
- throw new Error("Stale GitHub setup stage");
189
- if (state.providerId === "github-lens") await requireGitHubLensConnect(deps, state.workspaceId);
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 claimConnectOperation(deps.db, state, {
198
- ...operation,
199
- expectedRevision: stored.attempt.revision,
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
- await finishConnectOperation(deps.db, state, { ...operation, commit: prepared.commit });
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.