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