@opengeni/api-router 0.23.1 → 0.30.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/app.d.ts +3 -2
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +61 -0
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +22 -1
- package/dist/integrations/provider-oauth.d.ts +17 -0
- package/dist/integrations/slack-bot.d.ts +175 -1
- package/dist/integrations/slack-interactions.d.ts +24 -6
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/scheduled-task-view.d.ts +168 -0
- package/dist/mcp/server.d.ts +16 -4
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +18 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +62 -2
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +7 -3
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +20 -14
- package/src/app.ts +604 -64
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +186 -0
- package/src/browser-network-route.ts +34 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +29 -9
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +229 -5
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +365 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1005 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +756 -0
- package/src/integrations/slack-bot.ts +669 -11
- package/src/integrations/slack-interactions.ts +2063 -93
- package/src/integrations/social-api.ts +11 -0
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +146 -6
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +1756 -355
- package/src/memory-slack-delivery.ts +209 -0
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +410 -0
- package/src/routes/browser-identities.ts +160 -0
- package/src/routes/browser-sessions.ts +4482 -0
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1602 -0
- package/src/routes/connections.ts +421 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-facets.ts +258 -0
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +752 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +830 -278
- package/src/routes/skills.ts +256 -0
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +162 -0
- package/src/routes/workspaces.ts +68 -28
- package/src/sandbox/channel-a.ts +750 -116
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +39 -12
- package/src/slack-reaction-files.ts +181 -0
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import type { Settings } from "@opengeni/config";
|
|
3
|
+
import type { AccessGrant, ScheduledTask, ScheduledTaskScheduleSpec } from "@opengeni/contracts";
|
|
3
4
|
import {
|
|
4
5
|
bindConnectorDocumentDestination,
|
|
5
6
|
type ConnectorDocumentDestinationSelection,
|
|
@@ -9,11 +10,13 @@ import {
|
|
|
9
10
|
GOOGLE_DRIVE_CREDENTIAL_ROLE,
|
|
10
11
|
GOOGLE_DRIVE_PROVIDER_DOMAIN,
|
|
11
12
|
GOOGLE_DRIVE_READONLY_SCOPE,
|
|
13
|
+
GoogleDriveKnowledgeSourceConfig,
|
|
12
14
|
GoogleDriveBrowseItem,
|
|
13
15
|
GoogleDriveBrowseResponse,
|
|
14
16
|
GoogleDriveConnectionLifecycle,
|
|
15
17
|
GoogleDriveConnectionMetadata,
|
|
16
18
|
GoogleDriveOAuthStartResponse,
|
|
19
|
+
SaveGoogleDriveIntegrationSourceRequest,
|
|
17
20
|
SaveGoogleDriveSourceRequest,
|
|
18
21
|
googleDriveOAuthScopeDecision,
|
|
19
22
|
googleDriveScopesAllowCapability,
|
|
@@ -21,10 +24,30 @@ import {
|
|
|
21
24
|
type GoogleDriveLifecycleActionRequest,
|
|
22
25
|
type GoogleDriveOAuthStartRequest,
|
|
23
26
|
} from "@opengeni/contracts/google-drive";
|
|
24
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
29
|
+
ApiIntegrationOAuthConnectionMetadata,
|
|
30
|
+
IntegrationFacetMutationResult,
|
|
31
|
+
} from "@opengeni/contracts";
|
|
32
|
+
import {
|
|
33
|
+
GOOGLE_DRIVE_INTEGRATION_DEFINITION,
|
|
34
|
+
integrationDefinitionProviderDomain,
|
|
35
|
+
} from "@opengeni/capabilities";
|
|
36
|
+
import {
|
|
37
|
+
captureScheduledTaskRestoreState,
|
|
38
|
+
createValidatedScheduledTask,
|
|
39
|
+
hasPermission,
|
|
40
|
+
manualScheduledTaskTriggerWorkflowId,
|
|
41
|
+
requireEnvironmentEncryption,
|
|
42
|
+
syncCreatedScheduledTask,
|
|
43
|
+
syncUpdatedScheduledTask,
|
|
44
|
+
} from "@opengeni/core";
|
|
25
45
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
26
46
|
import {
|
|
27
47
|
buildConnectionTokenResolver,
|
|
48
|
+
configureIntegrationFacet,
|
|
49
|
+
appendKnowledgeSourceAclVersion,
|
|
50
|
+
deauthorizeKnowledgeSourceRetrieval,
|
|
28
51
|
ConnectionDisconnectGenerationError,
|
|
29
52
|
ConnectionDisconnectIdempotencyError,
|
|
30
53
|
consumeIntegrationOAuthStateNonce,
|
|
@@ -33,12 +56,21 @@ import {
|
|
|
33
56
|
disconnectConnectionIdempotently,
|
|
34
57
|
encryptEnvironmentValue,
|
|
35
58
|
getConnectionMetadata,
|
|
59
|
+
getKnowledgeSourceByExternalIdentityForSyncAuthority,
|
|
60
|
+
getKnowledgeSourceForSyncAuthority,
|
|
36
61
|
getWorkspaceGrant,
|
|
62
|
+
listKnowledgeSourceSyncTasksForConnection,
|
|
37
63
|
loadConnectionCredentialForBroker,
|
|
64
|
+
listIntegrationInstanceFacets,
|
|
38
65
|
transitionConnectionState,
|
|
39
66
|
updateConnection,
|
|
67
|
+
updateScheduledTask,
|
|
68
|
+
upsertKnowledgeProvider,
|
|
69
|
+
upsertKnowledgeSource,
|
|
70
|
+
recordKnowledgeLifecycleEvent,
|
|
40
71
|
type PermanentConnectionRefreshFailure,
|
|
41
72
|
} from "@opengeni/db";
|
|
73
|
+
import { googleDriveKnowledgeSourceIdentity } from "@opengeni/documents/google-drive";
|
|
42
74
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
43
75
|
import { readResponseJsonBounded, type FetchLike } from "@opengeni/network";
|
|
44
76
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -75,6 +107,74 @@ type GoogleDriveOAuthState = {
|
|
|
75
107
|
iat: number;
|
|
76
108
|
};
|
|
77
109
|
|
|
110
|
+
export async function wakeGoogleDriveSourcesFromWorkspaceEvent(
|
|
111
|
+
deps: ApiRouteDeps,
|
|
112
|
+
input: {
|
|
113
|
+
accountId: string;
|
|
114
|
+
workspaceId: string;
|
|
115
|
+
connectionId: string;
|
|
116
|
+
connectionOwnerSubjectId: string;
|
|
117
|
+
eventId: string;
|
|
118
|
+
driveId: string | null;
|
|
119
|
+
},
|
|
120
|
+
): Promise<{ enabled: boolean; triggered: number }> {
|
|
121
|
+
if (deps.settings.googleDriveWorkspaceEventsEnabled !== true) {
|
|
122
|
+
return { enabled: false, triggered: 0 };
|
|
123
|
+
}
|
|
124
|
+
const eventId = input.eventId.trim();
|
|
125
|
+
if (eventId.length < 1 || eventId.length > 1024) {
|
|
126
|
+
throw new Error("google_drive_workspace_event_id_invalid");
|
|
127
|
+
}
|
|
128
|
+
const connection = await getConnectionMetadata(
|
|
129
|
+
deps.db,
|
|
130
|
+
input.workspaceId,
|
|
131
|
+
input.connectionId,
|
|
132
|
+
input.connectionOwnerSubjectId,
|
|
133
|
+
);
|
|
134
|
+
if (!connection || connection.accountId !== input.accountId) {
|
|
135
|
+
throw new Error("google_drive_workspace_event_connection_not_found");
|
|
136
|
+
}
|
|
137
|
+
const metadata = requireGoogleDriveConnection(connection, input.connectionOwnerSubjectId);
|
|
138
|
+
if (effectiveGoogleDriveLifecycle(connection, metadata).state !== "active") {
|
|
139
|
+
return { enabled: true, triggered: 0 };
|
|
140
|
+
}
|
|
141
|
+
const selectedSources =
|
|
142
|
+
metadata.selectedSources ?? (metadata.selectedSource ? [metadata.selectedSource] : []);
|
|
143
|
+
const selectedById = new Map(selectedSources.map((source) => [source.id, source]));
|
|
144
|
+
const tasks = await listKnowledgeSourceSyncTasksForConnection(
|
|
145
|
+
deps.db,
|
|
146
|
+
input.workspaceId,
|
|
147
|
+
input.connectionId,
|
|
148
|
+
);
|
|
149
|
+
let triggered = 0;
|
|
150
|
+
for (const task of tasks) {
|
|
151
|
+
if (task.action.kind !== "knowledge_source_sync" || task.status !== "active") continue;
|
|
152
|
+
const externalSourceId =
|
|
153
|
+
typeof task.metadata.externalSourceId === "string" ? task.metadata.externalSourceId : null;
|
|
154
|
+
const selectedSource = externalSourceId ? selectedById.get(externalSourceId) : null;
|
|
155
|
+
if (
|
|
156
|
+
!selectedSource ||
|
|
157
|
+
!selectedSource.syncEnabled ||
|
|
158
|
+
selectedSource.driveId !== input.driveId
|
|
159
|
+
) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const token = createHash("sha256")
|
|
163
|
+
.update(`google-drive-workspace-event:${task.id}:${eventId}`)
|
|
164
|
+
.digest("hex")
|
|
165
|
+
.slice(0, 48);
|
|
166
|
+
await deps.workflowClient.triggerScheduledTask({
|
|
167
|
+
task,
|
|
168
|
+
agentRunUsageIdempotencyKey: `knowledge-source-sync:provider-event:${task.id}:${token}`,
|
|
169
|
+
triggerWorkflowId: manualScheduledTaskTriggerWorkflowId(task.id, `provider-event-${token}`),
|
|
170
|
+
initiator: { kind: "service", subjectId: "google-drive-workspace-events" },
|
|
171
|
+
triggerType: "provider_event",
|
|
172
|
+
});
|
|
173
|
+
triggered += 1;
|
|
174
|
+
}
|
|
175
|
+
return { enabled: true, triggered };
|
|
176
|
+
}
|
|
177
|
+
|
|
78
178
|
type GoogleTokenResponse = {
|
|
79
179
|
accessToken: string;
|
|
80
180
|
refreshToken?: string;
|
|
@@ -85,6 +185,18 @@ type GoogleTokenResponse = {
|
|
|
85
185
|
|
|
86
186
|
type GoogleDriveConnectionRecord = NonNullable<Awaited<ReturnType<typeof getConnectionMetadata>>>;
|
|
87
187
|
|
|
188
|
+
type GoogleDriveApiConnection =
|
|
189
|
+
| {
|
|
190
|
+
kind: "legacy";
|
|
191
|
+
connection: GoogleDriveConnectionRecord;
|
|
192
|
+
metadata: GoogleDriveConnectionMetadata;
|
|
193
|
+
}
|
|
194
|
+
| {
|
|
195
|
+
kind: "integration";
|
|
196
|
+
connection: GoogleDriveConnectionRecord;
|
|
197
|
+
metadata: ApiIntegrationOAuthConnectionMetadata;
|
|
198
|
+
};
|
|
199
|
+
|
|
88
200
|
export async function startGoogleDriveOAuth(
|
|
89
201
|
deps: ApiRouteDeps,
|
|
90
202
|
input: {
|
|
@@ -337,6 +449,21 @@ export async function transitionGoogleDriveLifecycle(
|
|
|
337
449
|
// Natural convergence makes retried pause/resume requests idempotent even if
|
|
338
450
|
// the caller still carries the pre-transition version.
|
|
339
451
|
if (existing.status === "active" && lifecycle.state === targetState) {
|
|
452
|
+
if (targetState === "paused") {
|
|
453
|
+
await deauthorizeGoogleDriveConnectionSources(
|
|
454
|
+
deps,
|
|
455
|
+
existing,
|
|
456
|
+
input.subjectId,
|
|
457
|
+
"connection_paused",
|
|
458
|
+
existing.version,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
await setGoogleDriveScheduleStatus(
|
|
462
|
+
deps,
|
|
463
|
+
input.workspaceId,
|
|
464
|
+
input.connectionId,
|
|
465
|
+
targetState === "active" ? "active" : "paused",
|
|
466
|
+
);
|
|
340
467
|
return existing;
|
|
341
468
|
}
|
|
342
469
|
if (existing.status === "revoked") {
|
|
@@ -358,6 +485,16 @@ export async function transitionGoogleDriveLifecycle(
|
|
|
358
485
|
throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
|
|
359
486
|
}
|
|
360
487
|
|
|
488
|
+
if (input.payload.action === "pause") {
|
|
489
|
+
await deauthorizeGoogleDriveConnectionSources(
|
|
490
|
+
deps,
|
|
491
|
+
existing,
|
|
492
|
+
input.subjectId,
|
|
493
|
+
"connection_paused",
|
|
494
|
+
existing.version + 1,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
361
498
|
const updated = await transitionConnectionState(deps.db, {
|
|
362
499
|
workspaceId: input.workspaceId,
|
|
363
500
|
connectionId: existing.id,
|
|
@@ -381,11 +518,23 @@ export async function transitionGoogleDriveLifecycle(
|
|
|
381
518
|
if (converged?.status === "active") {
|
|
382
519
|
const convergedMetadata = requireGoogleDriveConnection(converged, input.subjectId);
|
|
383
520
|
if (effectiveGoogleDriveLifecycle(converged, convergedMetadata).state === targetState) {
|
|
521
|
+
await setGoogleDriveScheduleStatus(
|
|
522
|
+
deps,
|
|
523
|
+
input.workspaceId,
|
|
524
|
+
input.connectionId,
|
|
525
|
+
targetState === "active" ? "active" : "paused",
|
|
526
|
+
);
|
|
384
527
|
return converged;
|
|
385
528
|
}
|
|
386
529
|
}
|
|
387
530
|
throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
|
|
388
531
|
}
|
|
532
|
+
await setGoogleDriveScheduleStatus(
|
|
533
|
+
deps,
|
|
534
|
+
input.workspaceId,
|
|
535
|
+
input.connectionId,
|
|
536
|
+
targetState === "active" ? "active" : "paused",
|
|
537
|
+
);
|
|
389
538
|
return updated;
|
|
390
539
|
}
|
|
391
540
|
|
|
@@ -400,7 +549,14 @@ export async function disconnectGoogleDrive(
|
|
|
400
549
|
) {
|
|
401
550
|
const metadata = requireGoogleDriveConnection(input.connection, input.subjectId);
|
|
402
551
|
try {
|
|
403
|
-
|
|
552
|
+
await deauthorizeGoogleDriveConnectionSources(
|
|
553
|
+
deps,
|
|
554
|
+
input.connection,
|
|
555
|
+
input.subjectId,
|
|
556
|
+
"connection_disconnected",
|
|
557
|
+
input.connection.version + 1,
|
|
558
|
+
);
|
|
559
|
+
const disconnected = await disconnectConnectionIdempotently(deps.db, {
|
|
404
560
|
accountId: input.connection.accountId,
|
|
405
561
|
workspaceId: input.workspaceId,
|
|
406
562
|
subjectId: input.subjectId,
|
|
@@ -414,6 +570,8 @@ export async function disconnectGoogleDrive(
|
|
|
414
570
|
lastError: null,
|
|
415
571
|
updatedBySubjectId: input.subjectId,
|
|
416
572
|
});
|
|
573
|
+
await setGoogleDriveScheduleStatus(deps, input.workspaceId, input.connection.id, "paused");
|
|
574
|
+
return disconnected;
|
|
417
575
|
} catch (error) {
|
|
418
576
|
if (error instanceof ConnectionDisconnectIdempotencyError) {
|
|
419
577
|
throw new HTTPException(409, {
|
|
@@ -429,6 +587,82 @@ export async function disconnectGoogleDrive(
|
|
|
429
587
|
}
|
|
430
588
|
}
|
|
431
589
|
|
|
590
|
+
async function setGoogleDriveScheduleStatus(
|
|
591
|
+
deps: ApiRouteDeps,
|
|
592
|
+
workspaceId: string,
|
|
593
|
+
connectionId: string,
|
|
594
|
+
status: "active" | "paused",
|
|
595
|
+
): Promise<void> {
|
|
596
|
+
await forEachGoogleDriveConnectionTask(deps, workspaceId, connectionId, async (task) => {
|
|
597
|
+
if (knowledgeSourceScheduleControl(task).connectionPaused === (status === "paused")) return;
|
|
598
|
+
const previous = await captureScheduledTaskRestoreState(deps.db, task);
|
|
599
|
+
const updated = await updateScheduledTask(deps.db, workspaceId, task.id, {
|
|
600
|
+
metadata: {
|
|
601
|
+
...task.metadata,
|
|
602
|
+
knowledgeSourceSync: {
|
|
603
|
+
...knowledgeSourceScheduleControl(task),
|
|
604
|
+
connectionPaused: status === "paused",
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
await syncUpdatedScheduledTask({
|
|
609
|
+
db: deps.db,
|
|
610
|
+
workflowClient: deps.workflowClient,
|
|
611
|
+
previous,
|
|
612
|
+
task: updated,
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function deauthorizeGoogleDriveConnectionSources(
|
|
618
|
+
deps: ApiRouteDeps,
|
|
619
|
+
connection: GoogleDriveConnectionRecord,
|
|
620
|
+
subjectId: string,
|
|
621
|
+
reasonCode: string,
|
|
622
|
+
authorityVersion: number,
|
|
623
|
+
): Promise<void> {
|
|
624
|
+
await forEachGoogleDriveConnectionTask(
|
|
625
|
+
deps,
|
|
626
|
+
connection.workspaceId,
|
|
627
|
+
connection.id,
|
|
628
|
+
async (task) => {
|
|
629
|
+
if (task.action.initiatingSubjectId !== subjectId) return;
|
|
630
|
+
const resolved = await getKnowledgeSourceForSyncAuthority(deps.db, {
|
|
631
|
+
accountId: task.accountId,
|
|
632
|
+
workspaceId: task.workspaceId,
|
|
633
|
+
sourceId: task.action.sourceId,
|
|
634
|
+
initiatingSubjectId: subjectId,
|
|
635
|
+
});
|
|
636
|
+
if (!resolved || resolved.source.lifecycleState !== "active") return;
|
|
637
|
+
await deauthorizeKnowledgeSourceRetrieval(deps.db, {
|
|
638
|
+
accountId: task.accountId,
|
|
639
|
+
workspaceId: task.workspaceId,
|
|
640
|
+
sourceId: task.action.sourceId,
|
|
641
|
+
audience: task.action.destination,
|
|
642
|
+
operationId: `google-drive-deauthorize:${connection.id}:${authorityVersion}:${task.action.sourceId}:${reasonCode}`,
|
|
643
|
+
reasonCode,
|
|
644
|
+
actor: {
|
|
645
|
+
kind: "human",
|
|
646
|
+
subjectId,
|
|
647
|
+
initiatingHumanSubjectId: subjectId,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
},
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function forEachGoogleDriveConnectionTask(
|
|
655
|
+
deps: ApiRouteDeps,
|
|
656
|
+
workspaceId: string,
|
|
657
|
+
connectionId: string,
|
|
658
|
+
fn: (task: ScheduledTask & { action: { kind: "knowledge_source_sync" } }) => Promise<void>,
|
|
659
|
+
): Promise<void> {
|
|
660
|
+
const tasks = await listKnowledgeSourceSyncTasksForConnection(deps.db, workspaceId, connectionId);
|
|
661
|
+
for (const task of tasks) {
|
|
662
|
+
await fn(task);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
432
666
|
export async function browseGoogleDrive(
|
|
433
667
|
deps: ApiRouteDeps,
|
|
434
668
|
input: {
|
|
@@ -448,7 +682,7 @@ export async function browseGoogleDrive(
|
|
|
448
682
|
if (!connection) {
|
|
449
683
|
throw new HTTPException(404, { message: "Google Drive connection not found" });
|
|
450
684
|
}
|
|
451
|
-
await
|
|
685
|
+
await requireGoogleDriveApiConnection(deps, connection, input.subjectId);
|
|
452
686
|
const parentId = validDriveId(input.parentId, "parentId");
|
|
453
687
|
const currentItem = await resolveGoogleDriveBoundaryItem(deps, {
|
|
454
688
|
workspaceId: input.workspaceId,
|
|
@@ -500,6 +734,105 @@ export async function browseGoogleDrive(
|
|
|
500
734
|
});
|
|
501
735
|
}
|
|
502
736
|
|
|
737
|
+
export async function browseGoogleDriveFacetSource(
|
|
738
|
+
deps: ApiRouteDeps,
|
|
739
|
+
input: {
|
|
740
|
+
workspaceId: string;
|
|
741
|
+
subjectId: string;
|
|
742
|
+
capabilityId: string;
|
|
743
|
+
instanceKey: string;
|
|
744
|
+
facetKey: string;
|
|
745
|
+
parentId: string;
|
|
746
|
+
pageToken?: string | undefined;
|
|
747
|
+
},
|
|
748
|
+
) {
|
|
749
|
+
const context = await requireGoogleDriveIntegrationFacet(deps, input);
|
|
750
|
+
return await browseGoogleDrive(deps, {
|
|
751
|
+
workspaceId: input.workspaceId,
|
|
752
|
+
subjectId: input.subjectId,
|
|
753
|
+
connectionId: context.connection.id,
|
|
754
|
+
parentId: input.parentId,
|
|
755
|
+
...(input.pageToken ? { pageToken: input.pageToken } : {}),
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export async function saveGoogleDriveFacetSource(
|
|
760
|
+
deps: ApiRouteDeps,
|
|
761
|
+
input: {
|
|
762
|
+
accountId: string;
|
|
763
|
+
workspaceId: string;
|
|
764
|
+
subjectId: string;
|
|
765
|
+
capabilityId: string;
|
|
766
|
+
instanceKey: string;
|
|
767
|
+
facetKey: string;
|
|
768
|
+
payload: unknown;
|
|
769
|
+
canManageOrganizationDestination: boolean;
|
|
770
|
+
canManageWorkspaceDestination: boolean;
|
|
771
|
+
canManagePersonalDestination: boolean;
|
|
772
|
+
},
|
|
773
|
+
): Promise<IntegrationFacetMutationResult> {
|
|
774
|
+
const parsedPayload = SaveGoogleDriveIntegrationSourceRequest.safeParse(input.payload);
|
|
775
|
+
if (!parsedPayload.success) {
|
|
776
|
+
throw new HTTPException(400, { message: "invalid Google Drive source selection" });
|
|
777
|
+
}
|
|
778
|
+
const payload = parsedPayload.data;
|
|
779
|
+
const context = await requireGoogleDriveIntegrationFacet(deps, input);
|
|
780
|
+
const documentDestination = googleDriveDocumentDestination(input, payload);
|
|
781
|
+
const verifiedSources = await verifyGoogleDriveSources(deps, {
|
|
782
|
+
workspaceId: input.workspaceId,
|
|
783
|
+
subjectId: input.subjectId,
|
|
784
|
+
connectionId: context.connection.id,
|
|
785
|
+
sources: payload.sources,
|
|
786
|
+
});
|
|
787
|
+
const config = GoogleDriveKnowledgeSourceConfig.parse({
|
|
788
|
+
sources: verifiedSources.map((verified) => ({
|
|
789
|
+
id: verified.id,
|
|
790
|
+
name: verified.name,
|
|
791
|
+
mimeType: verified.mimeType,
|
|
792
|
+
...(verified.driveId ? { driveId: verified.driveId } : {}),
|
|
793
|
+
sourceKind:
|
|
794
|
+
verified.id === "root"
|
|
795
|
+
? "my_drive"
|
|
796
|
+
: verified.driveId === verified.id
|
|
797
|
+
? "shared_drive"
|
|
798
|
+
: "folder",
|
|
799
|
+
includeDescendants: true,
|
|
800
|
+
})),
|
|
801
|
+
destination: {
|
|
802
|
+
authorityKind: documentDestination.authorityKind,
|
|
803
|
+
authorityAccountId: documentDestination.authorityAccountId,
|
|
804
|
+
...(documentDestination.authorityWorkspaceId
|
|
805
|
+
? { authorityWorkspaceId: documentDestination.authorityWorkspaceId }
|
|
806
|
+
: {}),
|
|
807
|
+
...(documentDestination.authoritySubjectId
|
|
808
|
+
? { authoritySubjectId: documentDestination.authoritySubjectId }
|
|
809
|
+
: {}),
|
|
810
|
+
...(documentDestination.collectionId
|
|
811
|
+
? { collectionId: documentDestination.collectionId }
|
|
812
|
+
: {}),
|
|
813
|
+
},
|
|
814
|
+
syncCadence: payload.syncCadence,
|
|
815
|
+
readPolicy: payload.readPolicy,
|
|
816
|
+
});
|
|
817
|
+
return IntegrationFacetMutationResult.parse(
|
|
818
|
+
await configureIntegrationFacet(deps.db, {
|
|
819
|
+
accountId: input.accountId,
|
|
820
|
+
workspaceId: input.workspaceId,
|
|
821
|
+
subjectId: input.subjectId,
|
|
822
|
+
capabilityId: input.capabilityId,
|
|
823
|
+
instanceKey: input.instanceKey,
|
|
824
|
+
facetKey: input.facetKey,
|
|
825
|
+
displayName:
|
|
826
|
+
context.facet.binding?.displayName ?? `${input.instanceKey} — Google Drive content`,
|
|
827
|
+
config,
|
|
828
|
+
...(payload.expectedVersion !== undefined
|
|
829
|
+
? { expectedVersion: payload.expectedVersion }
|
|
830
|
+
: {}),
|
|
831
|
+
idempotencyKey: payload.idempotencyKey,
|
|
832
|
+
}),
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
503
836
|
export async function saveGoogleDriveSource(
|
|
504
837
|
deps: ApiRouteDeps,
|
|
505
838
|
input: {
|
|
@@ -507,6 +840,7 @@ export async function saveGoogleDriveSource(
|
|
|
507
840
|
workspaceId: string;
|
|
508
841
|
subjectId: string;
|
|
509
842
|
connectionId: string;
|
|
843
|
+
grant: AccessGrant;
|
|
510
844
|
payload: unknown;
|
|
511
845
|
canManageOrganizationDestination: boolean;
|
|
512
846
|
canManageWorkspaceDestination: boolean;
|
|
@@ -531,6 +865,133 @@ export async function saveGoogleDriveSource(
|
|
|
531
865
|
throw new HTTPException(403, { message: "Google Drive connection authority mismatch" });
|
|
532
866
|
}
|
|
533
867
|
await requireGoogleDriveSourceConnection(deps, existing, input.subjectId);
|
|
868
|
+
const documentDestination = googleDriveDocumentDestination(input, payload);
|
|
869
|
+
const verifiedSources = await verifyGoogleDriveSources(deps, {
|
|
870
|
+
workspaceId: input.workspaceId,
|
|
871
|
+
subjectId: input.subjectId,
|
|
872
|
+
connectionId: input.connectionId,
|
|
873
|
+
sources: payload.sources,
|
|
874
|
+
});
|
|
875
|
+
const latest =
|
|
876
|
+
(await getConnectionMetadata(
|
|
877
|
+
deps.db,
|
|
878
|
+
input.workspaceId,
|
|
879
|
+
input.connectionId,
|
|
880
|
+
input.subjectId,
|
|
881
|
+
)) ?? existing;
|
|
882
|
+
const latestMetadata = await requireGoogleDriveSourceConnection(deps, latest, input.subjectId);
|
|
883
|
+
const previousSources =
|
|
884
|
+
latestMetadata.selectedSources ??
|
|
885
|
+
(latestMetadata.selectedSource ? [latestMetadata.selectedSource] : []);
|
|
886
|
+
const updated = await transitionConnectionState(deps.db, {
|
|
887
|
+
workspaceId: input.workspaceId,
|
|
888
|
+
connectionId: latest.id,
|
|
889
|
+
visibleToSubjectId: input.subjectId,
|
|
890
|
+
expectedVersion: latest.version,
|
|
891
|
+
metadata: GoogleDriveConnectionMetadata.parse({
|
|
892
|
+
...latestMetadata,
|
|
893
|
+
documentDestination,
|
|
894
|
+
selectedSource: null,
|
|
895
|
+
selectedSources: verifiedSources.map((verified) => {
|
|
896
|
+
const previous = previousSources.find((source) => source.id === verified.id);
|
|
897
|
+
return {
|
|
898
|
+
id: verified.id,
|
|
899
|
+
name: verified.name,
|
|
900
|
+
mimeType: verified.mimeType,
|
|
901
|
+
driveId: verified.driveId,
|
|
902
|
+
destination: documentDestination,
|
|
903
|
+
syncCadence: payload.syncCadence,
|
|
904
|
+
syncEnabled: payload.syncEnabled,
|
|
905
|
+
configGeneration: (previous?.configGeneration ?? 0) + 1,
|
|
906
|
+
readPolicy: payload.readPolicy,
|
|
907
|
+
selectedAt: new Date().toISOString(),
|
|
908
|
+
};
|
|
909
|
+
}),
|
|
910
|
+
}),
|
|
911
|
+
updatedBySubjectId: input.subjectId,
|
|
912
|
+
});
|
|
913
|
+
if (!updated) {
|
|
914
|
+
throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
|
|
915
|
+
}
|
|
916
|
+
await materializeGoogleDriveKnowledgeSchedules(deps, {
|
|
917
|
+
...input,
|
|
918
|
+
connection: updated,
|
|
919
|
+
metadata: GoogleDriveConnectionMetadata.parse(updated.metadata),
|
|
920
|
+
previouslySelectedSourceIds: new Set(
|
|
921
|
+
(
|
|
922
|
+
latestMetadata.selectedSources ??
|
|
923
|
+
(latestMetadata.selectedSource ? [latestMetadata.selectedSource] : [])
|
|
924
|
+
).map((source) => source.id),
|
|
925
|
+
),
|
|
926
|
+
previouslyEnabledSourceIds: new Set(
|
|
927
|
+
(
|
|
928
|
+
latestMetadata.selectedSources ??
|
|
929
|
+
(latestMetadata.selectedSource ? [latestMetadata.selectedSource] : [])
|
|
930
|
+
)
|
|
931
|
+
.filter((source) => source.syncEnabled)
|
|
932
|
+
.map((source) => source.id),
|
|
933
|
+
),
|
|
934
|
+
});
|
|
935
|
+
return updated;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function requireGoogleDriveIntegrationFacet(
|
|
939
|
+
deps: ApiRouteDeps,
|
|
940
|
+
input: {
|
|
941
|
+
workspaceId: string;
|
|
942
|
+
subjectId: string;
|
|
943
|
+
capabilityId: string;
|
|
944
|
+
instanceKey: string;
|
|
945
|
+
facetKey: string;
|
|
946
|
+
},
|
|
947
|
+
) {
|
|
948
|
+
const instance = await listIntegrationInstanceFacets(
|
|
949
|
+
deps.db,
|
|
950
|
+
input.workspaceId,
|
|
951
|
+
input.subjectId,
|
|
952
|
+
input.capabilityId,
|
|
953
|
+
input.instanceKey,
|
|
954
|
+
);
|
|
955
|
+
const facet = instance.facets.find(
|
|
956
|
+
(candidate) => candidate.definition.facetKey === input.facetKey,
|
|
957
|
+
);
|
|
958
|
+
if (
|
|
959
|
+
!facet ||
|
|
960
|
+
facet.definition.kind !== "knowledge_source" ||
|
|
961
|
+
facet.definition.capabilities.provider !== "google-drive"
|
|
962
|
+
) {
|
|
963
|
+
throw new HTTPException(404, { message: "Google Drive knowledge source not found" });
|
|
964
|
+
}
|
|
965
|
+
if (!instance.connectionId) {
|
|
966
|
+
throw new HTTPException(422, { message: "Google Drive Integration has no Connection" });
|
|
967
|
+
}
|
|
968
|
+
const connection = await getConnectionMetadata(
|
|
969
|
+
deps.db,
|
|
970
|
+
input.workspaceId,
|
|
971
|
+
instance.connectionId,
|
|
972
|
+
input.subjectId,
|
|
973
|
+
);
|
|
974
|
+
if (!connection) {
|
|
975
|
+
throw new HTTPException(404, { message: "Google Drive Connection not found" });
|
|
976
|
+
}
|
|
977
|
+
await requireGoogleDriveApiConnection(deps, connection, input.subjectId);
|
|
978
|
+
return { instance, facet, connection };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function googleDriveDocumentDestination(
|
|
982
|
+
input: {
|
|
983
|
+
accountId: string;
|
|
984
|
+
workspaceId: string;
|
|
985
|
+
subjectId: string;
|
|
986
|
+
canManageOrganizationDestination: boolean;
|
|
987
|
+
canManageWorkspaceDestination: boolean;
|
|
988
|
+
canManagePersonalDestination: boolean;
|
|
989
|
+
},
|
|
990
|
+
payload: {
|
|
991
|
+
destination?: ConnectorDocumentDestinationSelection | undefined;
|
|
992
|
+
targetScope?: "user" | "workspace" | "organization" | undefined;
|
|
993
|
+
},
|
|
994
|
+
) {
|
|
534
995
|
const destinationSelection: ConnectorDocumentDestinationSelection = payload.destination ?? {
|
|
535
996
|
authorityKind: "workspace",
|
|
536
997
|
collectionId: null,
|
|
@@ -547,13 +1008,24 @@ export async function saveGoogleDriveSource(
|
|
|
547
1008
|
if (destinationSelection.authorityKind === "personal" && !input.canManagePersonalDestination) {
|
|
548
1009
|
throw new HTTPException(403, { message: "personal destination requires the exact actor" });
|
|
549
1010
|
}
|
|
550
|
-
|
|
1011
|
+
return bindConnectorDocumentDestination(destinationSelection, {
|
|
551
1012
|
accountId: input.accountId,
|
|
552
1013
|
workspaceId: input.workspaceId,
|
|
553
1014
|
initiatingSubjectId: input.subjectId,
|
|
554
1015
|
});
|
|
555
|
-
|
|
556
|
-
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async function verifyGoogleDriveSources(
|
|
1019
|
+
deps: ApiRouteDeps,
|
|
1020
|
+
input: {
|
|
1021
|
+
workspaceId: string;
|
|
1022
|
+
subjectId: string;
|
|
1023
|
+
connectionId: string;
|
|
1024
|
+
sources: Array<{ id: string; name: string; mimeType: string; driveId: string | null }>;
|
|
1025
|
+
},
|
|
1026
|
+
): Promise<GoogleDriveBrowseItem[]> {
|
|
1027
|
+
const verifiedSources: GoogleDriveBrowseItem[] = [];
|
|
1028
|
+
for (const source of input.sources) {
|
|
557
1029
|
const sourceId = validDriveId(source.id, "source.id");
|
|
558
1030
|
const verified = await resolveGoogleDriveBoundaryItem(deps, {
|
|
559
1031
|
workspaceId: input.workspaceId,
|
|
@@ -572,40 +1044,380 @@ export async function saveGoogleDriveSource(
|
|
|
572
1044
|
}
|
|
573
1045
|
verifiedSources.push(verified);
|
|
574
1046
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1047
|
+
return verifiedSources;
|
|
1048
|
+
}
|
|
1049
|
+
async function materializeGoogleDriveKnowledgeSchedules(
|
|
1050
|
+
deps: ApiRouteDeps,
|
|
1051
|
+
input: {
|
|
1052
|
+
accountId: string;
|
|
1053
|
+
workspaceId: string;
|
|
1054
|
+
subjectId: string;
|
|
1055
|
+
connectionId: string;
|
|
1056
|
+
grant: AccessGrant;
|
|
1057
|
+
connection: GoogleDriveConnectionRecord;
|
|
1058
|
+
metadata: ReturnType<typeof GoogleDriveConnectionMetadata.parse>;
|
|
1059
|
+
previouslySelectedSourceIds: Set<string>;
|
|
1060
|
+
previouslyEnabledSourceIds: Set<string>;
|
|
1061
|
+
},
|
|
1062
|
+
): Promise<void> {
|
|
1063
|
+
const selectedSources = input.metadata.selectedSources ?? [];
|
|
1064
|
+
const enabledSources = selectedSources.filter((source) => source.syncEnabled);
|
|
1065
|
+
const enabledIds = new Set(enabledSources.map((source) => source.id));
|
|
1066
|
+
const connectionTasks = await listKnowledgeSourceSyncTasksForConnection(
|
|
1067
|
+
deps.db,
|
|
1068
|
+
input.workspaceId,
|
|
1069
|
+
input.connectionId,
|
|
1070
|
+
);
|
|
1071
|
+
const actor = {
|
|
1072
|
+
kind: "human" as const,
|
|
1073
|
+
subjectId: input.subjectId,
|
|
1074
|
+
initiatingHumanSubjectId: input.subjectId,
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
for (const selectedSource of enabledSources) {
|
|
1078
|
+
const explicitlyEnabled = !input.previouslyEnabledSourceIds.has(selectedSource.id);
|
|
1079
|
+
const identity = googleDriveKnowledgeSourceIdentity({
|
|
1080
|
+
googlePermissionId: input.metadata.googlePermissionId,
|
|
1081
|
+
googleEmail: input.metadata.googleEmail,
|
|
1082
|
+
source: selectedSource,
|
|
1083
|
+
accountId: input.accountId,
|
|
1084
|
+
workspaceId: input.workspaceId,
|
|
1085
|
+
connectionSubjectId: input.subjectId,
|
|
1086
|
+
});
|
|
1087
|
+
let source = null as Awaited<ReturnType<typeof upsertKnowledgeSource>> | null;
|
|
1088
|
+
let existingTask = null as (typeof connectionTasks)[number] | null;
|
|
1089
|
+
for (const task of connectionTasks) {
|
|
1090
|
+
if (task.action.kind !== "knowledge_source_sync") continue;
|
|
1091
|
+
const resolved = await getKnowledgeSourceForSyncAuthority(deps.db, {
|
|
1092
|
+
accountId: input.accountId,
|
|
1093
|
+
workspaceId: input.workspaceId,
|
|
1094
|
+
sourceId: task.action.sourceId,
|
|
1095
|
+
initiatingSubjectId: input.subjectId,
|
|
1096
|
+
});
|
|
1097
|
+
if (resolved?.source.externalSourceId !== identity.externalSourceId) continue;
|
|
1098
|
+
source = resolved.source;
|
|
1099
|
+
existingTask = task;
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
if (!source) {
|
|
1103
|
+
const provider = await upsertKnowledgeProvider(deps.db, {
|
|
1104
|
+
accountId: input.accountId,
|
|
1105
|
+
workspaceId: input.workspaceId,
|
|
1106
|
+
scope: identity.scope,
|
|
1107
|
+
providerKey: identity.providerKey,
|
|
1108
|
+
externalTenantId: identity.externalTenantId,
|
|
1109
|
+
operationId: `google-drive-provider:${input.connectionId}`,
|
|
1110
|
+
actor,
|
|
1111
|
+
});
|
|
1112
|
+
source =
|
|
1113
|
+
(await getKnowledgeSourceByExternalIdentityForSyncAuthority(deps.db, {
|
|
1114
|
+
accountId: input.accountId,
|
|
1115
|
+
workspaceId: input.workspaceId,
|
|
1116
|
+
providerId: provider.id,
|
|
1117
|
+
externalSourceId: identity.externalSourceId,
|
|
1118
|
+
initiatingSubjectId: input.subjectId,
|
|
1119
|
+
})) ??
|
|
1120
|
+
(await upsertKnowledgeSource(deps.db, {
|
|
1121
|
+
accountId: input.accountId,
|
|
1122
|
+
workspaceId: input.workspaceId,
|
|
1123
|
+
scope: identity.scope,
|
|
1124
|
+
providerId: provider.id,
|
|
1125
|
+
externalSourceId: identity.externalSourceId,
|
|
1126
|
+
sourceKind: identity.sourceKind,
|
|
1127
|
+
sourceUri: identity.sourceUri,
|
|
1128
|
+
operationId: `google-drive-source:${input.connectionId}:${identity.externalSourceId}`,
|
|
1129
|
+
actor,
|
|
1130
|
+
}));
|
|
1131
|
+
}
|
|
1132
|
+
if (source.lifecycleState !== "active") {
|
|
1133
|
+
if (!explicitlyEnabled) continue;
|
|
1134
|
+
const restored = await recordKnowledgeLifecycleEvent(deps.db, {
|
|
1135
|
+
accountId: input.accountId,
|
|
1136
|
+
workspaceId: input.workspaceId,
|
|
1137
|
+
targetKind: "source",
|
|
1138
|
+
targetId: source.id,
|
|
1139
|
+
eventType: "restored",
|
|
1140
|
+
expectedGeneration: source.lifecycleGeneration,
|
|
1141
|
+
operationId: `google-drive-source-restore:${source.id}:${input.connection.version}`,
|
|
1142
|
+
reasonCode: "source_explicitly_reenabled",
|
|
1143
|
+
actor,
|
|
1144
|
+
});
|
|
1145
|
+
source = {
|
|
1146
|
+
...source,
|
|
1147
|
+
lifecycleState: "active",
|
|
1148
|
+
lifecycleGeneration: restored.lifecycleGeneration,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
if (!source.currentAclGeneration) {
|
|
1152
|
+
await appendKnowledgeSourceAclVersion(deps.db, {
|
|
1153
|
+
accountId: input.accountId,
|
|
1154
|
+
workspaceId: input.workspaceId,
|
|
1155
|
+
sourceId: source.id,
|
|
1156
|
+
audience: identity.scope,
|
|
1157
|
+
expectedSourceLifecycleGeneration: source.lifecycleGeneration,
|
|
1158
|
+
expectedAclGeneration: 0,
|
|
1159
|
+
aclVersion: `google-drive-destination:${input.connection.version}`,
|
|
1160
|
+
agentAccess: false,
|
|
1161
|
+
operationId: `google-drive-source-acl:${source.id}`,
|
|
1162
|
+
reasonCode: "source_selected",
|
|
1163
|
+
actor,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
const action = {
|
|
1167
|
+
kind: "knowledge_source_sync" as const,
|
|
1168
|
+
sourceId: source.id,
|
|
1169
|
+
sourceGeneration: source.syncGeneration,
|
|
1170
|
+
sourceLifecycleGeneration: source.lifecycleGeneration,
|
|
1171
|
+
sourceConfigGeneration: selectedSource.configGeneration,
|
|
1172
|
+
controlWorkspaceId: input.workspaceId,
|
|
1173
|
+
providerCoordinationKey: `${identity.providerKey}:${identity.externalTenantId}:${selectedSource.driveId ?? "my-drive"}`,
|
|
1174
|
+
destination: identity.scope,
|
|
1175
|
+
initiatingSubjectId: input.subjectId,
|
|
1176
|
+
allDescendants: true,
|
|
1177
|
+
connection: {
|
|
1178
|
+
connectionId: input.connectionId,
|
|
1179
|
+
ownerSubjectId: input.subjectId,
|
|
1180
|
+
connectionVersion: input.connection.version,
|
|
1181
|
+
providerDomain: input.connection.providerDomain,
|
|
1182
|
+
kind: input.connection.kind,
|
|
1183
|
+
},
|
|
1184
|
+
limits: {
|
|
1185
|
+
maxItems: 500,
|
|
1186
|
+
maxBytes: 500_000_000,
|
|
1187
|
+
maxFileBytes: 100_000_000,
|
|
1188
|
+
maxProviderRequests: 1_000,
|
|
1189
|
+
maxElapsedSeconds: 300,
|
|
1190
|
+
maxConcurrency: 4,
|
|
1191
|
+
maxFailureDetails: 25,
|
|
1192
|
+
},
|
|
1193
|
+
};
|
|
1194
|
+
const schedule = googleDriveSchedule(selectedSource.syncCadence);
|
|
1195
|
+
let task;
|
|
1196
|
+
if (existingTask) {
|
|
1197
|
+
const previous = await captureScheduledTaskRestoreState(deps.db, existingTask);
|
|
1198
|
+
task = await updateScheduledTask(deps.db, input.workspaceId, existingTask.id, {
|
|
1199
|
+
name: `Sync Google Drive: ${selectedSource.name}`,
|
|
1200
|
+
overlapPolicy: "buffer_one",
|
|
1201
|
+
action,
|
|
1202
|
+
metadata: {
|
|
1203
|
+
...existingTask.metadata,
|
|
1204
|
+
connectorKind: "google_drive",
|
|
1205
|
+
connectionId: input.connectionId,
|
|
1206
|
+
externalSourceId: selectedSource.id,
|
|
1207
|
+
knowledgeSourceSync: {
|
|
1208
|
+
...knowledgeSourceScheduleControl(existingTask),
|
|
1209
|
+
sourceEnabled: true,
|
|
1210
|
+
connectionPaused: false,
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
await syncUpdatedScheduledTask({
|
|
1215
|
+
db: deps.db,
|
|
1216
|
+
workflowClient: deps.workflowClient,
|
|
1217
|
+
previous,
|
|
1218
|
+
task,
|
|
1219
|
+
});
|
|
1220
|
+
} else {
|
|
1221
|
+
task = await createValidatedScheduledTask({
|
|
1222
|
+
settings: deps.settings,
|
|
1223
|
+
db: deps.db,
|
|
1224
|
+
objectStorage: deps.objectStorage,
|
|
1225
|
+
grant: input.grant,
|
|
1226
|
+
authorizationSurface: "http",
|
|
1227
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
1228
|
+
payload: {
|
|
1229
|
+
name: `Sync Google Drive: ${selectedSource.name}`,
|
|
1230
|
+
status: "active",
|
|
1231
|
+
schedule,
|
|
1232
|
+
overlapPolicy: "buffer_one",
|
|
1233
|
+
action,
|
|
1234
|
+
runMode: "new_session_per_run",
|
|
1235
|
+
targetSessionId: null,
|
|
1236
|
+
agentConfig: {
|
|
1237
|
+
prompt: "Knowledge source synchronization",
|
|
1238
|
+
resources: [],
|
|
1239
|
+
tools: [],
|
|
1240
|
+
metadata: {},
|
|
1241
|
+
},
|
|
1242
|
+
variableSetId: null,
|
|
1243
|
+
environmentId: null,
|
|
1244
|
+
rigId: null,
|
|
1245
|
+
metadata: {
|
|
1246
|
+
connectorKind: "google_drive",
|
|
1247
|
+
connectionId: input.connectionId,
|
|
1248
|
+
externalSourceId: selectedSource.id,
|
|
1249
|
+
knowledgeSourceSync: { sourceEnabled: true, connectionPaused: false },
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
});
|
|
1253
|
+
await syncCreatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, task });
|
|
1254
|
+
}
|
|
1255
|
+
const triggerToken = `source-save-${input.connection.version}`;
|
|
1256
|
+
await deps.workflowClient.triggerScheduledTask({
|
|
1257
|
+
task,
|
|
1258
|
+
agentRunUsageIdempotencyKey: `knowledge-source-sync:initial:${task.id}:${triggerToken}`,
|
|
1259
|
+
triggerWorkflowId: manualScheduledTaskTriggerWorkflowId(task.id, triggerToken),
|
|
1260
|
+
initiator: { kind: "subject", subjectId: input.subjectId },
|
|
1261
|
+
triggerType: existingTask ? "repair" : "initial",
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
for (const task of connectionTasks) {
|
|
1266
|
+
if (task.action.kind !== "knowledge_source_sync") continue;
|
|
1267
|
+
const resolved = await getKnowledgeSourceForSyncAuthority(deps.db, {
|
|
1268
|
+
accountId: input.accountId,
|
|
1269
|
+
workspaceId: input.workspaceId,
|
|
1270
|
+
sourceId: task.action.sourceId,
|
|
1271
|
+
initiatingSubjectId: input.subjectId,
|
|
1272
|
+
});
|
|
1273
|
+
if (!resolved || enabledIds.has(resolved.source.externalSourceId)) continue;
|
|
1274
|
+
if (
|
|
1275
|
+
input.previouslySelectedSourceIds.has(resolved.source.externalSourceId) &&
|
|
1276
|
+
resolved.source.lifecycleState === "active"
|
|
1277
|
+
) {
|
|
1278
|
+
await recordKnowledgeLifecycleEvent(deps.db, {
|
|
1279
|
+
accountId: input.accountId,
|
|
1280
|
+
workspaceId: input.workspaceId,
|
|
1281
|
+
targetKind: "source",
|
|
1282
|
+
targetId: resolved.source.id,
|
|
1283
|
+
eventType: "deleted",
|
|
1284
|
+
expectedGeneration: resolved.source.lifecycleGeneration,
|
|
1285
|
+
operationId: `google-drive-source-deselect:${resolved.source.id}:${input.connection.version}`,
|
|
1286
|
+
reasonCode: "source_deselected",
|
|
1287
|
+
actor,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
const previous = await captureScheduledTaskRestoreState(deps.db, task);
|
|
1291
|
+
const disabled = await updateScheduledTask(deps.db, input.workspaceId, task.id, {
|
|
1292
|
+
metadata: {
|
|
1293
|
+
...task.metadata,
|
|
1294
|
+
knowledgeSourceSync: {
|
|
1295
|
+
...knowledgeSourceScheduleControl(task),
|
|
1296
|
+
sourceEnabled: false,
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
await syncUpdatedScheduledTask({
|
|
1301
|
+
db: deps.db,
|
|
1302
|
+
workflowClient: deps.workflowClient,
|
|
1303
|
+
previous,
|
|
1304
|
+
task: disabled,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
export async function revokeKnowledgeSourceScheduleAuthorization(
|
|
1310
|
+
deps: ApiRouteDeps,
|
|
1311
|
+
input: { task: ScheduledTask; subjectId: string },
|
|
1312
|
+
): Promise<void> {
|
|
1313
|
+
const { task } = input;
|
|
1314
|
+
if (task.action.kind !== "knowledge_source_sync") return;
|
|
1315
|
+
if (
|
|
1316
|
+
task.action.initiatingSubjectId !== input.subjectId ||
|
|
1317
|
+
task.action.connection.ownerSubjectId !== input.subjectId
|
|
1318
|
+
) {
|
|
1319
|
+
throw new HTTPException(403, {
|
|
1320
|
+
message: "knowledge source schedule requires the exact initiating subject",
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
if (task.metadata.connectorKind !== "google_drive") {
|
|
1324
|
+
throw new HTTPException(409, {
|
|
1325
|
+
message: "knowledge source schedule connector cannot be durably disabled",
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
const externalSourceId =
|
|
1329
|
+
typeof task.metadata.externalSourceId === "string" ? task.metadata.externalSourceId.trim() : "";
|
|
1330
|
+
if (!externalSourceId) {
|
|
1331
|
+
throw new HTTPException(409, { message: "knowledge source schedule identity is incomplete" });
|
|
1332
|
+
}
|
|
1333
|
+
const connection = await getConnectionMetadata(
|
|
1334
|
+
deps.db,
|
|
1335
|
+
task.workspaceId,
|
|
1336
|
+
task.action.connection.connectionId,
|
|
1337
|
+
input.subjectId,
|
|
1338
|
+
);
|
|
1339
|
+
if (!connection) {
|
|
1340
|
+
throw new HTTPException(409, {
|
|
1341
|
+
message: "knowledge source connection is unavailable for durable disable",
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
const metadata = requireGoogleDriveConnection(connection, input.subjectId);
|
|
1345
|
+
const sources =
|
|
1346
|
+
metadata.selectedSources ?? (metadata.selectedSource ? [metadata.selectedSource] : []);
|
|
1347
|
+
const selected = sources.find((source) => source.id === externalSourceId);
|
|
1348
|
+
let revocationVersion = connection.version;
|
|
1349
|
+
if (selected?.syncEnabled) {
|
|
1350
|
+
const updated = await transitionConnectionState(deps.db, {
|
|
1351
|
+
workspaceId: task.workspaceId,
|
|
1352
|
+
connectionId: connection.id,
|
|
1353
|
+
visibleToSubjectId: input.subjectId,
|
|
1354
|
+
expectedVersion: connection.version,
|
|
1355
|
+
metadata: GoogleDriveConnectionMetadata.parse({
|
|
1356
|
+
...metadata,
|
|
1357
|
+
selectedSource: null,
|
|
1358
|
+
selectedSources: sources.map((source) =>
|
|
1359
|
+
source.id === externalSourceId
|
|
1360
|
+
? {
|
|
1361
|
+
...source,
|
|
1362
|
+
syncEnabled: false,
|
|
1363
|
+
configGeneration: source.configGeneration + 1,
|
|
1364
|
+
}
|
|
1365
|
+
: source,
|
|
1366
|
+
),
|
|
1367
|
+
}),
|
|
1368
|
+
updatedBySubjectId: input.subjectId,
|
|
1369
|
+
});
|
|
1370
|
+
if (!updated) {
|
|
1371
|
+
throw new HTTPException(409, {
|
|
1372
|
+
message: "knowledge source connection changed; retry schedule deletion",
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
revocationVersion = updated.version;
|
|
1376
|
+
}
|
|
1377
|
+
const resolved = await getKnowledgeSourceForSyncAuthority(deps.db, {
|
|
1378
|
+
accountId: task.accountId,
|
|
1379
|
+
workspaceId: task.workspaceId,
|
|
1380
|
+
sourceId: task.action.sourceId,
|
|
1381
|
+
initiatingSubjectId: input.subjectId,
|
|
604
1382
|
});
|
|
605
|
-
if (
|
|
606
|
-
|
|
1383
|
+
if (resolved?.source.lifecycleState === "active") {
|
|
1384
|
+
await recordKnowledgeLifecycleEvent(deps.db, {
|
|
1385
|
+
accountId: task.accountId,
|
|
1386
|
+
workspaceId: task.workspaceId,
|
|
1387
|
+
targetKind: "source",
|
|
1388
|
+
targetId: resolved.source.id,
|
|
1389
|
+
eventType: "deleted",
|
|
1390
|
+
expectedGeneration: resolved.source.lifecycleGeneration,
|
|
1391
|
+
operationId: `knowledge-schedule-delete:${task.id}:${revocationVersion}`,
|
|
1392
|
+
reasonCode: "schedule_deleted",
|
|
1393
|
+
actor: {
|
|
1394
|
+
kind: "human",
|
|
1395
|
+
subjectId: input.subjectId,
|
|
1396
|
+
initiatingHumanSubjectId: input.subjectId,
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
607
1399
|
}
|
|
608
|
-
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function knowledgeSourceScheduleControl(task: ScheduledTask): {
|
|
1403
|
+
sourceEnabled: boolean;
|
|
1404
|
+
connectionPaused: boolean;
|
|
1405
|
+
} {
|
|
1406
|
+
const value = task.metadata.knowledgeSourceSync;
|
|
1407
|
+
if (!value || typeof value !== "object") {
|
|
1408
|
+
return { sourceEnabled: true, connectionPaused: false };
|
|
1409
|
+
}
|
|
1410
|
+
const record = value as Record<string, unknown>;
|
|
1411
|
+
return {
|
|
1412
|
+
sourceEnabled: record.sourceEnabled !== false,
|
|
1413
|
+
connectionPaused: record.connectionPaused === true,
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function googleDriveSchedule(cadence: "manual" | "hourly" | "daily"): ScheduledTaskScheduleSpec {
|
|
1418
|
+
if (cadence === "manual") return { type: "manual" };
|
|
1419
|
+
if (cadence === "hourly") return { type: "interval", everySeconds: 3_600 };
|
|
1420
|
+
return { type: "calendar", timeZone: "UTC", hour: 0, minute: 0 };
|
|
609
1421
|
}
|
|
610
1422
|
|
|
611
1423
|
function googleDriveFileMetadataUrl(fileId: string): URL {
|
|
@@ -715,6 +1527,62 @@ function requireGoogleDriveConnection(connection: GoogleDriveConnectionRecord, s
|
|
|
715
1527
|
return parsed.data;
|
|
716
1528
|
}
|
|
717
1529
|
|
|
1530
|
+
async function requireGoogleDriveApiConnection(
|
|
1531
|
+
deps: ApiRouteDeps,
|
|
1532
|
+
connection: GoogleDriveConnectionRecord,
|
|
1533
|
+
subjectId: string,
|
|
1534
|
+
): Promise<GoogleDriveApiConnection> {
|
|
1535
|
+
const legacy = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
|
|
1536
|
+
const integration = ApiIntegrationOAuthConnectionMetadata.safeParse(connection.metadata);
|
|
1537
|
+
const genericProviderDomain = integrationDefinitionProviderDomain(
|
|
1538
|
+
GOOGLE_DRIVE_INTEGRATION_DEFINITION,
|
|
1539
|
+
);
|
|
1540
|
+
const authority: GoogleDriveApiConnection | null =
|
|
1541
|
+
connection.kind === "oauth2" &&
|
|
1542
|
+
connection.subjectId === subjectId &&
|
|
1543
|
+
connection.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
|
|
1544
|
+
legacy.success
|
|
1545
|
+
? { kind: "legacy", connection, metadata: legacy.data }
|
|
1546
|
+
: connection.kind === "oauth2" &&
|
|
1547
|
+
(connection.subjectId === null || connection.subjectId === subjectId) &&
|
|
1548
|
+
connection.providerDomain === genericProviderDomain &&
|
|
1549
|
+
integration.success &&
|
|
1550
|
+
integration.data.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE &&
|
|
1551
|
+
integration.data.providerFamily === "google" &&
|
|
1552
|
+
integration.data.authorizedDefinitionIds.includes(GOOGLE_DRIVE_INTEGRATION_DEFINITION.id)
|
|
1553
|
+
? { kind: "integration", connection, metadata: integration.data }
|
|
1554
|
+
: null;
|
|
1555
|
+
if (!authority) {
|
|
1556
|
+
throw new HTTPException(422, { message: "Connection is not compatible with Google Drive" });
|
|
1557
|
+
}
|
|
1558
|
+
if (authority.kind === "legacy") {
|
|
1559
|
+
await requireGoogleDriveSourceConnection(deps, connection, subjectId);
|
|
1560
|
+
return authority;
|
|
1561
|
+
}
|
|
1562
|
+
if (connection.status === "revoked") {
|
|
1563
|
+
throw new HTTPException(409, { message: "Google Drive is disconnected" });
|
|
1564
|
+
}
|
|
1565
|
+
if (connection.status !== "active") {
|
|
1566
|
+
throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
|
|
1567
|
+
}
|
|
1568
|
+
if (!googleDriveScopesAllowCapability(connection.grantedScopes, "recursive_source_sync")) {
|
|
1569
|
+
await transitionConnectionState(deps.db, {
|
|
1570
|
+
workspaceId: connection.workspaceId,
|
|
1571
|
+
connectionId: connection.id,
|
|
1572
|
+
visibleToSubjectId: subjectId,
|
|
1573
|
+
expectedVersion: connection.version,
|
|
1574
|
+
status: "needs_reauth",
|
|
1575
|
+
metadata: connection.metadata,
|
|
1576
|
+
lastError: "google_drive_reconsent_required",
|
|
1577
|
+
updatedBySubjectId: subjectId,
|
|
1578
|
+
});
|
|
1579
|
+
throw new HTTPException(401, {
|
|
1580
|
+
message: "Google Drive needs permission re-consent for selected-source read access",
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
return authority;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
718
1586
|
function googleDriveLifecycle(
|
|
719
1587
|
state: GoogleDriveConnectionLifecycle["state"],
|
|
720
1588
|
): GoogleDriveConnectionLifecycle {
|
|
@@ -744,11 +1612,21 @@ async function transitionGoogleDriveConnectionLifecycle(
|
|
|
744
1612
|
lastError: string | null,
|
|
745
1613
|
) {
|
|
746
1614
|
const metadata = requireGoogleDriveConnection(connection, subjectId);
|
|
747
|
-
|
|
1615
|
+
const alreadyCurrent =
|
|
748
1616
|
connection.status === status &&
|
|
749
1617
|
metadata.lifecycle?.state === lifecycle.state &&
|
|
750
|
-
metadata.lifecycle.recoverable === lifecycle.recoverable
|
|
751
|
-
) {
|
|
1618
|
+
metadata.lifecycle.recoverable === lifecycle.recoverable;
|
|
1619
|
+
if (lifecycle.state !== "active") {
|
|
1620
|
+
await deauthorizeGoogleDriveConnectionSources(
|
|
1621
|
+
deps,
|
|
1622
|
+
connection,
|
|
1623
|
+
subjectId,
|
|
1624
|
+
`connection_${lifecycle.state}`,
|
|
1625
|
+
connection.version + (alreadyCurrent ? 0 : 1),
|
|
1626
|
+
);
|
|
1627
|
+
await setGoogleDriveScheduleStatus(deps, connection.workspaceId, connection.id, "paused");
|
|
1628
|
+
}
|
|
1629
|
+
if (alreadyCurrent) {
|
|
752
1630
|
return connection;
|
|
753
1631
|
}
|
|
754
1632
|
return await transitionConnectionState(deps.db, {
|
|
@@ -821,21 +1699,31 @@ function readGoogleDriveOAuthState(
|
|
|
821
1699
|
if (iat === undefined || now < iat || now - iat > oauthStateTtlMs / 1000) {
|
|
822
1700
|
throw new HTTPException(400, { message: "invalid or expired Google Drive OAuth state" });
|
|
823
1701
|
}
|
|
1702
|
+
const accountId = requiredString(payload.accountId, "state.accountId");
|
|
1703
|
+
const workspaceId = requiredString(payload.workspaceId, "state.workspaceId");
|
|
1704
|
+
const subjectId = requiredString(payload.subjectId, "state.subjectId");
|
|
1705
|
+
const returnPath = requiredString(payload.returnPath, "state.returnPath");
|
|
1706
|
+
if (returnPath !== GOOGLE_DRIVE_RETURN_PATH(workspaceId)) {
|
|
1707
|
+
throw new HTTPException(400, { message: "invalid Google Drive OAuth return path" });
|
|
1708
|
+
}
|
|
1709
|
+
const connectionId = optionalString(payload.connectionId) ?? undefined;
|
|
1710
|
+
const connectionVersion = numberValue(payload.connectionVersion);
|
|
1711
|
+
if (
|
|
1712
|
+
(connectionVersion !== undefined && !Number.isInteger(connectionVersion)) ||
|
|
1713
|
+
Boolean(connectionId) !== Boolean(connectionVersion)
|
|
1714
|
+
) {
|
|
1715
|
+
throw new HTTPException(400, { message: "invalid Google Drive reconnect state" });
|
|
1716
|
+
}
|
|
824
1717
|
return {
|
|
825
|
-
accountId
|
|
826
|
-
workspaceId
|
|
827
|
-
subjectId
|
|
828
|
-
returnPath
|
|
1718
|
+
accountId,
|
|
1719
|
+
workspaceId,
|
|
1720
|
+
subjectId,
|
|
1721
|
+
returnPath,
|
|
829
1722
|
encryptedPkceVerifier: requiredString(
|
|
830
1723
|
payload.encryptedPkceVerifier,
|
|
831
1724
|
"state.encryptedPkceVerifier",
|
|
832
1725
|
),
|
|
833
|
-
...(
|
|
834
|
-
? { connectionId: optionalString(payload.connectionId)! }
|
|
835
|
-
: {}),
|
|
836
|
-
...(numberValue(payload.connectionVersion) !== undefined
|
|
837
|
-
? { connectionVersion: numberValue(payload.connectionVersion)! }
|
|
838
|
-
: {}),
|
|
1726
|
+
...(connectionId ? { connectionId, connectionVersion: connectionVersion! } : {}),
|
|
839
1727
|
nonce: requiredString(payload.nonce, "state.nonce"),
|
|
840
1728
|
iat,
|
|
841
1729
|
};
|
|
@@ -1052,11 +1940,15 @@ async function googleDriveApiRequest(
|
|
|
1052
1940
|
if (!current) {
|
|
1053
1941
|
throw new HTTPException(404, { message: "Google Drive connection not found" });
|
|
1054
1942
|
}
|
|
1055
|
-
await
|
|
1943
|
+
const authority = await requireGoogleDriveApiConnection(deps, current, input.subjectId);
|
|
1056
1944
|
const resolver = buildConnectionTokenResolver(deps.db, deps.settings, undefined, {
|
|
1057
1945
|
...(deps.googleDriveFetch ? { refreshTransport: { fetchImpl: deps.googleDriveFetch } } : {}),
|
|
1058
|
-
|
|
1059
|
-
|
|
1946
|
+
...(authority.kind === "legacy"
|
|
1947
|
+
? {
|
|
1948
|
+
transitionPermanentRefreshFailure: async (failure: PermanentConnectionRefreshFailure) =>
|
|
1949
|
+
await transitionGoogleDrivePermanentRefreshFailure(deps, failure),
|
|
1950
|
+
}
|
|
1951
|
+
: {}),
|
|
1060
1952
|
});
|
|
1061
1953
|
const resolve = async (forceRefresh: boolean) =>
|
|
1062
1954
|
await resolver({
|
|
@@ -1065,14 +1957,26 @@ async function googleDriveApiRequest(
|
|
|
1065
1957
|
serverId: "google-drive-source-browser",
|
|
1066
1958
|
toolName: input.label,
|
|
1067
1959
|
connectionRef: {
|
|
1068
|
-
providerDomain:
|
|
1960
|
+
providerDomain: current.providerDomain,
|
|
1069
1961
|
connectionId: input.connectionId,
|
|
1070
1962
|
kind: "oauth2",
|
|
1071
|
-
subjectScope: "subject",
|
|
1963
|
+
subjectScope: current.subjectId ? "subject" : "workspace",
|
|
1072
1964
|
},
|
|
1073
1965
|
destinationUrl: input.url.toString(),
|
|
1074
1966
|
forceRefresh,
|
|
1075
1967
|
});
|
|
1968
|
+
const requireResolvedConnection = async (connectionVersion: number) => {
|
|
1969
|
+
const resolved = await getConnectionMetadata(
|
|
1970
|
+
deps.db,
|
|
1971
|
+
input.workspaceId,
|
|
1972
|
+
input.connectionId,
|
|
1973
|
+
input.subjectId,
|
|
1974
|
+
);
|
|
1975
|
+
if (!resolved || resolved.version !== connectionVersion) {
|
|
1976
|
+
throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
|
|
1977
|
+
}
|
|
1978
|
+
await requireGoogleDriveSourceConnection(deps, resolved, input.subjectId);
|
|
1979
|
+
};
|
|
1076
1980
|
let credential = await resolve(false);
|
|
1077
1981
|
if (credential.status !== "ok") {
|
|
1078
1982
|
throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
|
|
@@ -1081,6 +1985,7 @@ async function googleDriveApiRequest(
|
|
|
1081
1985
|
if (providerConnectionVersion === undefined) {
|
|
1082
1986
|
throw new Error("Google Drive credential resolver omitted the connection version");
|
|
1083
1987
|
}
|
|
1988
|
+
await requireResolvedConnection(providerConnectionVersion);
|
|
1084
1989
|
const fetchImpl = deps.googleDriveFetch ?? fetch;
|
|
1085
1990
|
let response = await providerFetch(fetchImpl, input.url, {
|
|
1086
1991
|
headers: { ...credential.headers, accept: "application/json" },
|
|
@@ -1095,6 +2000,7 @@ async function googleDriveApiRequest(
|
|
|
1095
2000
|
if (providerConnectionVersion === undefined) {
|
|
1096
2001
|
throw new Error("Google Drive credential resolver omitted the connection version");
|
|
1097
2002
|
}
|
|
2003
|
+
await requireResolvedConnection(providerConnectionVersion);
|
|
1098
2004
|
response = await providerFetch(fetchImpl, input.url, {
|
|
1099
2005
|
headers: { ...credential.headers, accept: "application/json" },
|
|
1100
2006
|
});
|
|
@@ -1102,13 +2008,10 @@ async function googleDriveApiRequest(
|
|
|
1102
2008
|
if (!response.ok) {
|
|
1103
2009
|
if (response.status === 401) {
|
|
1104
2010
|
await response.body?.cancel().catch(() => undefined);
|
|
1105
|
-
await
|
|
1106
|
-
workspaceId: input.workspaceId,
|
|
2011
|
+
await transitionGoogleDriveApiFailure(deps, authority, {
|
|
1107
2012
|
subjectId: input.subjectId,
|
|
1108
|
-
connectionId: input.connectionId,
|
|
1109
2013
|
connectionVersion: providerConnectionVersion,
|
|
1110
2014
|
lifecycle: googleDriveLifecycle("reconnect_required"),
|
|
1111
|
-
status: "needs_reauth",
|
|
1112
2015
|
lastError: "google_drive_reconnect_required",
|
|
1113
2016
|
});
|
|
1114
2017
|
throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
|
|
@@ -1123,13 +2026,10 @@ async function googleDriveApiRequest(
|
|
|
1123
2026
|
providerErrorCode &&
|
|
1124
2027
|
GOOGLE_DRIVE_RECONSENT_ERROR_CODES.has(providerErrorCode)
|
|
1125
2028
|
) {
|
|
1126
|
-
await
|
|
1127
|
-
workspaceId: input.workspaceId,
|
|
2029
|
+
await transitionGoogleDriveApiFailure(deps, authority, {
|
|
1128
2030
|
subjectId: input.subjectId,
|
|
1129
|
-
connectionId: input.connectionId,
|
|
1130
2031
|
connectionVersion: providerConnectionVersion,
|
|
1131
2032
|
lifecycle: googleDriveLifecycle("reconsent_required"),
|
|
1132
|
-
status: "needs_reauth",
|
|
1133
2033
|
lastError: "google_drive_reconsent_required",
|
|
1134
2034
|
});
|
|
1135
2035
|
}
|
|
@@ -1143,6 +2043,47 @@ async function googleDriveApiRequest(
|
|
|
1143
2043
|
return await readResponseJsonBounded<unknown>(response, GOOGLE_RESPONSE_MAX_BYTES, input.label);
|
|
1144
2044
|
}
|
|
1145
2045
|
|
|
2046
|
+
async function transitionGoogleDriveApiFailure(
|
|
2047
|
+
deps: ApiRouteDeps,
|
|
2048
|
+
authority: GoogleDriveApiConnection,
|
|
2049
|
+
input: {
|
|
2050
|
+
subjectId: string;
|
|
2051
|
+
connectionVersion: number;
|
|
2052
|
+
lifecycle: GoogleDriveConnectionLifecycle;
|
|
2053
|
+
lastError: string;
|
|
2054
|
+
},
|
|
2055
|
+
): Promise<void> {
|
|
2056
|
+
if (authority.kind === "legacy") {
|
|
2057
|
+
await transitionGoogleDriveProviderResponseFailure(deps, {
|
|
2058
|
+
workspaceId: authority.connection.workspaceId,
|
|
2059
|
+
subjectId: input.subjectId,
|
|
2060
|
+
connectionId: authority.connection.id,
|
|
2061
|
+
connectionVersion: input.connectionVersion,
|
|
2062
|
+
lifecycle: input.lifecycle,
|
|
2063
|
+
status: "needs_reauth",
|
|
2064
|
+
lastError: input.lastError,
|
|
2065
|
+
});
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
const latest = await getConnectionMetadata(
|
|
2069
|
+
deps.db,
|
|
2070
|
+
authority.connection.workspaceId,
|
|
2071
|
+
authority.connection.id,
|
|
2072
|
+
input.subjectId,
|
|
2073
|
+
);
|
|
2074
|
+
if (!latest || latest.version !== input.connectionVersion) return;
|
|
2075
|
+
await transitionConnectionState(deps.db, {
|
|
2076
|
+
workspaceId: latest.workspaceId,
|
|
2077
|
+
connectionId: latest.id,
|
|
2078
|
+
visibleToSubjectId: input.subjectId,
|
|
2079
|
+
expectedVersion: latest.version,
|
|
2080
|
+
status: "needs_reauth",
|
|
2081
|
+
metadata: latest.metadata,
|
|
2082
|
+
lastError: input.lastError,
|
|
2083
|
+
updatedBySubjectId: input.subjectId,
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1146
2087
|
async function providerFetch(
|
|
1147
2088
|
fetchImpl: FetchLike,
|
|
1148
2089
|
url: string | URL,
|