@opengeni/api-router 0.30.3 → 2.2.0-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/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-WCXHI3FF.js.map +0 -1
|
@@ -53,17 +53,31 @@ import {
|
|
|
53
53
|
updateScheduledTask,
|
|
54
54
|
upsertKnowledgeProvider,
|
|
55
55
|
upsertKnowledgeSource,
|
|
56
|
+
withWorkspaceSubjectRls,
|
|
56
57
|
} from "@opengeni/db";
|
|
57
58
|
import { atlassianKnowledgeSourceIdentity } from "@opengeni/documents/atlassian";
|
|
58
59
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
59
60
|
import { readResponseJsonBounded, type FetchLike } from "@opengeni/network";
|
|
60
61
|
import { HTTPException } from "hono/http-exception";
|
|
62
|
+
import {
|
|
63
|
+
personalOnlyConnectionPrincipalMessage,
|
|
64
|
+
personalOwnerStateAccepted,
|
|
65
|
+
personalOwnerVerifiedInState,
|
|
66
|
+
PERSONAL_OWNER_VERIFIED_STATE_CLAIM,
|
|
67
|
+
} from "../connection-ownership";
|
|
61
68
|
import {
|
|
62
69
|
integrationBaseUrl,
|
|
63
70
|
oauthStateTtlMs,
|
|
64
71
|
requireIntegrationsStateSecret,
|
|
65
72
|
} from "./oauth-client";
|
|
66
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Flow discriminator for this connector's signed OAuth state. See the matching
|
|
76
|
+
* constant in `google-drive.ts`: this state carries no `ownership` field and no
|
|
77
|
+
* provider identity, and its return path is byte-identical to one the MCP OAuth
|
|
78
|
+
* start signs from caller input, so the flow kind is what binds a state here.
|
|
79
|
+
*/
|
|
80
|
+
const ATLASSIAN_OAUTH_STATE_KIND = "atlassian_oauth";
|
|
67
81
|
const ATLASSIAN_AUTHORIZATION_URL = "https://auth.atlassian.com/authorize";
|
|
68
82
|
const ATLASSIAN_TOKEN_URL = "https://auth.atlassian.com/oauth/token";
|
|
69
83
|
const ATLASSIAN_RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources";
|
|
@@ -76,6 +90,8 @@ type AtlassianOAuthState = {
|
|
|
76
90
|
accountId: string;
|
|
77
91
|
workspaceId: string;
|
|
78
92
|
subjectId: string;
|
|
93
|
+
/** Signed proof that a live managed human started this personal-only flow. */
|
|
94
|
+
personalOwnerVerified: boolean;
|
|
79
95
|
returnPath: string;
|
|
80
96
|
connectionId?: string;
|
|
81
97
|
connectionVersion?: number;
|
|
@@ -121,6 +137,10 @@ export async function startAtlassianOAuth(
|
|
|
121
137
|
accountId: input.accountId,
|
|
122
138
|
workspaceId: input.workspaceId,
|
|
123
139
|
subjectId: input.subjectId,
|
|
140
|
+
kind: ATLASSIAN_OAUTH_STATE_KIND,
|
|
141
|
+
// The route admits only a managed human, so reaching here is the proof; the
|
|
142
|
+
// callback has no live principal and enforces exactly this claim.
|
|
143
|
+
[PERSONAL_OWNER_VERIFIED_STATE_CLAIM]: true,
|
|
124
144
|
returnPath: ATLASSIAN_RETURN_PATH(input.workspaceId),
|
|
125
145
|
...(existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}),
|
|
126
146
|
});
|
|
@@ -719,7 +739,7 @@ export async function disconnectAtlassian(
|
|
|
719
739
|
}
|
|
720
740
|
}
|
|
721
741
|
|
|
722
|
-
export async function
|
|
742
|
+
export async function preflightAtlassianScheduleAuthorization(
|
|
723
743
|
deps: ApiRouteDeps,
|
|
724
744
|
input: { task: ScheduledTask; subjectId: string },
|
|
725
745
|
): Promise<void> {
|
|
@@ -737,50 +757,118 @@ export async function revokeAtlassianScheduleAuthorization(
|
|
|
737
757
|
if (!externalSourceId) {
|
|
738
758
|
throw new HTTPException(409, { message: "Atlassian schedule identity is incomplete" });
|
|
739
759
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
if (
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
metadata: AtlassianConnectionMetadata.parse({
|
|
757
|
-
...metadata,
|
|
758
|
-
selectedSources: metadata.selectedSources.map((source) =>
|
|
759
|
-
source.id === externalSourceId
|
|
760
|
-
? { ...source, syncEnabled: false, configGeneration: source.configGeneration + 1 }
|
|
761
|
-
: source,
|
|
762
|
-
),
|
|
763
|
-
}),
|
|
764
|
-
updatedBySubjectId: input.subjectId,
|
|
760
|
+
// Current connector attachment is not deletion authority. The frozen task
|
|
761
|
+
// revision and generation-fenced cleanup descriptor are.
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export async function revokeAtlassianScheduleAuthorization(
|
|
765
|
+
deps: ApiRouteDeps,
|
|
766
|
+
input: { task: ScheduledTask; subjectId: string },
|
|
767
|
+
): Promise<void> {
|
|
768
|
+
const { task } = input;
|
|
769
|
+
if (task.action.kind !== "knowledge_source_sync") return;
|
|
770
|
+
if (
|
|
771
|
+
task.action.initiatingSubjectId !== input.subjectId ||
|
|
772
|
+
task.action.connection.ownerSubjectId !== input.subjectId
|
|
773
|
+
) {
|
|
774
|
+
throw new HTTPException(403, {
|
|
775
|
+
message: "knowledge source schedule requires the exact initiating subject",
|
|
765
776
|
});
|
|
766
|
-
if (!updated) throw new HTTPException(409, { message: "Atlassian connection changed" });
|
|
767
|
-
authorityVersion = updated.version;
|
|
768
777
|
}
|
|
769
|
-
const
|
|
778
|
+
const externalSourceId = optionalString(task.metadata.externalSourceId);
|
|
779
|
+
if (!externalSourceId) {
|
|
780
|
+
throw new HTTPException(409, { message: "Atlassian schedule identity is incomplete" });
|
|
781
|
+
}
|
|
782
|
+
await cleanupAtlassianScheduleAuthorization(deps, {
|
|
783
|
+
taskId: task.id,
|
|
770
784
|
accountId: task.accountId,
|
|
771
785
|
workspaceId: task.workspaceId,
|
|
786
|
+
connectionId: task.action.connection.connectionId,
|
|
787
|
+
connectionVersion: task.action.connection.connectionVersion,
|
|
772
788
|
sourceId: task.action.sourceId,
|
|
789
|
+
sourceLifecycleGeneration: task.action.sourceLifecycleGeneration,
|
|
790
|
+
sourceConfigGeneration: task.action.sourceConfigGeneration,
|
|
791
|
+
externalSourceId,
|
|
792
|
+
subjectId: input.subjectId,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export async function cleanupAtlassianScheduleAuthorization(
|
|
797
|
+
deps: ApiRouteDeps,
|
|
798
|
+
input: {
|
|
799
|
+
taskId: string;
|
|
800
|
+
accountId: string;
|
|
801
|
+
workspaceId: string;
|
|
802
|
+
connectionId: string;
|
|
803
|
+
connectionVersion: number;
|
|
804
|
+
sourceId: string;
|
|
805
|
+
sourceLifecycleGeneration: number;
|
|
806
|
+
sourceConfigGeneration: number;
|
|
807
|
+
externalSourceId: string;
|
|
808
|
+
subjectId: string;
|
|
809
|
+
},
|
|
810
|
+
): Promise<void> {
|
|
811
|
+
const resolvedBeforeLock = await getKnowledgeSourceForSyncAuthority(deps.db, {
|
|
812
|
+
accountId: input.accountId,
|
|
813
|
+
workspaceId: input.workspaceId,
|
|
814
|
+
sourceId: input.sourceId,
|
|
773
815
|
initiatingSubjectId: input.subjectId,
|
|
774
816
|
});
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
817
|
+
if (
|
|
818
|
+
resolvedBeforeLock?.source.lifecycleState !== "active" ||
|
|
819
|
+
resolvedBeforeLock.source.lifecycleGeneration !== input.sourceLifecycleGeneration
|
|
820
|
+
) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
await withWorkspaceSubjectRls(deps.db, input.workspaceId, input.subjectId, async (tx) => {
|
|
824
|
+
const connection = await getConnectionMetadata(
|
|
825
|
+
tx,
|
|
826
|
+
input.workspaceId,
|
|
827
|
+
input.connectionId,
|
|
828
|
+
input.subjectId,
|
|
829
|
+
);
|
|
830
|
+
let authorityVersion = input.connectionVersion;
|
|
831
|
+
if (connection) {
|
|
832
|
+
const metadata = requireAtlassianConnection(connection, input.subjectId);
|
|
833
|
+
authorityVersion = connection.version;
|
|
834
|
+
const updated = await transitionConnectionState(tx, {
|
|
835
|
+
workspaceId: input.workspaceId,
|
|
836
|
+
connectionId: connection.id,
|
|
837
|
+
visibleToSubjectId: input.subjectId,
|
|
838
|
+
expectedVersion: connection.version,
|
|
839
|
+
metadata: AtlassianConnectionMetadata.parse({
|
|
840
|
+
...metadata,
|
|
841
|
+
selectedSources: metadata.selectedSources.map((source) =>
|
|
842
|
+
source.id === input.externalSourceId
|
|
843
|
+
? { ...source, syncEnabled: false, configGeneration: source.configGeneration + 1 }
|
|
844
|
+
: source,
|
|
845
|
+
),
|
|
846
|
+
}),
|
|
847
|
+
updatedBySubjectId: input.subjectId,
|
|
848
|
+
});
|
|
849
|
+
if (!updated) throw new HTTPException(409, { message: "Atlassian connection changed" });
|
|
850
|
+
authorityVersion = updated.version;
|
|
851
|
+
}
|
|
852
|
+
const resolved = await getKnowledgeSourceForSyncAuthority(tx, {
|
|
853
|
+
accountId: input.accountId,
|
|
854
|
+
workspaceId: input.workspaceId,
|
|
855
|
+
sourceId: input.sourceId,
|
|
856
|
+
initiatingSubjectId: input.subjectId,
|
|
857
|
+
});
|
|
858
|
+
if (
|
|
859
|
+
resolved?.source.lifecycleState !== "active" ||
|
|
860
|
+
resolved.source.lifecycleGeneration !== input.sourceLifecycleGeneration
|
|
861
|
+
) {
|
|
862
|
+
throw new HTTPException(409, { message: "Atlassian knowledge source changed" });
|
|
863
|
+
}
|
|
864
|
+
await recordKnowledgeLifecycleEvent(tx, {
|
|
865
|
+
accountId: input.accountId,
|
|
866
|
+
workspaceId: input.workspaceId,
|
|
779
867
|
targetKind: "source",
|
|
780
868
|
targetId: resolved.source.id,
|
|
781
869
|
eventType: "deleted",
|
|
782
870
|
expectedGeneration: resolved.source.lifecycleGeneration,
|
|
783
|
-
operationId: `atlassian-schedule-delete:${
|
|
871
|
+
operationId: `atlassian-schedule-delete:${input.taskId}:${authorityVersion}`,
|
|
784
872
|
reasonCode: "schedule_deleted",
|
|
785
873
|
actor: {
|
|
786
874
|
kind: "human",
|
|
@@ -788,7 +876,7 @@ export async function revokeAtlassianScheduleAuthorization(
|
|
|
788
876
|
initiatingHumanSubjectId: input.subjectId,
|
|
789
877
|
},
|
|
790
878
|
});
|
|
791
|
-
}
|
|
879
|
+
});
|
|
792
880
|
}
|
|
793
881
|
|
|
794
882
|
async function materializeSchedules(
|
|
@@ -972,6 +1060,7 @@ async function materializeSchedules(
|
|
|
972
1060
|
action,
|
|
973
1061
|
runMode: "new_session_per_run",
|
|
974
1062
|
targetSessionId: null,
|
|
1063
|
+
connectionAuthorities: [],
|
|
975
1064
|
agentConfig: {
|
|
976
1065
|
prompt: "Knowledge source synchronization",
|
|
977
1066
|
resources: [],
|
|
@@ -1409,6 +1498,12 @@ function readAtlassianOAuthState(raw: string | undefined, settings: Settings): A
|
|
|
1409
1498
|
if (iat === undefined || now < iat || now - iat > oauthStateTtlMs / 1_000) {
|
|
1410
1499
|
throw new HTTPException(400, { message: "expired Atlassian OAuth state" });
|
|
1411
1500
|
}
|
|
1501
|
+
// Reject a state minted by any other flow before reading anything else. A
|
|
1502
|
+
// pre-cutover state carries no kind and is refused; that is the same bounded
|
|
1503
|
+
// `oauthStateTtlMs` window the personal-owner claim already fails closed on.
|
|
1504
|
+
if (payload.kind !== ATLASSIAN_OAUTH_STATE_KIND) {
|
|
1505
|
+
throw new HTTPException(400, { message: "invalid Atlassian OAuth state" });
|
|
1506
|
+
}
|
|
1412
1507
|
const accountId = requiredString(payload.accountId, "state.accountId");
|
|
1413
1508
|
const workspaceId = requiredString(payload.workspaceId, "state.workspaceId");
|
|
1414
1509
|
const subjectId = requiredString(payload.subjectId, "state.subjectId");
|
|
@@ -1425,6 +1520,7 @@ function readAtlassianOAuthState(raw: string | undefined, settings: Settings): A
|
|
|
1425
1520
|
accountId,
|
|
1426
1521
|
workspaceId,
|
|
1427
1522
|
subjectId,
|
|
1523
|
+
personalOwnerVerified: personalOwnerVerifiedInState(payload),
|
|
1428
1524
|
returnPath,
|
|
1429
1525
|
...(connectionId ? { connectionId, connectionVersion: connectionVersion! } : {}),
|
|
1430
1526
|
nonce: requiredString(payload.nonce, "state.nonce"),
|
|
@@ -1433,6 +1529,21 @@ function readAtlassianOAuthState(raw: string | undefined, settings: Settings): A
|
|
|
1433
1529
|
}
|
|
1434
1530
|
|
|
1435
1531
|
async function requireCallbackGrant(deps: ApiRouteDeps, state: AtlassianOAuthState): Promise<void> {
|
|
1532
|
+
// This connector is personal-only by construction: its start request carries
|
|
1533
|
+
// no ownership and the callback always writes `subjectId: state.subjectId`.
|
|
1534
|
+
// A state minted before the start-side principal fence existed carries no
|
|
1535
|
+
// `personalOwnerVerified` claim and must not land a personal Connection.
|
|
1536
|
+
if (
|
|
1537
|
+
!personalOwnerStateAccepted({
|
|
1538
|
+
ownership: "personal",
|
|
1539
|
+
subjectId: state.subjectId,
|
|
1540
|
+
personalOwnerVerified: state.personalOwnerVerified,
|
|
1541
|
+
})
|
|
1542
|
+
) {
|
|
1543
|
+
throw new HTTPException(422, {
|
|
1544
|
+
message: personalOnlyConnectionPrincipalMessage("Atlassian"),
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1436
1547
|
const grant = await getWorkspaceGrant(deps.db, state.subjectId, state.workspaceId);
|
|
1437
1548
|
if (
|
|
1438
1549
|
!grant ||
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getConnectionMetadata,
|
|
28
28
|
getWorkspaceGrant,
|
|
29
29
|
listConnectionsMetadata,
|
|
30
|
+
loadConnectionCredentialForBroker,
|
|
30
31
|
recordAuditEvent,
|
|
31
32
|
setConnectionStatus,
|
|
32
33
|
updateConnection,
|
|
@@ -102,6 +103,8 @@ type FikenContext = {
|
|
|
102
103
|
sessionId: string | null;
|
|
103
104
|
};
|
|
104
105
|
|
|
106
|
+
type FikenProviderAuthorization = () => Promise<boolean | void>;
|
|
107
|
+
|
|
105
108
|
/**
|
|
106
109
|
* Fiken allows only a single concurrent API request per credential; concurrent
|
|
107
110
|
* requests can 429 and repeated violations can get the token banned. Serialize
|
|
@@ -281,6 +284,7 @@ export type FikenListInput = {
|
|
|
281
284
|
|
|
282
285
|
export class FikenClient {
|
|
283
286
|
private readonly resolveCredential: ReturnType<typeof buildConnectionTokenResolver>;
|
|
287
|
+
private expectedConnectionVersion: number;
|
|
284
288
|
|
|
285
289
|
constructor(
|
|
286
290
|
private readonly db: Database,
|
|
@@ -289,11 +293,22 @@ export class FikenClient {
|
|
|
289
293
|
private readonly metadata: FikenConnectionMetadata,
|
|
290
294
|
private readonly context: FikenContext,
|
|
291
295
|
private readonly fetchImpl: FetchLike = fetch,
|
|
296
|
+
private readonly authorizeProviderRequest?: FikenProviderAuthorization,
|
|
292
297
|
) {
|
|
298
|
+
this.expectedConnectionVersion = connection.version;
|
|
293
299
|
// OAuth-kind connections refresh through the generic broker; route that
|
|
294
|
-
// token-endpoint transport through the same injectable Fiken fetch.
|
|
300
|
+
// token-endpoint transport through the same injectable Fiken fetch. Keep
|
|
301
|
+
// the optional authorization immediately before actual refresh I/O.
|
|
302
|
+
const refreshFetch: FetchLike = this.authorizeProviderRequest
|
|
303
|
+
? async (input, init) => {
|
|
304
|
+
if ((await this.authorizeProviderRequest?.()) === false) {
|
|
305
|
+
throw new Error("the Fiken credential refresh is no longer authorized");
|
|
306
|
+
}
|
|
307
|
+
return await this.fetchImpl(input, init);
|
|
308
|
+
}
|
|
309
|
+
: fetchImpl;
|
|
295
310
|
this.resolveCredential = buildConnectionTokenResolver(db, settings, undefined, {
|
|
296
|
-
refreshTransport: { fetchImpl },
|
|
311
|
+
refreshTransport: { fetchImpl: refreshFetch },
|
|
297
312
|
});
|
|
298
313
|
}
|
|
299
314
|
|
|
@@ -619,17 +634,27 @@ export class FikenClient {
|
|
|
619
634
|
for (const [key, value] of Object.entries(input.query ?? {})) {
|
|
620
635
|
url.searchParams.set(key, value);
|
|
621
636
|
}
|
|
622
|
-
// Credential resolution (DB read + possible broker refresh) happens
|
|
623
|
-
// outside the serialization chain: only the provider HTTP call itself
|
|
624
|
-
// must obey Fiken's single-concurrent-request rule.
|
|
625
|
-
const headers = await this.headersFor(operation, url.toString());
|
|
626
637
|
const result = await serializedPerConnection(this.connection.id, async () => {
|
|
638
|
+
const authority = await this.currentAuthority(this.expectedConnectionVersion);
|
|
639
|
+
const credential = await this.credentialFor(operation, url.toString());
|
|
640
|
+
await this.currentAuthority(credential.connectionVersion, authority.authorityGeneration);
|
|
641
|
+
// The adapter callback is a fallible preflight. The canonical
|
|
642
|
+
// credential callback below records the physical-use fact, so it must
|
|
643
|
+
// be the final await before the target fetch.
|
|
644
|
+
if (this.authorizeProviderRequest && (await this.authorizeProviderRequest()) === false) {
|
|
645
|
+
throw new Error("the Fiken provider request is no longer authorized");
|
|
646
|
+
}
|
|
647
|
+
await this.currentAuthority(credential.connectionVersion, authority.authorityGeneration);
|
|
648
|
+
if (credential.authorizeProviderRequest && !(await credential.authorizeProviderRequest())) {
|
|
649
|
+
throw new Error("the Fiken provider request is no longer authorized");
|
|
650
|
+
}
|
|
651
|
+
this.expectedConnectionVersion = credential.connectionVersion;
|
|
627
652
|
let response: Response;
|
|
628
653
|
try {
|
|
629
654
|
response = await this.fetchImpl(url, {
|
|
630
655
|
method,
|
|
631
656
|
headers: {
|
|
632
|
-
...headers,
|
|
657
|
+
...credential.headers,
|
|
633
658
|
accept: "application/json",
|
|
634
659
|
...(input.body !== undefined ? { "content-type": "application/json" } : {}),
|
|
635
660
|
},
|
|
@@ -642,7 +667,7 @@ export class FikenClient {
|
|
|
642
667
|
} catch {
|
|
643
668
|
throw new FikenProviderError("transport_error");
|
|
644
669
|
}
|
|
645
|
-
return await this.readResponse(operation, response);
|
|
670
|
+
return await this.readResponse(operation, response, credential.connectionVersion);
|
|
646
671
|
});
|
|
647
672
|
await this.recordAudit(operation, "succeeded");
|
|
648
673
|
return result;
|
|
@@ -655,15 +680,14 @@ export class FikenClient {
|
|
|
655
680
|
private async readResponse(
|
|
656
681
|
operation: FikenOperation,
|
|
657
682
|
response: Response,
|
|
683
|
+
credentialVersion: number,
|
|
658
684
|
): Promise<{ payload: unknown; page: FikenPage | null; locationId: number | null }> {
|
|
659
685
|
if (response.status === 401) {
|
|
660
686
|
await response.body?.cancel().catch(() => undefined);
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
// instead of using the resolution-time snapshot.
|
|
666
|
-
await this.markNeedsReauth().catch(() => undefined);
|
|
687
|
+
// Only the exact active credential generation sent on this request may
|
|
688
|
+
// transition. A late 401 must not poison a reconnect or revocation that
|
|
689
|
+
// became current while the provider response was in flight.
|
|
690
|
+
await this.markNeedsReauth(credentialVersion).catch(() => undefined);
|
|
667
691
|
throw new FikenProviderError("credential_rejected", 401);
|
|
668
692
|
}
|
|
669
693
|
if (response.status === 429) {
|
|
@@ -699,27 +723,67 @@ export class FikenClient {
|
|
|
699
723
|
return { payload, page: pageFromHeaders(response.headers), locationId };
|
|
700
724
|
}
|
|
701
725
|
|
|
702
|
-
private async markNeedsReauth(): Promise<void> {
|
|
726
|
+
private async markNeedsReauth(credentialVersion: number): Promise<void> {
|
|
703
727
|
const current = await getConnectionMetadata(
|
|
704
728
|
this.db,
|
|
705
729
|
this.context.workspaceId,
|
|
706
730
|
this.connection.id,
|
|
707
731
|
null,
|
|
708
732
|
);
|
|
709
|
-
if (
|
|
733
|
+
if (
|
|
734
|
+
!current ||
|
|
735
|
+
current.status !== "active" ||
|
|
736
|
+
current.version !== credentialVersion ||
|
|
737
|
+
!isFikenConnection(current)
|
|
738
|
+
) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
710
741
|
await setConnectionStatus(
|
|
711
742
|
this.db,
|
|
712
743
|
this.context.workspaceId,
|
|
713
744
|
"needs_reauth",
|
|
714
745
|
"Fiken rejected the credential",
|
|
715
|
-
{ id: current.id, version:
|
|
746
|
+
{ id: current.id, version: credentialVersion, subjectId: null },
|
|
716
747
|
);
|
|
717
748
|
}
|
|
718
749
|
|
|
719
|
-
private async
|
|
750
|
+
private async currentAuthority(
|
|
751
|
+
expectedVersion: number,
|
|
752
|
+
expectedAuthorityGeneration?: number,
|
|
753
|
+
): Promise<{ version: number; authorityGeneration: number }> {
|
|
754
|
+
const current = await loadConnectionCredentialForBroker(this.db, this.settings, {
|
|
755
|
+
workspaceId: this.context.workspaceId,
|
|
756
|
+
connectionId: this.connection.id,
|
|
757
|
+
providerDomain: FIKEN_PROVIDER_DOMAIN,
|
|
758
|
+
kind: this.connection.kind === "oauth2" ? "oauth2" : "api_key",
|
|
759
|
+
subjectId: null,
|
|
760
|
+
...(expectedAuthorityGeneration !== undefined ? { expectedAuthorityGeneration } : {}),
|
|
761
|
+
});
|
|
762
|
+
if (
|
|
763
|
+
!current ||
|
|
764
|
+
current.accountId !== this.context.accountId ||
|
|
765
|
+
current.workspaceId !== this.context.workspaceId ||
|
|
766
|
+
current.subjectId !== null ||
|
|
767
|
+
current.providerDomain !== FIKEN_PROVIDER_DOMAIN ||
|
|
768
|
+
current.kind !== this.connection.kind ||
|
|
769
|
+
current.status !== "active" ||
|
|
770
|
+
current.version !== expectedVersion ||
|
|
771
|
+
current.authorityGeneration === undefined ||
|
|
772
|
+
!fikenConnectionMetadata(current.metadata)
|
|
773
|
+
) {
|
|
774
|
+
throw new Error("the Fiken connection authority changed");
|
|
775
|
+
}
|
|
776
|
+
return { version: current.version, authorityGeneration: current.authorityGeneration };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private async credentialFor(
|
|
720
780
|
operation: FikenOperation,
|
|
721
781
|
destinationUrl: string,
|
|
722
|
-
): Promise<
|
|
782
|
+
): Promise<{
|
|
783
|
+
headers: Record<string, string>;
|
|
784
|
+
connectionVersion: number;
|
|
785
|
+
authorizeProviderRequest?: () => Promise<boolean>;
|
|
786
|
+
}> {
|
|
723
787
|
const result = await this.resolveCredential({
|
|
724
788
|
workspaceId: this.context.workspaceId,
|
|
725
789
|
serverId: "opengeni-fiken",
|
|
@@ -732,10 +796,20 @@ export class FikenClient {
|
|
|
732
796
|
},
|
|
733
797
|
destinationUrl,
|
|
734
798
|
});
|
|
735
|
-
if (
|
|
799
|
+
if (
|
|
800
|
+
result.status !== "ok" ||
|
|
801
|
+
result.connectionId !== this.connection.id ||
|
|
802
|
+
result.connectionVersion === undefined
|
|
803
|
+
) {
|
|
736
804
|
throw new Error("the Fiken connection needs to be reconnected");
|
|
737
805
|
}
|
|
738
|
-
return
|
|
806
|
+
return {
|
|
807
|
+
headers: result.headers,
|
|
808
|
+
connectionVersion: result.connectionVersion,
|
|
809
|
+
...(result.authorizeProviderRequest
|
|
810
|
+
? { authorizeProviderRequest: result.authorizeProviderRequest }
|
|
811
|
+
: {}),
|
|
812
|
+
};
|
|
739
813
|
}
|
|
740
814
|
|
|
741
815
|
private receipt(operation: FikenOperation, operationId?: string) {
|
|
@@ -771,7 +845,12 @@ export class FikenClient {
|
|
|
771
845
|
}
|
|
772
846
|
|
|
773
847
|
export function createFikenClient(
|
|
774
|
-
deps: {
|
|
848
|
+
deps: {
|
|
849
|
+
db: Database;
|
|
850
|
+
settings: Settings;
|
|
851
|
+
fikenFetch?: typeof fetch;
|
|
852
|
+
authorizeProviderRequest?: FikenProviderAuthorization;
|
|
853
|
+
},
|
|
775
854
|
resolved: Awaited<ReturnType<typeof resolveFikenConnectionForTool>>,
|
|
776
855
|
): FikenClient {
|
|
777
856
|
return new FikenClient(
|
|
@@ -781,6 +860,7 @@ export function createFikenClient(
|
|
|
781
860
|
resolved.metadata,
|
|
782
861
|
resolved.context,
|
|
783
862
|
deps.fikenFetch,
|
|
863
|
+
deps.authorizeProviderRequest,
|
|
784
864
|
);
|
|
785
865
|
}
|
|
786
866
|
|