@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.
- package/dist/admission-trace.d.ts +8 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
- package/dist/auth/managed-auth.d.ts +1 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
- package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
- package/dist/connection-ownership.d.ts +1 -2
- package/dist/fatal-process-boundary.d.ts +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +5 -65
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +119 -11
- package/dist/integrations/oauth-profiles.d.ts +8 -13
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +22 -1
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/codex.d.ts +28 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/dist/routes/sessions.d.ts +24 -2
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/workspace-tool-gateway.d.ts +3 -1
- package/package.json +15 -15
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +141 -94
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-session-adapter.ts +11 -0
- package/src/auth/managed-auth.ts +39 -2
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/connection-ownership.ts +2 -9
- package/src/editable-artifact-production.ts +6 -0
- package/src/fatal-process-boundary.ts +17 -2
- package/src/index.ts +3 -4
- package/src/integrations/atlassian.ts +184 -76
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +51 -15
- package/src/integrations/github-lens-connect.ts +7 -28
- package/src/integrations/google-drive.ts +296 -125
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +231 -132
- package/src/integrations/oauth-profiles.ts +10 -45
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +57 -82
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/editable-artifacts.ts +7 -2
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +25 -8
- package/src/mcp/server.ts +135 -43
- package/src/mcp-oauth.ts +336 -34
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/automations.ts +5 -13
- package/src/routes/billing.ts +52 -0
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/codex.ts +4 -2
- package/src/routes/connect.ts +90 -31
- package/src/routes/connections.ts +487 -307
- package/src/routes/files.ts +21 -12
- package/src/routes/github.ts +48 -19
- package/src/routes/insights.ts +17 -7
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/organization-memberships.ts +22 -2
- package/src/routes/plugins.ts +42 -4
- package/src/routes/pr-review-github.ts +67 -57
- package/src/routes/pr-review.ts +74 -71
- package/src/routes/rigs.ts +32 -13
- package/src/routes/scheduled-tasks.ts +6 -6
- package/src/routes/sessions.ts +233 -11
- package/src/routes/skill-content.ts +19 -3
- package/src/routes/social.ts +24 -13
- package/src/sandbox/channel-a.ts +7 -1
- package/src/sandbox/rematerialize.ts +2 -1
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/workspace-tool-gateway.ts +31 -31
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- 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
|
-
|
|
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("
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
390
|
-
|
|
391
|
-
|
|
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:
|
|
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
|
|
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
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
}
|