@opengeni/api-router 3.0.1 → 4.0.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 (96) hide show
  1. package/dist/admission-trace.d.ts +8 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  4. package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
  5. package/dist/auth/managed-auth.d.ts +1 -0
  6. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  7. package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
  8. package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
  9. package/dist/connection-ownership.d.ts +1 -2
  10. package/dist/fatal-process-boundary.d.ts +1 -1
  11. package/dist/index.js +24 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/atlassian.d.ts +8 -3
  14. package/dist/integrations/fiken.d.ts +3 -1
  15. package/dist/integrations/github-lens-connect.d.ts +5 -65
  16. package/dist/integrations/google-drive.d.ts +2 -0
  17. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  18. package/dist/integrations/oauth-client.d.ts +119 -11
  19. package/dist/integrations/oauth-profiles.d.ts +8 -13
  20. package/dist/integrations/slack-interactions.d.ts +1 -1
  21. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  22. package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
  23. package/dist/mcp-oauth.d.ts +22 -1
  24. package/dist/routes/api-integrations.d.ts +1 -0
  25. package/dist/routes/codex.d.ts +28 -0
  26. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  27. package/dist/routes/organization-integration-policy.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +24 -2
  29. package/dist/scheduled-task-deletion.d.ts +3 -4
  30. package/dist/workspace-tool-gateway.d.ts +3 -1
  31. package/package.json +15 -15
  32. package/src/admission-trace.ts +14 -0
  33. package/src/app.ts +141 -94
  34. package/src/auth/managed-auth-attempt-context.ts +9 -0
  35. package/src/auth/managed-auth-session-adapter.ts +11 -0
  36. package/src/auth/managed-auth.ts +39 -2
  37. package/src/auth/managed-sign-in-notifications.ts +81 -0
  38. package/src/connection-ownership.ts +2 -9
  39. package/src/editable-artifact-production.ts +6 -0
  40. package/src/fatal-process-boundary.ts +17 -2
  41. package/src/index.ts +3 -4
  42. package/src/integrations/atlassian.ts +184 -76
  43. package/src/integrations/fiken.ts +57 -35
  44. package/src/integrations/github-app-connect.ts +51 -15
  45. package/src/integrations/github-lens-connect.ts +7 -28
  46. package/src/integrations/google-drive.ts +296 -125
  47. package/src/integrations/integration-commit-authority.ts +69 -0
  48. package/src/integrations/oauth-client.ts +231 -132
  49. package/src/integrations/oauth-profiles.ts +10 -45
  50. package/src/integrations/personal-github.ts +55 -31
  51. package/src/integrations/provider-oauth.ts +57 -82
  52. package/src/integrations/social-oauth.ts +83 -56
  53. package/src/mcp/capability-account-readiness.ts +17 -0
  54. package/src/mcp/editable-artifacts.ts +7 -2
  55. package/src/mcp/first-party-tool-permissions.ts +1 -0
  56. package/src/mcp/knowledge-entries.ts +25 -8
  57. package/src/mcp/server.ts +135 -43
  58. package/src/mcp-oauth.ts +336 -34
  59. package/src/routes/api-integrations.ts +116 -4
  60. package/src/routes/automations.ts +5 -13
  61. package/src/routes/billing.ts +52 -0
  62. package/src/routes/canonical-human-identities.ts +61 -0
  63. package/src/routes/capabilities.ts +51 -1
  64. package/src/routes/codex.ts +4 -2
  65. package/src/routes/connect.ts +90 -31
  66. package/src/routes/connections.ts +487 -307
  67. package/src/routes/files.ts +21 -12
  68. package/src/routes/github.ts +48 -19
  69. package/src/routes/insights.ts +17 -7
  70. package/src/routes/integration-facets.ts +66 -25
  71. package/src/routes/knowledge.ts +48 -1
  72. package/src/routes/managed-sign-in-methods.ts +433 -0
  73. package/src/routes/organization-integration-policy.ts +113 -0
  74. package/src/routes/organization-memberships.ts +22 -2
  75. package/src/routes/plugins.ts +42 -4
  76. package/src/routes/pr-review-github.ts +67 -57
  77. package/src/routes/pr-review.ts +74 -71
  78. package/src/routes/rigs.ts +32 -13
  79. package/src/routes/scheduled-tasks.ts +6 -6
  80. package/src/routes/sessions.ts +233 -11
  81. package/src/routes/skill-content.ts +19 -3
  82. package/src/routes/social.ts +24 -13
  83. package/src/sandbox/channel-a.ts +7 -1
  84. package/src/sandbox/rematerialize.ts +2 -1
  85. package/src/scheduled-task-deletion.ts +13 -3
  86. package/src/workspace-tool-gateway.ts +31 -31
  87. package/dist/connection-authority-owner.d.ts +0 -59
  88. package/dist/routes/connection-authorities.d.ts +0 -3
  89. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  90. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  91. package/dist/routes/packs.d.ts +0 -3
  92. package/src/connection-authority-owner.ts +0 -61
  93. package/src/routes/connection-authorities.ts +0 -139
  94. package/src/routes/host-mcp-bindings.ts +0 -122
  95. package/src/routes/host-mcp-resolvers.ts +0 -41
  96. package/src/routes/packs.ts +0 -784
