@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.
Files changed (152) hide show
  1. package/dist/app.d.ts +3 -2
  2. package/dist/app.js +5 -3
  3. package/dist/auth/canonical-human-session-admission.d.ts +27 -0
  4. package/dist/browser-auth-broker.d.ts +20 -0
  5. package/dist/browser-controller-authority.d.ts +61 -0
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/browser-state-authority.d.ts +27 -0
  8. package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
  9. package/dist/chunk-2PQMSRCB.js.map +1 -0
  10. package/dist/codemode.d.ts +23 -0
  11. package/dist/controller-data-plane.d.ts +12 -0
  12. package/dist/editable-artifact-live-hints.d.ts +11 -0
  13. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  14. package/dist/editable-artifact-office-import.d.ts +22 -0
  15. package/dist/editable-artifact-production.d.ts +29 -0
  16. package/dist/editable-artifact-websocket.d.ts +49 -0
  17. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  18. package/dist/github-browser-flow.d.ts +6 -0
  19. package/dist/http/auth.d.ts +1 -1
  20. package/dist/http/cors.d.ts +1 -0
  21. package/dist/http/sse.d.ts +18 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +1796 -22
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/api-integrations.d.ts +24 -0
  26. package/dist/integrations/atlassian.d.ts +176 -0
  27. package/dist/integrations/fiken.d.ts +233 -0
  28. package/dist/integrations/github-skill-source.d.ts +5 -0
  29. package/dist/integrations/google-drive.d.ts +85 -0
  30. package/dist/integrations/oauth-client.d.ts +22 -1
  31. package/dist/integrations/provider-oauth.d.ts +17 -0
  32. package/dist/integrations/slack-bot.d.ts +175 -1
  33. package/dist/integrations/slack-interactions.d.ts +24 -6
  34. package/dist/integrations/social-api.d.ts +2 -1
  35. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  36. package/dist/interaction-metrics.d.ts +11 -0
  37. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  38. package/dist/mcp/editable-artifacts.d.ts +13 -0
  39. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  40. package/dist/mcp/server.d.ts +16 -4
  41. package/dist/memory-slack-delivery.d.ts +9 -0
  42. package/dist/model-catalog.d.ts +1 -0
  43. package/dist/routes/api-integrations.d.ts +8 -0
  44. package/dist/routes/browser-identities.d.ts +5 -0
  45. package/dist/routes/browser-sessions.d.ts +6 -0
  46. package/dist/routes/canonical-human-identities.d.ts +3 -0
  47. package/dist/routes/channels.d.ts +3 -0
  48. package/dist/routes/company-profile.d.ts +3 -0
  49. package/dist/routes/computer-sessions.d.ts +6 -0
  50. package/dist/routes/editable-artifacts.d.ts +44 -0
  51. package/dist/routes/integration-facets.d.ts +3 -0
  52. package/dist/routes/interaction-resources.d.ts +5 -0
  53. package/dist/routes/memory-slack-publications.d.ts +6 -0
  54. package/dist/routes/organization-memberships.d.ts +3 -0
  55. package/dist/routes/plugins.d.ts +8 -0
  56. package/dist/routes/sessions.d.ts +18 -2
  57. package/dist/routes/skills.d.ts +6 -0
  58. package/dist/routes/slack-task-policy.d.ts +3 -0
  59. package/dist/routes/supergrok.d.ts +3 -0
  60. package/dist/routes/video-generation.d.ts +3 -0
  61. package/dist/sandbox/channel-a.d.ts +62 -2
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  64. package/dist/sandbox/viewer.d.ts +7 -3
  65. package/dist/slack-reaction-files.d.ts +27 -0
  66. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  67. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  68. package/dist/xai-realtime.d.ts +26 -0
  69. package/dist/xai-subscription-auth.d.ts +23 -0
  70. package/package.json +20 -14
  71. package/src/app.ts +604 -64
  72. package/src/auth/canonical-human-session-admission.ts +116 -0
  73. package/src/auth/managed-auth.ts +101 -0
  74. package/src/browser-auth-broker.ts +155 -0
  75. package/src/browser-controller-authority.ts +186 -0
  76. package/src/browser-network-route.ts +34 -0
  77. package/src/browser-state-authority.ts +236 -0
  78. package/src/codemode.ts +186 -0
  79. package/src/controller-data-plane.ts +35 -0
  80. package/src/editable-artifact-live-hints.ts +64 -0
  81. package/src/editable-artifact-native-kernel.ts +659 -0
  82. package/src/editable-artifact-office-import.ts +230 -0
  83. package/src/editable-artifact-production.ts +419 -0
  84. package/src/editable-artifact-websocket.ts +311 -0
  85. package/src/editable-artifact-workspace-files.ts +186 -0
  86. package/src/github-browser-flow.ts +35 -6
  87. package/src/http/auth.ts +29 -9
  88. package/src/http/cors.ts +3 -0
  89. package/src/http/sse.ts +229 -5
  90. package/src/index.ts +119 -20
  91. package/src/integrations/api-integrations.ts +365 -0
  92. package/src/integrations/atlassian.ts +1621 -0
  93. package/src/integrations/fiken.ts +1230 -0
  94. package/src/integrations/github-skill-source.ts +142 -0
  95. package/src/integrations/google-drive.ts +1005 -64
  96. package/src/integrations/oauth-client.ts +123 -33
  97. package/src/integrations/provider-oauth.ts +756 -0
  98. package/src/integrations/slack-bot.ts +669 -11
  99. package/src/integrations/slack-interactions.ts +2063 -93
  100. package/src/integrations/social-api.ts +11 -0
  101. package/src/interaction-holder-heartbeat.ts +95 -0
  102. package/src/interaction-metrics.ts +159 -0
  103. package/src/mcp/documents.ts +146 -6
  104. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  105. package/src/mcp/editable-artifacts.ts +448 -0
  106. package/src/mcp/scheduled-task-view.ts +34 -0
  107. package/src/mcp/server.ts +1756 -355
  108. package/src/memory-slack-delivery.ts +209 -0
  109. package/src/model-catalog.ts +19 -6
  110. package/src/routes/api-integrations.ts +410 -0
  111. package/src/routes/browser-identities.ts +160 -0
  112. package/src/routes/browser-sessions.ts +4482 -0
  113. package/src/routes/canonical-human-identities.ts +156 -0
  114. package/src/routes/channels.ts +90 -0
  115. package/src/routes/company-profile.ts +255 -0
  116. package/src/routes/computer-sessions.ts +1602 -0
  117. package/src/routes/connections.ts +421 -99
  118. package/src/routes/documents.ts +22 -3
  119. package/src/routes/editable-artifacts.ts +1159 -0
  120. package/src/routes/files.ts +106 -4
  121. package/src/routes/github.ts +18 -2
  122. package/src/routes/install.ts +7 -1
  123. package/src/routes/integration-facets.ts +258 -0
  124. package/src/routes/interaction-resources.ts +595 -0
  125. package/src/routes/memory-slack-publications.ts +216 -0
  126. package/src/routes/organization-memberships.ts +53 -0
  127. package/src/routes/packs.ts +437 -7
  128. package/src/routes/plugins.ts +752 -0
  129. package/src/routes/rigs.ts +77 -20
  130. package/src/routes/scheduled-tasks.ts +67 -40
  131. package/src/routes/sessions.ts +830 -278
  132. package/src/routes/skills.ts +256 -0
  133. package/src/routes/slack-task-policy.ts +115 -0
  134. package/src/routes/supergrok.ts +717 -0
  135. package/src/routes/transcription-recordings.ts +9 -2
  136. package/src/routes/transcriptions.ts +2 -1
  137. package/src/routes/video-generation.ts +162 -0
  138. package/src/routes/workspaces.ts +68 -28
  139. package/src/sandbox/channel-a.ts +750 -116
  140. package/src/sandbox/machines.ts +5 -5
  141. package/src/sandbox/metrics-ingestion.ts +121 -3
  142. package/src/sandbox/rematerialize.ts +35 -47
  143. package/src/sandbox/viewer.ts +39 -12
  144. package/src/slack-reaction-files.ts +181 -0
  145. package/src/temporal-schedule-cleanup.ts +135 -0
  146. package/src/transcription/providers/xai-subscription.ts +110 -0
  147. package/src/transcription/service.ts +17 -2
  148. package/src/xai-realtime.ts +216 -0
  149. package/src/xai-subscription-auth.ts +132 -0
  150. package/dist/chunk-T4T2PGU4.js.map +0 -1
  151. package/dist/mcp/toolspace.d.ts +0 -71
  152. 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 { hasPermission, requireEnvironmentEncryption } from "@opengeni/core";
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
- return await disconnectConnectionIdempotently(deps.db, {
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 requireGoogleDriveSourceConnection(deps, connection, input.subjectId);
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
- const documentDestination = bindConnectorDocumentDestination(destinationSelection, {
1011
+ return bindConnectorDocumentDestination(destinationSelection, {
551
1012
  accountId: input.accountId,
552
1013
  workspaceId: input.workspaceId,
553
1014
  initiatingSubjectId: input.subjectId,
554
1015
  });
555
- const verifiedSources = [];
556
- for (const source of payload.sources) {
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
- const latest =
576
- (await getConnectionMetadata(
577
- deps.db,
578
- input.workspaceId,
579
- input.connectionId,
580
- input.subjectId,
581
- )) ?? existing;
582
- const latestMetadata = await requireGoogleDriveSourceConnection(deps, latest, input.subjectId);
583
- const updated = await transitionConnectionState(deps.db, {
584
- workspaceId: input.workspaceId,
585
- connectionId: latest.id,
586
- visibleToSubjectId: input.subjectId,
587
- expectedVersion: latest.version,
588
- metadata: GoogleDriveConnectionMetadata.parse({
589
- ...latestMetadata,
590
- documentDestination,
591
- selectedSource: null,
592
- selectedSources: verifiedSources.map((verified) => ({
593
- id: verified.id,
594
- name: verified.name,
595
- mimeType: verified.mimeType,
596
- driveId: verified.driveId,
597
- destination: documentDestination,
598
- syncCadence: payload.syncCadence,
599
- readPolicy: payload.readPolicy,
600
- selectedAt: new Date().toISOString(),
601
- })),
602
- }),
603
- updatedBySubjectId: input.subjectId,
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 (!updated) {
606
- throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
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
- return updated;
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
- if (
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: requiredString(payload.accountId, "state.accountId"),
826
- workspaceId: requiredString(payload.workspaceId, "state.workspaceId"),
827
- subjectId: requiredString(payload.subjectId, "state.subjectId"),
828
- returnPath: requiredString(payload.returnPath, "state.returnPath"),
1718
+ accountId,
1719
+ workspaceId,
1720
+ subjectId,
1721
+ returnPath,
829
1722
  encryptedPkceVerifier: requiredString(
830
1723
  payload.encryptedPkceVerifier,
831
1724
  "state.encryptedPkceVerifier",
832
1725
  ),
833
- ...(optionalString(payload.connectionId)
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 requireGoogleDriveSourceConnection(deps, current, input.subjectId);
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
- transitionPermanentRefreshFailure: async (failure) =>
1059
- await transitionGoogleDrivePermanentRefreshFailure(deps, failure),
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: GOOGLE_DRIVE_PROVIDER_DOMAIN,
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 transitionGoogleDriveProviderResponseFailure(deps, {
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 transitionGoogleDriveProviderResponseFailure(deps, {
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,