@opengeni/api-router 0.30.3 → 2.2.0-canary.0

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