@@ -15,7 +15,8 @@ export type ApiFatalReasonKind =
15
15
  | "symbol"
16
16
  | "undefined";
17
17
 
18
- type ApiFatalObservability = Pick<Observability, "error" | "flush" | "startSpan">;
18
+ type ApiFatalObservability = Pick<Observability, "error" | "flush" | "startSpan"> &
19
+ Partial<Pick<Observability, "recordFailureDiagnostic">>;
19
20
 
20
21
  type ApiFatalProcess = {
21
22
  on: (
@@ -69,10 +70,24 @@ export function installApiFatalProcessBoundary(
69
70
  reporting = true;
70
71
 
71
72
  const diagnostic = apiFatalDiagnostic(event, phase, reason, correlationId);
72
- const message = apiFatalMessage(diagnostic);
73
+ let message = apiFatalMessage(diagnostic);
73
74
  const activeObservability = observability;
74
75
  try {
75
76
  if (activeObservability) {
77
+ try {
78
+ const diagnosticId = activeObservability.recordFailureDiagnostic?.({
79
+ code: FATAL_ERROR_CODES[event],
80
+ stage: phase,
81
+ retryDecision: "not_retryable",
82
+ error: reason,
83
+ });
84
+ if (diagnosticId) {
85
+ diagnostic.correlationId = diagnosticId;
86
+ message = apiFatalMessage(diagnostic);
87
+ }
88
+ } catch {
89
+ // Never replace the required fatal exit with an observer failure.
90
+ }
76
91
  let logged = false;
77
92
  try {
78
93
  activeObservability.error(message, diagnostic);
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  } from "@opengeni/contracts";
17
17
  import {
18
18
  assertRuntimeDatabasePosture,
19
+ isRetryableRuntimeDatabaseStartupError,
19
20
  createDb,
20
21
  markSessionWorkflowWakeDelivered,
21
22
  runtimeDatabaseReadyCheck,
@@ -28,7 +29,6 @@ import {
28
29
  type Observability,
29
30
  } from "@opengeni/observability";
30
31
  import { createObjectStorage } from "@opengeni/storage";
31
- import { createNativeRemoteMcpCredentialsPort } from "@opengeni/core/remote-mcp-credentials";
32
32
  import { isArtifactRuntimeConfigured } from "@opengeni/artifact-tool/runtime/development";
33
33
  import {
34
34
  resolveCatalogSettings,
@@ -266,7 +266,7 @@ export async function createTemporalWorkflowClient(
266
266
  startRigVerification: async ({ workspaceId, changeId, versionId, workflowId }) => {
267
267
  const targetId = changeId ?? versionId;
268
268
  if (!targetId) {
269
- throw new Error("rig verification requires changeId or versionId");
269
+ throw new Error("sandbox environment verification requires changeId or versionId");
270
270
  }
271
271
  try {
272
272
  await temporal.workflow.start("rigVerificationWorkflow", {
@@ -363,7 +363,7 @@ export async function startApi(
363
363
  await retryStartupDependency(
364
364
  "PostgreSQL runtime posture",
365
365
  () => assertRuntimeDatabasePosture(dbClient.db, databasePosture),
366
- { ...retryOptions, onRetry },
366
+ { ...retryOptions, onRetry, shouldRetry: isRetryableRuntimeDatabaseStartupError },
367
367
  );
368
368
  const resolvedCatalog = await retryStartupDependency(
369
369
  "model catalog",
@@ -421,7 +421,6 @@ export async function startApi(
421
421
  }
422
422
  const { app, routeDeps } = createAppComposition({
423
423
  settings,
424
- connectionCredentials: createNativeRemoteMcpCredentialsPort(settings, dbClient.db),
425
424
  db: dbClient.db,
426
425
  bus,
427
426
  workflowClient: workflowClient.client,
@@ -1,7 +1,22 @@
1
+ type AtlassianRequestAuthority = {
2
+ connectionUseContext?: Omit<AcceptedConnectionUseContext, "physicalRequestId" | "usePhase">;
3
+ };
4
+
5
+ import type { AcceptedConnectionUseContext } from "@opengeni/db";
6
+ import {
7
+ withOrganizationIntegrationAcquisition,
8
+ withOrganizationIntegrationPolicyFence,
9
+ } from "@opengeni/db/organization-integration-policy";
10
+ import {
11
+ claimOAuthAcquisition,
12
+ finishOAuthAcquisition,
13
+ integrationSourceSelectionRequiresAcquisition,
14
+ } from "./oauth-client";
1
15
  import {
2
16
  scheduledTaskKnowledgeSource,
3
17
  requireScheduledTaskKnowledgeSource,
4
18
  knowledgeSourceAgentConfig,
19
+ assertOrganizationIntegrationAllowed,
5
20
  } from "@opengeni/contracts";
6
21
  import type { Settings } from "@opengeni/config";
7
22
  import { createHash } from "node:crypto";
@@ -44,7 +59,6 @@ import {
44
59
  ConnectionDisconnectGenerationError,
45
60
  ConnectionDisconnectIdempotencyError,
46
61
  consumeIntegrationOAuthStateNonce,
47
- claimConnectOperation,
48
62
  finishConnectOperation,
49
63
  getConnectAttempt,
50
64
  decryptEnvironmentValue,
@@ -132,6 +146,7 @@ export async function startAtlassianOAuth(
132
146
  externalContinuation?: ExternalActorContinuation;
133
147
  },
134
148
  ): Promise<AtlassianOAuthStartResponse> {
149
+ await withOrganizationIntegrationAcquisition(deps.db, input, ["atlassian"], async () => {});
135
150
  const oauth = requireAtlassianSettings(deps.settings);
136
151
  const existing = input.payload.connectionId
137
152
  ? await getConnectionMetadata(
@@ -208,12 +223,18 @@ export async function completeAtlassianOAuthCallback(
208
223
  operationId: `oauth:${state.nonce}`,
209
224
  inputDigest: createHash("sha256").update(input.state!).digest("hex"),
210
225
  };
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
- });
226
+ const claim = await claimOAuthAcquisition(
227
+ deps.db,
228
+ state,
229
+ {
230
+ ...operation,
231
+ expectedRevision: stored.attempt.revision,
232
+ authorize: (tx, _attempt, origin) =>
233
+ requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
234
+ },
235
+ "atlassian",
236
+ Boolean(input.code && !input.error),
237
+ );
217
238
  if (claim.status === "replayed") return { redirectTo: exactReturnUrl, exactReturn: true };
218
239
  }
219
240
  await requireCallbackGrant(deps, state);
@@ -247,6 +268,7 @@ export async function completeAtlassianOAuthCallback(
247
268
  }
248
269
  if (input.error) throw new AtlassianCallbackError("provider_denied");
249
270
  if (!input.code) throw new AtlassianCallbackError("missing_code");
271
+ await withOrganizationIntegrationAcquisition(deps.db, state, ["atlassian"], async () => {});
250
272
 
251
273
  const oauth = requireAtlassianSettings(deps.settings);
252
274
  const redirectUri = `${baseUrl}/v1/integrations/atlassian/callback`;
@@ -360,36 +382,46 @@ export async function completeAtlassianOAuthCallback(
360
382
  });
361
383
  };
362
384
  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
- };
385
+ await finishOAuthAcquisition(
386
+ deps.db,
387
+ acceptedState,
388
+ {
389
+ ...operation,
390
+ authorize: (tx, _attempt, origin) =>
391
+ requireConnectOwnerAuthority(tx, acceptedState, "connections:write", origin),
392
+ commit: async (tx, current) => {
393
+ const connection = await persist(tx);
394
+ if (!connection) throw new AtlassianCallbackError("connection_conflict");
395
+ return {
396
+ ...current,
397
+ revision: current.revision + 1,
398
+ state: "complete",
399
+ credentialsCommitted: true,
400
+ nextAction: { type: "none" },
401
+ account: {
402
+ id: connection.id,
403
+ version: connection.version,
404
+ providerId: "atlassian",
405
+ label: profile.displayName ?? "Atlassian",
406
+ ownership: "personal",
407
+ status: "connected",
408
+ },
409
+ };
410
+ },
385
411
  },
386
- });
412
+ "atlassian",
413
+ );
387
414
  return { redirectTo: exactReturnUrl!, exactReturn: true };
388
415
  }
389
- const connection = await deps.db.transaction(async (tx) => {
390
- await requireCallbackGrant({ ...deps, db: tx }, acceptedState);
391
- return persist(tx);
392
- });
416
+ const connection = await withOrganizationIntegrationAcquisition(
417
+ deps.db,
418
+ acceptedState,
419
+ ["atlassian"],
420
+ async (tx) => {
421
+ await requireCallbackGrant({ ...deps, db: tx }, acceptedState);
422
+ return persist(tx);
423
+ },
424
+ );
393
425
  if (!connection) throw new AtlassianCallbackError("connection_conflict");
394
426
  return {
395
427
  redirectTo: returnUrl(returnBaseUrl, state.returnPath, "connected", connection.id),
@@ -409,7 +441,11 @@ export async function completeAtlassianOAuthCallback(
409
441
 
410
442
  export async function browseAtlassianSources(
411
443
  deps: ApiRouteDeps,
412
- input: { workspaceId: string; subjectId: string; connectionId: string },
444
+ input: AtlassianRequestAuthority & {
445
+ workspaceId: string;
446
+ subjectId: string;
447
+ connectionId: string;
448
+ },
413
449
  ) {
414
450
  const connection = await getConnectionMetadata(
415
451
  deps.db,
@@ -450,7 +486,7 @@ export async function browseAtlassianSources(
450
486
 
451
487
  export async function searchAtlassianLive(
452
488
  deps: ApiRouteDeps,
453
- input: {
489
+ input: AtlassianRequestAuthority & {
454
490
  workspaceId: string;
455
491
  subjectId: string;
456
492
  connectionId: string;
@@ -553,7 +589,7 @@ export async function searchAtlassianLive(
553
589
 
554
590
  export async function getAtlassianLiveItem(
555
591
  deps: ApiRouteDeps,
556
- input: {
592
+ input: AtlassianRequestAuthority & {
557
593
  workspaceId: string;
558
594
  subjectId: string;
559
595
  connectionId: string;
@@ -705,9 +741,28 @@ export async function saveAtlassianSources(
705
741
  initiatingSubjectId: input.subjectId,
706
742
  });
707
743
 
708
- const available = (await browseAtlassianSources(deps, input)).items;
744
+ const requestedSelection = parsed.data.sources.map((source) => ({
745
+ ...source,
746
+ destination,
747
+ syncCadence: parsed.data.syncCadence,
748
+ syncEnabled: parsed.data.syncEnabled,
749
+ readPolicy: parsed.data.readPolicy,
750
+ }));
751
+ const acquiring = integrationSourceSelectionRequiresAcquisition(
752
+ metadata.selectedSources,
753
+ requestedSelection,
754
+ );
755
+ if (acquiring) {
756
+ await withOrganizationIntegrationAcquisition(deps.db, input, ["atlassian"], async () => {});
757
+ }
758
+
759
+ const available =
760
+ acquiring && parsed.data.sources.length
761
+ ? (await browseAtlassianSources(deps, input)).items
762
+ : [];
709
763
  const availableById = new Map(available.map((source) => [source.id, source]));
710
764
  const verified = parsed.data.sources.map((source) => {
765
+ if (!acquiring) return source;
711
766
  const current = availableById.get(source.id);
712
767
  if (
713
768
  !current ||
@@ -724,28 +779,39 @@ export async function saveAtlassianSources(
724
779
  return current;
725
780
  });
726
781
  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
- });
782
+ const updated = await withOrganizationIntegrationPolicyFence(
783
+ deps.db,
784
+ input,
785
+ async (tx, policy) => {
786
+ // The exact generation checked by transitionConnectionState binds this
787
+ // comparison to the row being changed. Provider verification already ended.
788
+ if (integrationSourceSelectionRequiresAcquisition(previousSources, requestedSelection)) {
789
+ assertOrganizationIntegrationAllowed(policy, "atlassian");
790
+ }
791
+ return transitionConnectionState(tx, {
792
+ workspaceId: input.workspaceId,
793
+ connectionId: existing.id,
794
+ visibleToSubjectId: input.subjectId,
795
+ expectedVersion: existing.version,
796
+ metadata: AtlassianConnectionMetadata.parse({
797
+ ...metadata,
798
+ documentDestination: destination,
799
+ selectedSources: verified.map((source) => ({
800
+ ...source,
801
+ destination,
802
+ syncCadence: parsed.data.syncCadence,
803
+ syncEnabled: parsed.data.syncEnabled,
804
+ configGeneration:
805
+ (previousSources.find((previous) => previous.id === source.id)?.configGeneration ??
806
+ 0) + 1,
807
+ readPolicy: parsed.data.readPolicy,
808
+ selectedAt: new Date().toISOString(),
809
+ })),
810
+ }),
811
+ updatedBySubjectId: input.subjectId,
812
+ });
813
+ },
814
+ );
749
815
  if (!updated) throw new HTTPException(409, { message: "Atlassian connection changed" });
750
816
  await materializeSchedules(deps, {
751
817
  ...input,
@@ -783,14 +849,27 @@ export async function transitionAtlassianLifecycle(
783
849
  if (target === "paused") {
784
850
  await deauthorizeConnectionSources(deps, connection, input.subjectId, "connection_paused");
785
851
  }
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
- });
852
+ const persist = (tx: Database) =>
853
+ transitionConnectionState(tx, {
854
+ workspaceId: input.workspaceId,
855
+ connectionId: input.connectionId,
856
+ visibleToSubjectId: input.subjectId,
857
+ expectedVersion: connection.version,
858
+ metadata: AtlassianConnectionMetadata.parse({ ...metadata, lifecycle: lifecycle(target) }),
859
+ updatedBySubjectId: input.subjectId,
860
+ });
861
+ const updated =
862
+ target === "active"
863
+ ? await withOrganizationIntegrationPolicyFence(
864
+ deps.db,
865
+ { accountId: connection.accountId, workspaceId: input.workspaceId },
866
+ (tx, policy) => {
867
+ if ((metadata.lifecycle?.state ?? "active") !== "active")
868
+ assertOrganizationIntegrationAllowed(policy, "atlassian");
869
+ return persist(tx);
870
+ },
871
+ )
872
+ : await persist(deps.db);
794
873
  if (!updated) throw new HTTPException(409, { message: "Atlassian connection changed" });
795
874
  await setSchedulePause(deps, input.workspaceId, input.connectionId, target === "paused");
796
875
  return updated;
@@ -1164,7 +1243,7 @@ async function materializeSchedules(
1164
1243
  action: { kind: "agent_turn" },
1165
1244
  runMode: "new_session_per_run",
1166
1245
  targetSessionId: null,
1167
- connectionAuthorities: [],
1246
+ connectionAccounts: [],
1168
1247
  agentConfig: knowledgeSourceAgentConfig(action),
1169
1248
  variableSetId: null,
1170
1249
  environmentId: null,
@@ -1229,7 +1308,11 @@ async function materializeSchedules(
1229
1308
 
1230
1309
  async function browseJiraProjects(
1231
1310
  deps: ApiRouteDeps,
1232
- input: { workspaceId: string; subjectId: string; connectionId: string },
1311
+ input: AtlassianRequestAuthority & {
1312
+ workspaceId: string;
1313
+ subjectId: string;
1314
+ connectionId: string;
1315
+ },
1233
1316
  site: AtlassianSite,
1234
1317
  ): Promise<AtlassianBrowseItem[]> {
1235
1318
  const items: AtlassianBrowseItem[] = [];
@@ -1275,7 +1358,11 @@ async function browseJiraProjects(
1275
1358
 
1276
1359
  async function browseConfluenceSpaces(
1277
1360
  deps: ApiRouteDeps,
1278
- input: { workspaceId: string; subjectId: string; connectionId: string },
1361
+ input: AtlassianRequestAuthority & {
1362
+ workspaceId: string;
1363
+ subjectId: string;
1364
+ connectionId: string;
1365
+ },
1279
1366
  site: AtlassianSite,
1280
1367
  ): Promise<AtlassianBrowseItem[]> {
1281
1368
  const items: AtlassianBrowseItem[] = [];
@@ -1333,7 +1420,7 @@ export function confluenceNextUrl(cloudId: string, next: string): URL {
1333
1420
 
1334
1421
  async function atlassianApiRequest(
1335
1422
  deps: ApiRouteDeps,
1336
- input: {
1423
+ input: AtlassianRequestAuthority & {
1337
1424
  workspaceId: string;
1338
1425
  subjectId: string;
1339
1426
  connectionId: string;
@@ -1354,9 +1441,20 @@ async function atlassianApiRequest(
1354
1441
  });
1355
1442
  const resolve = async (forceRefresh: boolean) =>
1356
1443
  await resolver({
1357
- workspaceId: input.workspaceId,
1444
+ workspaceId: input.connectionUseContext?.workspaceId ?? input.workspaceId,
1445
+ ...(input.connectionUseContext
1446
+ ? {
1447
+ connectionUseContext: {
1448
+ ...input.connectionUseContext,
1449
+ physicalRequestId: crypto.randomUUID(),
1450
+ usePhase: "credential_resolution" as const,
1451
+ },
1452
+ }
1453
+ : {}),
1358
1454
  subjectId: input.subjectId,
1359
- serverId: "atlassian-source-browser",
1455
+ serverId: input.connectionUseContext
1456
+ ? `atlassian:${input.connectionId}`
1457
+ : "atlassian-source-browser",
1360
1458
  toolName: input.label,
1361
1459
  connectionRef: {
1362
1460
  providerDomain: ATLASSIAN_PROVIDER_DOMAIN,
@@ -1763,7 +1861,12 @@ function cqlString(value: string): string {
1763
1861
 
1764
1862
  async function readJiraLiveComments(
1765
1863
  deps: ApiRouteDeps,
1766
- input: { workspaceId: string; subjectId: string; connectionId: string; id: string },
1864
+ input: AtlassianRequestAuthority & {
1865
+ workspaceId: string;
1866
+ subjectId: string;
1867
+ connectionId: string;
1868
+ id: string;
1869
+ },
1767
1870
  cloudId: string,
1768
1871
  ) {
1769
1872
  const comments: Array<{ author: string | null; createdAt: string | null; body: string }> = [];
@@ -1796,7 +1899,12 @@ async function readJiraLiveComments(
1796
1899
 
1797
1900
  async function readConfluenceLiveComments(
1798
1901
  deps: ApiRouteDeps,
1799
- input: { workspaceId: string; subjectId: string; connectionId: string; id: string },
1902
+ input: AtlassianRequestAuthority & {
1903
+ workspaceId: string;
1904
+ subjectId: string;
1905
+ connectionId: string;
1906
+ id: string;
1907
+ },
1800
1908
  cloudId: string,
1801
1909
  ) {
1802
1910
  const comments: Array<{ createdAt: string | null; content: string }> = [];
@@ -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
  }