@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
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
configuredGoogleDriveSyncLimits,
|
|
4
|
+
googleDriveOAuthCallbackUrl,
|
|
5
|
+
type Settings,
|
|
6
|
+
} from "@opengeni/config";
|
|
3
7
|
import type { AccessGrant, ScheduledTask, ScheduledTaskScheduleSpec } from "@opengeni/contracts";
|
|
4
8
|
import {
|
|
5
9
|
bindConnectorDocumentDestination,
|
|
10
|
+
type ConnectorDocumentDestination,
|
|
6
11
|
type ConnectorDocumentDestinationSelection,
|
|
7
12
|
} from "@opengeni/contracts/connector-destinations";
|
|
8
13
|
import {
|
|
@@ -51,6 +56,7 @@ import type { ApiRouteDeps } from "@opengeni/core";
|
|
|
51
56
|
import {
|
|
52
57
|
buildConnectionTokenResolver,
|
|
53
58
|
configureIntegrationFacet,
|
|
59
|
+
integrationFacetConfigureRequestDigest,
|
|
54
60
|
appendKnowledgeSourceAclVersion,
|
|
55
61
|
deauthorizeKnowledgeSourceRetrieval,
|
|
56
62
|
ConnectionDisconnectGenerationError,
|
|
@@ -68,11 +74,13 @@ import {
|
|
|
68
74
|
listKnowledgeSourceSyncTasksForConnection,
|
|
69
75
|
loadConnectionCredentialForBroker,
|
|
70
76
|
listIntegrationInstanceFacets,
|
|
77
|
+
replayCompletedIntegrationFacetOperation,
|
|
71
78
|
transitionConnectionState,
|
|
72
79
|
updateConnection,
|
|
73
80
|
updateScheduledTask,
|
|
74
81
|
upsertKnowledgeProvider,
|
|
75
82
|
upsertKnowledgeSource,
|
|
83
|
+
withWorkspaceSubjectRls,
|
|
76
84
|
recordKnowledgeLifecycleEvent,
|
|
77
85
|
type PermanentConnectionRefreshFailure,
|
|
78
86
|
} from "@opengeni/db";
|
|
@@ -80,6 +88,12 @@ import { googleDriveKnowledgeSourceIdentity } from "@opengeni/documents/google-d
|
|
|
80
88
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
81
89
|
import { readResponseJsonBounded, type FetchLike } from "@opengeni/network";
|
|
82
90
|
import { HTTPException } from "hono/http-exception";
|
|
91
|
+
import {
|
|
92
|
+
personalOnlyConnectionPrincipalMessage,
|
|
93
|
+
personalOwnerStateAccepted,
|
|
94
|
+
personalOwnerVerifiedInState,
|
|
95
|
+
PERSONAL_OWNER_VERIFIED_STATE_CLAIM,
|
|
96
|
+
} from "../connection-ownership";
|
|
83
97
|
import {
|
|
84
98
|
integrationBaseUrl,
|
|
85
99
|
oauthStateTtlMs,
|
|
@@ -94,6 +108,17 @@ const GOOGLE_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
94
108
|
const GOOGLE_REQUEST_TIMEOUT_MS = 10_000;
|
|
95
109
|
const GOOGLE_DRIVE_PAGE_SIZE = 100;
|
|
96
110
|
const GOOGLE_DRIVE_RETURN_PATH = (workspaceId: string) => `/workspaces/${workspaceId}/capabilities`;
|
|
111
|
+
/**
|
|
112
|
+
* Flow discriminator for this connector's signed OAuth state.
|
|
113
|
+
*
|
|
114
|
+
* This state carries no `ownership` field and no provider identity - it is
|
|
115
|
+
* personal-only by construction - so the `personalOwnerVerified` claim would
|
|
116
|
+
* otherwise be its sole ownership gate. Its return path is also byte-identical
|
|
117
|
+
* to the MCP OAuth start's, which signs that path from caller input, so a
|
|
118
|
+
* caller's own MCP state could be presented here. Requiring an exact flow kind
|
|
119
|
+
* binds a state to the flow that minted it.
|
|
120
|
+
*/
|
|
121
|
+
const GOOGLE_DRIVE_OAUTH_STATE_KIND = "google_drive_oauth";
|
|
97
122
|
const GOOGLE_DRIVE_RECONSENT_ERROR_CODES = new Set([
|
|
98
123
|
"appNotAuthorizedToFile",
|
|
99
124
|
"authError",
|
|
@@ -105,6 +130,8 @@ type GoogleDriveOAuthState = {
|
|
|
105
130
|
accountId: string;
|
|
106
131
|
workspaceId: string;
|
|
107
132
|
subjectId: string;
|
|
133
|
+
/** Signed proof that a live managed human started this personal-only flow. */
|
|
134
|
+
personalOwnerVerified: boolean;
|
|
108
135
|
returnPath: string;
|
|
109
136
|
encryptedPkceVerifier: string;
|
|
110
137
|
capability: "source_read" | "publish";
|
|
@@ -237,12 +264,15 @@ export async function startGoogleDriveOAuth(
|
|
|
237
264
|
|
|
238
265
|
const key = requireEnvironmentEncryption(deps.settings);
|
|
239
266
|
const verifier = randomBytes(48).toString("base64url");
|
|
240
|
-
const
|
|
241
|
-
const redirectUri = `${baseUrl}/v1/integrations/google-drive/callback`;
|
|
267
|
+
const redirectUri = requireGoogleDriveOAuthCallbackUrl(deps.settings);
|
|
242
268
|
const state = createSignedState(requireIntegrationsStateSecret(deps.settings), {
|
|
243
269
|
accountId: input.accountId,
|
|
244
270
|
workspaceId: input.workspaceId,
|
|
245
271
|
subjectId: input.subjectId,
|
|
272
|
+
kind: GOOGLE_DRIVE_OAUTH_STATE_KIND,
|
|
273
|
+
// The route admits only a managed human, so reaching here is the proof; the
|
|
274
|
+
// callback has no live principal and enforces exactly this claim.
|
|
275
|
+
[PERSONAL_OWNER_VERIFIED_STATE_CLAIM]: true,
|
|
246
276
|
returnPath: GOOGLE_DRIVE_RETURN_PATH(input.workspaceId),
|
|
247
277
|
encryptedPkceVerifier: encryptEnvironmentValue(key, verifier),
|
|
248
278
|
capability: input.payload.capability,
|
|
@@ -290,6 +320,7 @@ export async function completeGoogleDriveOAuthCallback(
|
|
|
290
320
|
requestUrl: string;
|
|
291
321
|
},
|
|
292
322
|
): Promise<{ redirectTo: string }> {
|
|
323
|
+
const redirectUri = requireGoogleDriveOAuthCallbackUrl(deps.settings);
|
|
293
324
|
const baseUrl = integrationBaseUrl(deps.settings.publicBaseUrl, input.requestUrl);
|
|
294
325
|
const returnBaseUrl = deps.settings.webBaseUrl?.replace(/\/+$/, "") ?? baseUrl;
|
|
295
326
|
let state: GoogleDriveOAuthState | null = null;
|
|
@@ -317,7 +348,6 @@ export async function completeGoogleDriveOAuthCallback(
|
|
|
317
348
|
const google = requireGoogleDriveSettings(deps.settings);
|
|
318
349
|
const key = requireEnvironmentEncryption(deps.settings);
|
|
319
350
|
const verifier = decryptEnvironmentValue(key, state.encryptedPkceVerifier);
|
|
320
|
-
const redirectUri = `${baseUrl}/v1/integrations/google-drive/callback`;
|
|
321
351
|
const fetchImpl = deps.googleDriveFetch ?? fetch;
|
|
322
352
|
const token = await exchangeGoogleAuthorizationCode(
|
|
323
353
|
{
|
|
@@ -829,43 +859,50 @@ export async function saveGoogleDriveFacetSource(
|
|
|
829
859
|
throw new HTTPException(400, { message: "invalid Google Drive source selection" });
|
|
830
860
|
}
|
|
831
861
|
const payload = parsedPayload.data;
|
|
862
|
+
const requestedSources = payload.sources.map((source) => ({
|
|
863
|
+
...source,
|
|
864
|
+
id: validDriveId(source.id, "source.id"),
|
|
865
|
+
}));
|
|
866
|
+
if (new Set(requestedSources.map((source) => source.id)).size !== requestedSources.length) {
|
|
867
|
+
throw new HTTPException(400, { message: "Google Drive sources must be unique" });
|
|
868
|
+
}
|
|
869
|
+
const requestedDestination = bindGoogleDriveDocumentDestination(input, payload);
|
|
870
|
+
const requestedConfig = googleDriveFacetConfig(requestedSources, requestedDestination, payload);
|
|
871
|
+
const replayed = await replayCompletedIntegrationFacetOperation<IntegrationFacetMutationResult>(
|
|
872
|
+
deps.db,
|
|
873
|
+
{
|
|
874
|
+
accountId: input.accountId,
|
|
875
|
+
workspaceId: input.workspaceId,
|
|
876
|
+
subjectId: input.subjectId,
|
|
877
|
+
capabilityId: input.capabilityId,
|
|
878
|
+
instanceKey: input.instanceKey,
|
|
879
|
+
facetKey: input.facetKey,
|
|
880
|
+
idempotencyKey: payload.idempotencyKey,
|
|
881
|
+
kind: "configure",
|
|
882
|
+
expectedRequestDigest: (result) => {
|
|
883
|
+
const displayName = integrationFacetReceiptDisplayName(result);
|
|
884
|
+
if (displayName === null) return "invalid-integration-facet-receipt";
|
|
885
|
+
return integrationFacetConfigureRequestDigest({
|
|
886
|
+
capabilityId: input.capabilityId,
|
|
887
|
+
instanceKey: input.instanceKey,
|
|
888
|
+
facetKey: input.facetKey,
|
|
889
|
+
displayName,
|
|
890
|
+
config: requestedConfig,
|
|
891
|
+
...(payload.expectedVersion !== undefined
|
|
892
|
+
? { expectedVersion: payload.expectedVersion }
|
|
893
|
+
: {}),
|
|
894
|
+
});
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
);
|
|
898
|
+
if (replayed) return IntegrationFacetMutationResult.parse(replayed);
|
|
832
899
|
const context = await requireGoogleDriveIntegrationFacet(deps, input);
|
|
833
|
-
|
|
834
|
-
|
|
900
|
+
googleDriveDocumentDestination(input, payload);
|
|
901
|
+
await verifyGoogleDriveSources(deps, {
|
|
835
902
|
workspaceId: input.workspaceId,
|
|
836
903
|
subjectId: input.subjectId,
|
|
837
904
|
connectionId: context.connection.id,
|
|
838
|
-
sources:
|
|
839
|
-
});
|
|
840
|
-
const config = GoogleDriveKnowledgeSourceConfig.parse({
|
|
841
|
-
sources: verifiedSources.map((verified) => ({
|
|
842
|
-
id: verified.id,
|
|
843
|
-
name: verified.name,
|
|
844
|
-
mimeType: verified.mimeType,
|
|
845
|
-
...(verified.driveId ? { driveId: verified.driveId } : {}),
|
|
846
|
-
sourceKind:
|
|
847
|
-
verified.id === "root"
|
|
848
|
-
? "my_drive"
|
|
849
|
-
: verified.driveId === verified.id
|
|
850
|
-
? "shared_drive"
|
|
851
|
-
: "folder",
|
|
852
|
-
includeDescendants: true,
|
|
853
|
-
})),
|
|
854
|
-
destination: {
|
|
855
|
-
authorityKind: documentDestination.authorityKind,
|
|
856
|
-
authorityAccountId: documentDestination.authorityAccountId,
|
|
857
|
-
...(documentDestination.authorityWorkspaceId
|
|
858
|
-
? { authorityWorkspaceId: documentDestination.authorityWorkspaceId }
|
|
859
|
-
: {}),
|
|
860
|
-
...(documentDestination.authoritySubjectId
|
|
861
|
-
? { authoritySubjectId: documentDestination.authoritySubjectId }
|
|
862
|
-
: {}),
|
|
863
|
-
...(documentDestination.collectionId
|
|
864
|
-
? { collectionId: documentDestination.collectionId }
|
|
865
|
-
: {}),
|
|
866
|
-
},
|
|
867
|
-
syncCadence: payload.syncCadence,
|
|
868
|
-
readPolicy: payload.readPolicy,
|
|
905
|
+
sources: requestedSources,
|
|
869
906
|
});
|
|
870
907
|
return IntegrationFacetMutationResult.parse(
|
|
871
908
|
await configureIntegrationFacet(deps.db, {
|
|
@@ -877,7 +914,7 @@ export async function saveGoogleDriveFacetSource(
|
|
|
877
914
|
facetKey: input.facetKey,
|
|
878
915
|
displayName:
|
|
879
916
|
context.facet.binding?.displayName ?? `${input.instanceKey} — Google Drive content`,
|
|
880
|
-
config,
|
|
917
|
+
config: requestedConfig,
|
|
881
918
|
...(payload.expectedVersion !== undefined
|
|
882
919
|
? { expectedVersion: payload.expectedVersion }
|
|
883
920
|
: {}),
|
|
@@ -886,6 +923,13 @@ export async function saveGoogleDriveFacetSource(
|
|
|
886
923
|
);
|
|
887
924
|
}
|
|
888
925
|
|
|
926
|
+
function integrationFacetReceiptDisplayName(result: Record<string, unknown>): string | null {
|
|
927
|
+
const binding = result.binding;
|
|
928
|
+
if (!binding || typeof binding !== "object" || Array.isArray(binding)) return null;
|
|
929
|
+
const bindingRecord = binding as Record<string, unknown>;
|
|
930
|
+
return typeof bindingRecord.displayName === "string" ? bindingRecord.displayName : null;
|
|
931
|
+
}
|
|
932
|
+
|
|
889
933
|
export async function saveGoogleDriveSource(
|
|
890
934
|
deps: ApiRouteDeps,
|
|
891
935
|
input: {
|
|
@@ -1108,6 +1152,24 @@ function googleDriveDocumentDestination(
|
|
|
1108
1152
|
if (destinationSelection.authorityKind === "personal" && !input.canManagePersonalDestination) {
|
|
1109
1153
|
throw new HTTPException(403, { message: "personal destination requires the exact actor" });
|
|
1110
1154
|
}
|
|
1155
|
+
return bindGoogleDriveDocumentDestination(input, payload);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function bindGoogleDriveDocumentDestination(
|
|
1159
|
+
input: {
|
|
1160
|
+
accountId: string;
|
|
1161
|
+
workspaceId: string;
|
|
1162
|
+
subjectId: string;
|
|
1163
|
+
},
|
|
1164
|
+
payload: {
|
|
1165
|
+
destination?: ConnectorDocumentDestinationSelection | undefined;
|
|
1166
|
+
targetScope?: "user" | "workspace" | "organization" | undefined;
|
|
1167
|
+
},
|
|
1168
|
+
): ConnectorDocumentDestination {
|
|
1169
|
+
const destinationSelection: ConnectorDocumentDestinationSelection = payload.destination ?? {
|
|
1170
|
+
authorityKind: "workspace",
|
|
1171
|
+
collectionId: null,
|
|
1172
|
+
};
|
|
1111
1173
|
return bindConnectorDocumentDestination(destinationSelection, {
|
|
1112
1174
|
accountId: input.accountId,
|
|
1113
1175
|
workspaceId: input.workspaceId,
|
|
@@ -1115,6 +1177,51 @@ function googleDriveDocumentDestination(
|
|
|
1115
1177
|
});
|
|
1116
1178
|
}
|
|
1117
1179
|
|
|
1180
|
+
function googleDriveFacetConfig(
|
|
1181
|
+
sources: ReadonlyArray<{
|
|
1182
|
+
id: string;
|
|
1183
|
+
name: string;
|
|
1184
|
+
mimeType: string;
|
|
1185
|
+
driveId: string | null;
|
|
1186
|
+
}>,
|
|
1187
|
+
documentDestination: ConnectorDocumentDestination,
|
|
1188
|
+
payload: {
|
|
1189
|
+
syncCadence: "manual" | "hourly" | "daily";
|
|
1190
|
+
readPolicy: "allow" | "ask" | "block";
|
|
1191
|
+
},
|
|
1192
|
+
) {
|
|
1193
|
+
return GoogleDriveKnowledgeSourceConfig.parse({
|
|
1194
|
+
sources: sources.map((source) => ({
|
|
1195
|
+
id: source.id,
|
|
1196
|
+
name: source.name,
|
|
1197
|
+
mimeType: source.mimeType,
|
|
1198
|
+
...(source.driveId ? { driveId: source.driveId } : {}),
|
|
1199
|
+
sourceKind:
|
|
1200
|
+
source.id === "root"
|
|
1201
|
+
? "my_drive"
|
|
1202
|
+
: source.driveId === source.id
|
|
1203
|
+
? "shared_drive"
|
|
1204
|
+
: "folder",
|
|
1205
|
+
includeDescendants: true,
|
|
1206
|
+
})),
|
|
1207
|
+
destination: {
|
|
1208
|
+
authorityKind: documentDestination.authorityKind,
|
|
1209
|
+
authorityAccountId: documentDestination.authorityAccountId,
|
|
1210
|
+
...(documentDestination.authorityWorkspaceId
|
|
1211
|
+
? { authorityWorkspaceId: documentDestination.authorityWorkspaceId }
|
|
1212
|
+
: {}),
|
|
1213
|
+
...(documentDestination.authoritySubjectId
|
|
1214
|
+
? { authoritySubjectId: documentDestination.authoritySubjectId }
|
|
1215
|
+
: {}),
|
|
1216
|
+
...(documentDestination.collectionId
|
|
1217
|
+
? { collectionId: documentDestination.collectionId }
|
|
1218
|
+
: {}),
|
|
1219
|
+
},
|
|
1220
|
+
syncCadence: payload.syncCadence,
|
|
1221
|
+
readPolicy: payload.readPolicy,
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1118
1225
|
async function verifyGoogleDriveSources(
|
|
1119
1226
|
deps: ApiRouteDeps,
|
|
1120
1227
|
input: {
|
|
@@ -1282,13 +1389,8 @@ async function materializeGoogleDriveKnowledgeSchedules(
|
|
|
1282
1389
|
kind: input.connection.kind,
|
|
1283
1390
|
},
|
|
1284
1391
|
limits: {
|
|
1285
|
-
|
|
1286
|
-
maxBytes: 500_000_000,
|
|
1287
|
-
maxFileBytes: 100_000_000,
|
|
1288
|
-
maxProviderRequests: 1_000,
|
|
1289
|
-
maxElapsedSeconds: 300,
|
|
1392
|
+
...configuredGoogleDriveSyncLimits(deps.settings),
|
|
1290
1393
|
maxConcurrency: 4,
|
|
1291
|
-
maxFailureDetails: 25,
|
|
1292
1394
|
},
|
|
1293
1395
|
};
|
|
1294
1396
|
const schedule = googleDriveSchedule(selectedSource.syncCadence);
|
|
@@ -1333,6 +1435,7 @@ async function materializeGoogleDriveKnowledgeSchedules(
|
|
|
1333
1435
|
action,
|
|
1334
1436
|
runMode: "new_session_per_run",
|
|
1335
1437
|
targetSessionId: null,
|
|
1438
|
+
connectionAuthorities: [],
|
|
1336
1439
|
agentConfig: {
|
|
1337
1440
|
prompt: "Knowledge source synchronization",
|
|
1338
1441
|
resources: [],
|
|
@@ -1406,7 +1509,7 @@ async function materializeGoogleDriveKnowledgeSchedules(
|
|
|
1406
1509
|
}
|
|
1407
1510
|
}
|
|
1408
1511
|
|
|
1409
|
-
export async function
|
|
1512
|
+
export async function preflightKnowledgeSourceScheduleAuthorization(
|
|
1410
1513
|
deps: ApiRouteDeps,
|
|
1411
1514
|
input: { task: ScheduledTask; subjectId: string },
|
|
1412
1515
|
): Promise<void> {
|
|
@@ -1430,65 +1533,138 @@ export async function revokeKnowledgeSourceScheduleAuthorization(
|
|
|
1430
1533
|
if (!externalSourceId) {
|
|
1431
1534
|
throw new HTTPException(409, { message: "knowledge source schedule identity is incomplete" });
|
|
1432
1535
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1536
|
+
// A deselected/disconnected source still owns a retained task that must be
|
|
1537
|
+
// deletable. The row-locked task revision below freezes this descriptor;
|
|
1538
|
+
// cleanup mutates only its exact accepted generations.
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
export async function revokeKnowledgeSourceScheduleAuthorization(
|
|
1542
|
+
deps: ApiRouteDeps,
|
|
1543
|
+
input: { task: ScheduledTask; subjectId: string },
|
|
1544
|
+
): Promise<void> {
|
|
1545
|
+
const { task } = input;
|
|
1546
|
+
if (task.action.kind !== "knowledge_source_sync") return;
|
|
1547
|
+
if (
|
|
1548
|
+
task.action.initiatingSubjectId !== input.subjectId ||
|
|
1549
|
+
task.action.connection.ownerSubjectId !== input.subjectId
|
|
1550
|
+
) {
|
|
1551
|
+
throw new HTTPException(403, {
|
|
1552
|
+
message: "knowledge source schedule requires the exact initiating subject",
|
|
1442
1553
|
});
|
|
1443
1554
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const selected = sources.find((source) => source.id === externalSourceId);
|
|
1448
|
-
let revocationVersion = connection.version;
|
|
1449
|
-
if (selected?.syncEnabled) {
|
|
1450
|
-
const updated = await transitionConnectionState(deps.db, {
|
|
1451
|
-
workspaceId: task.workspaceId,
|
|
1452
|
-
connectionId: connection.id,
|
|
1453
|
-
visibleToSubjectId: input.subjectId,
|
|
1454
|
-
expectedVersion: connection.version,
|
|
1455
|
-
metadata: GoogleDriveConnectionMetadata.parse({
|
|
1456
|
-
...metadata,
|
|
1457
|
-
selectedSource: null,
|
|
1458
|
-
selectedSources: sources.map((source) =>
|
|
1459
|
-
source.id === externalSourceId
|
|
1460
|
-
? {
|
|
1461
|
-
...source,
|
|
1462
|
-
syncEnabled: false,
|
|
1463
|
-
configGeneration: source.configGeneration + 1,
|
|
1464
|
-
}
|
|
1465
|
-
: source,
|
|
1466
|
-
),
|
|
1467
|
-
}),
|
|
1468
|
-
updatedBySubjectId: input.subjectId,
|
|
1555
|
+
if (task.metadata.connectorKind !== "google_drive") {
|
|
1556
|
+
throw new HTTPException(409, {
|
|
1557
|
+
message: "knowledge source schedule connector cannot be durably disabled",
|
|
1469
1558
|
});
|
|
1470
|
-
if (!updated) {
|
|
1471
|
-
throw new HTTPException(409, {
|
|
1472
|
-
message: "knowledge source connection changed; retry schedule deletion",
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
revocationVersion = updated.version;
|
|
1476
1559
|
}
|
|
1477
|
-
const
|
|
1560
|
+
const externalSourceId =
|
|
1561
|
+
typeof task.metadata.externalSourceId === "string" ? task.metadata.externalSourceId.trim() : "";
|
|
1562
|
+
if (!externalSourceId) {
|
|
1563
|
+
throw new HTTPException(409, { message: "knowledge source schedule identity is incomplete" });
|
|
1564
|
+
}
|
|
1565
|
+
await cleanupKnowledgeSourceScheduleAuthorization(deps, {
|
|
1566
|
+
taskId: task.id,
|
|
1478
1567
|
accountId: task.accountId,
|
|
1479
1568
|
workspaceId: task.workspaceId,
|
|
1569
|
+
connectionId: task.action.connection.connectionId,
|
|
1570
|
+
connectionVersion: task.action.connection.connectionVersion,
|
|
1480
1571
|
sourceId: task.action.sourceId,
|
|
1572
|
+
sourceLifecycleGeneration: task.action.sourceLifecycleGeneration,
|
|
1573
|
+
sourceConfigGeneration: task.action.sourceConfigGeneration,
|
|
1574
|
+
externalSourceId,
|
|
1575
|
+
subjectId: input.subjectId,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
export async function cleanupKnowledgeSourceScheduleAuthorization(
|
|
1580
|
+
deps: ApiRouteDeps,
|
|
1581
|
+
input: {
|
|
1582
|
+
taskId: string;
|
|
1583
|
+
accountId: string;
|
|
1584
|
+
workspaceId: string;
|
|
1585
|
+
connectionId: string;
|
|
1586
|
+
connectionVersion: number;
|
|
1587
|
+
sourceId: string;
|
|
1588
|
+
sourceLifecycleGeneration: number;
|
|
1589
|
+
sourceConfigGeneration: number;
|
|
1590
|
+
externalSourceId: string;
|
|
1591
|
+
subjectId: string;
|
|
1592
|
+
},
|
|
1593
|
+
): Promise<void> {
|
|
1594
|
+
const resolvedBeforeLock = await getKnowledgeSourceForSyncAuthority(deps.db, {
|
|
1595
|
+
accountId: input.accountId,
|
|
1596
|
+
workspaceId: input.workspaceId,
|
|
1597
|
+
sourceId: input.sourceId,
|
|
1481
1598
|
initiatingSubjectId: input.subjectId,
|
|
1482
1599
|
});
|
|
1483
|
-
if (
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1600
|
+
if (
|
|
1601
|
+
resolvedBeforeLock?.source.lifecycleState !== "active" ||
|
|
1602
|
+
resolvedBeforeLock.source.lifecycleGeneration !== input.sourceLifecycleGeneration
|
|
1603
|
+
) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
await withWorkspaceSubjectRls(deps.db, input.workspaceId, input.subjectId, async (tx) => {
|
|
1607
|
+
const connection = await getConnectionMetadata(
|
|
1608
|
+
tx,
|
|
1609
|
+
input.workspaceId,
|
|
1610
|
+
input.connectionId,
|
|
1611
|
+
input.subjectId,
|
|
1612
|
+
);
|
|
1613
|
+
let revocationVersion = input.connectionVersion;
|
|
1614
|
+
if (connection) {
|
|
1615
|
+
const metadata = requireGoogleDriveConnection(connection, input.subjectId);
|
|
1616
|
+
const sources =
|
|
1617
|
+
metadata.selectedSources ?? (metadata.selectedSource ? [metadata.selectedSource] : []);
|
|
1618
|
+
revocationVersion = connection.version;
|
|
1619
|
+
const updated = await transitionConnectionState(tx, {
|
|
1620
|
+
workspaceId: input.workspaceId,
|
|
1621
|
+
connectionId: connection.id,
|
|
1622
|
+
visibleToSubjectId: input.subjectId,
|
|
1623
|
+
expectedVersion: connection.version,
|
|
1624
|
+
metadata: GoogleDriveConnectionMetadata.parse({
|
|
1625
|
+
...metadata,
|
|
1626
|
+
selectedSource: null,
|
|
1627
|
+
selectedSources: sources.map((source) =>
|
|
1628
|
+
source.id === input.externalSourceId
|
|
1629
|
+
? {
|
|
1630
|
+
...source,
|
|
1631
|
+
syncEnabled: false,
|
|
1632
|
+
configGeneration: source.configGeneration + 1,
|
|
1633
|
+
}
|
|
1634
|
+
: source,
|
|
1635
|
+
),
|
|
1636
|
+
}),
|
|
1637
|
+
updatedBySubjectId: input.subjectId,
|
|
1638
|
+
});
|
|
1639
|
+
if (!updated) {
|
|
1640
|
+
throw new HTTPException(409, {
|
|
1641
|
+
message: "knowledge source connection changed; retry schedule deletion",
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
revocationVersion = updated.version;
|
|
1645
|
+
}
|
|
1646
|
+
const resolved = await getKnowledgeSourceForSyncAuthority(tx, {
|
|
1647
|
+
accountId: input.accountId,
|
|
1648
|
+
workspaceId: input.workspaceId,
|
|
1649
|
+
sourceId: input.sourceId,
|
|
1650
|
+
initiatingSubjectId: input.subjectId,
|
|
1651
|
+
});
|
|
1652
|
+
if (
|
|
1653
|
+
resolved?.source.lifecycleState !== "active" ||
|
|
1654
|
+
resolved.source.lifecycleGeneration !== input.sourceLifecycleGeneration
|
|
1655
|
+
) {
|
|
1656
|
+
throw new HTTPException(409, {
|
|
1657
|
+
message: "knowledge source changed; retry schedule deletion",
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
await recordKnowledgeLifecycleEvent(tx, {
|
|
1661
|
+
accountId: input.accountId,
|
|
1662
|
+
workspaceId: input.workspaceId,
|
|
1487
1663
|
targetKind: "source",
|
|
1488
1664
|
targetId: resolved.source.id,
|
|
1489
1665
|
eventType: "deleted",
|
|
1490
1666
|
expectedGeneration: resolved.source.lifecycleGeneration,
|
|
1491
|
-
operationId: `knowledge-schedule-delete:${
|
|
1667
|
+
operationId: `knowledge-schedule-delete:${input.taskId}:${revocationVersion}`,
|
|
1492
1668
|
reasonCode: "schedule_deleted",
|
|
1493
1669
|
actor: {
|
|
1494
1670
|
kind: "human",
|
|
@@ -1496,7 +1672,7 @@ export async function revokeKnowledgeSourceScheduleAuthorization(
|
|
|
1496
1672
|
initiatingHumanSubjectId: input.subjectId,
|
|
1497
1673
|
},
|
|
1498
1674
|
});
|
|
1499
|
-
}
|
|
1675
|
+
});
|
|
1500
1676
|
}
|
|
1501
1677
|
|
|
1502
1678
|
function knowledgeSourceScheduleControl(task: ScheduledTask): {
|
|
@@ -1614,6 +1790,16 @@ function requireGoogleDriveSettings(settings: Settings): {
|
|
|
1614
1790
|
return { clientId, clientSecret };
|
|
1615
1791
|
}
|
|
1616
1792
|
|
|
1793
|
+
function requireGoogleDriveOAuthCallbackUrl(settings: Settings): string {
|
|
1794
|
+
const callbackUrl = googleDriveOAuthCallbackUrl(settings.publicBaseUrl);
|
|
1795
|
+
if (!callbackUrl) {
|
|
1796
|
+
throw new HTTPException(503, {
|
|
1797
|
+
message: "Google Drive requires a canonical OPENGENI_PUBLIC_BASE_URL origin",
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
return callbackUrl;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1617
1803
|
function requireGoogleDriveConnection(connection: GoogleDriveConnectionRecord, subjectId: string) {
|
|
1618
1804
|
const parsed = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
|
|
1619
1805
|
if (
|
|
@@ -1799,6 +1985,13 @@ function readGoogleDriveOAuthState(
|
|
|
1799
1985
|
if (iat === undefined || now < iat || now - iat > oauthStateTtlMs / 1000) {
|
|
1800
1986
|
throw new HTTPException(400, { message: "invalid or expired Google Drive OAuth state" });
|
|
1801
1987
|
}
|
|
1988
|
+
// Reject a state minted by any other flow before reading anything else. A
|
|
1989
|
+
// pre-cutover state carries no kind and is refused; that is the same bounded
|
|
1990
|
+
// `oauthStateTtlMs` window the personal-owner claim already fails closed on,
|
|
1991
|
+
// so this adds no new breakage.
|
|
1992
|
+
if (payload.kind !== GOOGLE_DRIVE_OAUTH_STATE_KIND) {
|
|
1993
|
+
throw new HTTPException(400, { message: "invalid Google Drive OAuth state" });
|
|
1994
|
+
}
|
|
1802
1995
|
const accountId = requiredString(payload.accountId, "state.accountId");
|
|
1803
1996
|
const workspaceId = requiredString(payload.workspaceId, "state.workspaceId");
|
|
1804
1997
|
const subjectId = requiredString(payload.subjectId, "state.subjectId");
|
|
@@ -1823,6 +2016,7 @@ function readGoogleDriveOAuthState(
|
|
|
1823
2016
|
accountId,
|
|
1824
2017
|
workspaceId,
|
|
1825
2018
|
subjectId,
|
|
2019
|
+
personalOwnerVerified: personalOwnerVerifiedInState(payload),
|
|
1826
2020
|
returnPath,
|
|
1827
2021
|
encryptedPkceVerifier: requiredString(
|
|
1828
2022
|
payload.encryptedPkceVerifier,
|
|
@@ -1839,6 +2033,21 @@ async function requireGoogleDriveCallbackGrant(
|
|
|
1839
2033
|
deps: ApiRouteDeps,
|
|
1840
2034
|
state: GoogleDriveOAuthState,
|
|
1841
2035
|
): Promise<void> {
|
|
2036
|
+
// This connector is personal-only by construction: its start request carries
|
|
2037
|
+
// no ownership and the callback always writes `subjectId: state.subjectId`.
|
|
2038
|
+
// A state minted before the start-side principal fence existed carries no
|
|
2039
|
+
// `personalOwnerVerified` claim and must not land a personal Connection.
|
|
2040
|
+
if (
|
|
2041
|
+
!personalOwnerStateAccepted({
|
|
2042
|
+
ownership: "personal",
|
|
2043
|
+
subjectId: state.subjectId,
|
|
2044
|
+
personalOwnerVerified: state.personalOwnerVerified,
|
|
2045
|
+
})
|
|
2046
|
+
) {
|
|
2047
|
+
throw new HTTPException(422, {
|
|
2048
|
+
message: personalOnlyConnectionPrincipalMessage("Google Drive"),
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
1842
2051
|
const grant = await getWorkspaceGrant(deps.db, state.subjectId, state.workspaceId);
|
|
1843
2052
|
if (
|
|
1844
2053
|
!grant ||
|