@opengeni/api-router 0.30.1 → 2.1.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 (167) 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-2PQMSRCB.js → chunk-QKDFBBUE.js} +29110 -18137
  5. package/dist/chunk-QKDFBBUE.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-office-import.d.ts +7 -4
  13. package/dist/editable-artifact-websocket.d.ts +5 -15
  14. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  15. package/dist/http/api-error.d.ts +10 -0
  16. package/dist/http/cors.d.ts +5 -0
  17. package/dist/http/interaction-control-error.d.ts +13 -0
  18. package/dist/http/sse.d.ts +2 -3
  19. package/dist/index.d.ts +5 -2
  20. package/dist/index.js +364 -86
  21. package/dist/index.js.map +1 -1
  22. package/dist/integrations/atlassian.d.ts +17 -0
  23. package/dist/integrations/fiken.d.ts +7 -2
  24. package/dist/integrations/google-drive.d.ts +19 -0
  25. package/dist/integrations/oauth-client.d.ts +22 -3
  26. package/dist/integrations/oauth-profiles.d.ts +195 -0
  27. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  28. package/dist/integrations/personal-github.d.ts +40 -0
  29. package/dist/integrations/pr-review-provider.d.ts +22 -0
  30. package/dist/integrations/provider-oauth.d.ts +8 -0
  31. package/dist/integrations/slack-app-home.d.ts +24 -0
  32. package/dist/integrations/slack-bot.d.ts +149 -9
  33. package/dist/integrations/slack-interactions.d.ts +10 -4
  34. package/dist/integrations/slack-routing.d.ts +120 -0
  35. package/dist/integrations/social-oauth.d.ts +5 -0
  36. package/dist/interaction-frame-proxy.d.ts +68 -0
  37. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  38. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  39. package/dist/mcp/documents.d.ts +1 -0
  40. package/dist/mcp/remember.d.ts +28 -0
  41. package/dist/mcp/request-abort.d.ts +19 -0
  42. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  43. package/dist/mcp/server.d.ts +15 -2
  44. package/dist/mcp/session-view.d.ts +4 -0
  45. package/dist/mcp/session-wait.d.ts +137 -0
  46. package/dist/routes/automations.d.ts +4 -0
  47. package/dist/routes/billing.d.ts +5 -0
  48. package/dist/routes/browser-sessions.d.ts +17 -3
  49. package/dist/routes/company-brain.d.ts +3 -0
  50. package/dist/routes/company-profile.d.ts +2 -1
  51. package/dist/routes/connection-authorities.d.ts +3 -0
  52. package/dist/routes/files.d.ts +4 -0
  53. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  54. package/dist/routes/personal-github.d.ts +3 -0
  55. package/dist/routes/pr-review.d.ts +3 -0
  56. package/dist/routes/sessions.d.ts +1 -0
  57. package/dist/routes/user-resource-authorities.d.ts +3 -0
  58. package/dist/routes/workspace-capture.d.ts +10 -2
  59. package/dist/routes/workspace-learning.d.ts +3 -0
  60. package/dist/routes/workspaces.d.ts +19 -0
  61. package/dist/sandbox/channel-a.d.ts +11 -1
  62. package/dist/sandbox/connection-authority.d.ts +3 -0
  63. package/dist/sandbox/enrollment.d.ts +2 -0
  64. package/dist/sandbox/machines.d.ts +2 -2
  65. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  66. package/dist/sandbox/viewer.d.ts +24 -1
  67. package/dist/sandbox-file-artifacts.d.ts +11 -0
  68. package/dist/scheduled-task-deletion.d.ts +14 -0
  69. package/dist/slack-reaction-files.d.ts +1 -1
  70. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  71. package/package.json +20 -18
  72. package/src/api-websocket.ts +23 -0
  73. package/src/app.ts +160 -27
  74. package/src/auth/managed-auth.ts +25 -3
  75. package/src/codemode.ts +53 -23
  76. package/src/codex-realtime.ts +8 -2
  77. package/src/company-brain-okf.ts +340 -0
  78. package/src/connected-machine-computer-access.ts +33 -0
  79. package/src/connection-authority-owner.ts +61 -0
  80. package/src/connection-ownership.ts +180 -0
  81. package/src/controller-data-plane.ts +47 -0
  82. package/src/editable-artifact-native-kernel.ts +26 -15
  83. package/src/editable-artifact-office-import.ts +77 -9
  84. package/src/editable-artifact-production.ts +13 -7
  85. package/src/editable-artifact-websocket.ts +11 -18
  86. package/src/editable-artifact-workspace-files.ts +31 -6
  87. package/src/http/api-error.ts +28 -0
  88. package/src/http/auth.ts +4 -0
  89. package/src/http/cors.ts +35 -0
  90. package/src/http/interaction-control-error.ts +164 -0
  91. package/src/http/sse.ts +112 -66
  92. package/src/index.ts +46 -6
  93. package/src/integrations/atlassian.ts +146 -35
  94. package/src/integrations/fiken.ts +102 -22
  95. package/src/integrations/google-drive.ts +416 -101
  96. package/src/integrations/oauth-client.ts +272 -143
  97. package/src/integrations/oauth-profiles.ts +477 -0
  98. package/src/integrations/personal-github-repositories.ts +445 -0
  99. package/src/integrations/personal-github.ts +705 -0
  100. package/src/integrations/pr-review-provider.ts +246 -0
  101. package/src/integrations/provider-oauth.ts +121 -5
  102. package/src/integrations/slack-app-home.ts +300 -0
  103. package/src/integrations/slack-bot.ts +337 -57
  104. package/src/integrations/slack-interactions.ts +1274 -214
  105. package/src/integrations/slack-routing.ts +324 -0
  106. package/src/integrations/social-oauth.ts +25 -0
  107. package/src/interaction-frame-proxy.ts +409 -0
  108. package/src/mcp/company-brain-governed-writes.ts +262 -0
  109. package/src/mcp/company-profile-agent-admin.ts +205 -0
  110. package/src/mcp/documents.ts +37 -6
  111. package/src/mcp/files.ts +22 -2
  112. package/src/mcp/remember.ts +181 -0
  113. package/src/mcp/request-abort.ts +44 -0
  114. package/src/mcp/scheduled-task-view.ts +1 -0
  115. package/src/mcp/server.ts +719 -369
  116. package/src/mcp/session-view.ts +54 -0
  117. package/src/mcp/session-wait.ts +554 -0
  118. package/src/model-catalog.ts +20 -12
  119. package/src/routes/api-integrations.ts +4 -0
  120. package/src/routes/api-keys.ts +1 -0
  121. package/src/routes/automations.ts +534 -0
  122. package/src/routes/billing.ts +57 -1
  123. package/src/routes/browser-sessions.ts +388 -98
  124. package/src/routes/channels.ts +17 -1
  125. package/src/routes/codex.ts +50 -0
  126. package/src/routes/company-brain.ts +367 -0
  127. package/src/routes/company-profile.ts +1 -1
  128. package/src/routes/computer-sessions.ts +320 -93
  129. package/src/routes/connection-authorities.ts +139 -0
  130. package/src/routes/connections.ts +145 -34
  131. package/src/routes/documents.ts +221 -3
  132. package/src/routes/editable-artifacts.ts +11 -3
  133. package/src/routes/enrollments.ts +116 -26
  134. package/src/routes/environments.ts +127 -55
  135. package/src/routes/files.ts +152 -27
  136. package/src/routes/install.ts +82 -28
  137. package/src/routes/integration-facets.ts +29 -0
  138. package/src/routes/interaction-resources.ts +20 -2
  139. package/src/routes/machines.ts +275 -13
  140. package/src/routes/organization-memberships.ts +717 -28
  141. package/src/routes/packs.ts +4 -3
  142. package/src/routes/personal-github-git-broker.ts +785 -0
  143. package/src/routes/personal-github.ts +319 -0
  144. package/src/routes/pr-review.ts +531 -0
  145. package/src/routes/rigs.ts +104 -39
  146. package/src/routes/scheduled-tasks.ts +19 -21
  147. package/src/routes/sessions.ts +777 -40
  148. package/src/routes/social.ts +14 -2
  149. package/src/routes/supergrok.ts +23 -4
  150. package/src/routes/transcription-recordings.ts +8 -2
  151. package/src/routes/user-resource-authorities.ts +138 -0
  152. package/src/routes/workspace-artifacts.ts +4 -1
  153. package/src/routes/workspace-capture.ts +21 -0
  154. package/src/routes/workspace-learning.ts +228 -0
  155. package/src/routes/workspaces.ts +59 -6
  156. package/src/sandbox/auth-callout.ts +38 -12
  157. package/src/sandbox/channel-a.ts +242 -19
  158. package/src/sandbox/connection-authority.ts +3 -0
  159. package/src/sandbox/enrollment.ts +15 -3
  160. package/src/sandbox/machines.ts +186 -64
  161. package/src/sandbox/metrics-ingestion.ts +220 -58
  162. package/src/sandbox/viewer.ts +243 -19
  163. package/src/sandbox-file-artifacts.ts +329 -0
  164. package/src/scheduled-task-deletion.ts +127 -0
  165. package/src/slack-reaction-files.ts +16 -5
  166. package/src/temporal-schedule-cleanup.ts +13 -0
  167. package/dist/chunk-2PQMSRCB.js.map +0 -1
@@ -1,13 +1,22 @@
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 {
9
14
  GOOGLE_DRIVE_CREDENTIAL_LABEL,
10
15
  GOOGLE_DRIVE_CREDENTIAL_ROLE,
16
+ GOOGLE_DRIVE_FILE_SCOPE,
17
+ GOOGLE_DRIVE_PUBLICATION_CREATE_ACTION,
18
+ GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
19
+ GOOGLE_DRIVE_PUBLICATION_TOOL_NAME,
11
20
  GOOGLE_DRIVE_PROVIDER_DOMAIN,
12
21
  GOOGLE_DRIVE_READONLY_SCOPE,
13
22
  GoogleDriveKnowledgeSourceConfig,
@@ -16,6 +25,7 @@ import {
16
25
  GoogleDriveConnectionLifecycle,
17
26
  GoogleDriveConnectionMetadata,
18
27
  GoogleDriveOAuthStartResponse,
28
+ GoogleDriveOutputDestination,
19
29
  SaveGoogleDriveIntegrationSourceRequest,
20
30
  SaveGoogleDriveSourceRequest,
21
31
  googleDriveOAuthScopeDecision,
@@ -46,6 +56,7 @@ import type { ApiRouteDeps } from "@opengeni/core";
46
56
  import {
47
57
  buildConnectionTokenResolver,
48
58
  configureIntegrationFacet,
59
+ integrationFacetConfigureRequestDigest,
49
60
  appendKnowledgeSourceAclVersion,
50
61
  deauthorizeKnowledgeSourceRetrieval,
51
62
  ConnectionDisconnectGenerationError,
@@ -55,6 +66,7 @@ import {
55
66
  decryptEnvironmentValue,
56
67
  disconnectConnectionIdempotently,
57
68
  encryptEnvironmentValue,
69
+ ensureConnectorActionPolicyDefault,
58
70
  getConnectionMetadata,
59
71
  getKnowledgeSourceByExternalIdentityForSyncAuthority,
60
72
  getKnowledgeSourceForSyncAuthority,
@@ -62,11 +74,13 @@ import {
62
74
  listKnowledgeSourceSyncTasksForConnection,
63
75
  loadConnectionCredentialForBroker,
64
76
  listIntegrationInstanceFacets,
77
+ replayCompletedIntegrationFacetOperation,
65
78
  transitionConnectionState,
66
79
  updateConnection,
67
80
  updateScheduledTask,
68
81
  upsertKnowledgeProvider,
69
82
  upsertKnowledgeSource,
83
+ withWorkspaceSubjectRls,
70
84
  recordKnowledgeLifecycleEvent,
71
85
  type PermanentConnectionRefreshFailure,
72
86
  } from "@opengeni/db";
@@ -74,6 +88,12 @@ import { googleDriveKnowledgeSourceIdentity } from "@opengeni/documents/google-d
74
88
  import { createSignedState, readSignedState } from "@opengeni/github";
75
89
  import { readResponseJsonBounded, type FetchLike } from "@opengeni/network";
76
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";
77
97
  import {
78
98
  integrationBaseUrl,
79
99
  oauthStateTtlMs,
@@ -88,6 +108,17 @@ const GOOGLE_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
88
108
  const GOOGLE_REQUEST_TIMEOUT_MS = 10_000;
89
109
  const GOOGLE_DRIVE_PAGE_SIZE = 100;
90
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";
91
122
  const GOOGLE_DRIVE_RECONSENT_ERROR_CODES = new Set([
92
123
  "appNotAuthorizedToFile",
93
124
  "authError",
@@ -99,8 +130,11 @@ type GoogleDriveOAuthState = {
99
130
  accountId: string;
100
131
  workspaceId: string;
101
132
  subjectId: string;
133
+ /** Signed proof that a live managed human started this personal-only flow. */
134
+ personalOwnerVerified: boolean;
102
135
  returnPath: string;
103
136
  encryptedPkceVerifier: string;
137
+ capability: "source_read" | "publish";
104
138
  connectionId?: string;
105
139
  connectionVersion?: number;
106
140
  nonce: string;
@@ -219,30 +253,51 @@ export async function startGoogleDriveOAuth(
219
253
  if (input.payload.connectionId && !existing) {
220
254
  throw new HTTPException(404, { message: "Google Drive connection not found" });
221
255
  }
256
+ if (input.payload.capability === "publish" && !existing) {
257
+ throw new HTTPException(409, {
258
+ message: "Connect Google Drive for source access before enabling publishing",
259
+ });
260
+ }
222
261
  if (existing) {
223
262
  requireGoogleDriveConnection(existing, input.subjectId);
224
263
  }
225
264
 
226
265
  const key = requireEnvironmentEncryption(deps.settings);
227
266
  const verifier = randomBytes(48).toString("base64url");
228
- const baseUrl = integrationBaseUrl(deps.settings.publicBaseUrl, input.requestUrl);
229
- const redirectUri = `${baseUrl}/v1/integrations/google-drive/callback`;
267
+ const redirectUri = requireGoogleDriveOAuthCallbackUrl(deps.settings);
230
268
  const state = createSignedState(requireIntegrationsStateSecret(deps.settings), {
231
269
  accountId: input.accountId,
232
270
  workspaceId: input.workspaceId,
233
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,
234
276
  returnPath: GOOGLE_DRIVE_RETURN_PATH(input.workspaceId),
235
277
  encryptedPkceVerifier: encryptEnvironmentValue(key, verifier),
278
+ capability: input.payload.capability,
236
279
  ...(existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}),
237
280
  });
238
281
  const authorizationUrl = new URL(GOOGLE_AUTHORIZATION_URL);
239
282
  authorizationUrl.searchParams.set("client_id", google.clientId);
240
283
  authorizationUrl.searchParams.set("redirect_uri", redirectUri);
241
284
  authorizationUrl.searchParams.set("response_type", "code");
242
- authorizationUrl.searchParams.set("scope", GOOGLE_DRIVE_READONLY_SCOPE);
285
+ authorizationUrl.searchParams.set(
286
+ "scope",
287
+ input.payload.capability === "publish" ? GOOGLE_DRIVE_FILE_SCOPE : GOOGLE_DRIVE_READONLY_SCOPE,
288
+ );
243
289
  authorizationUrl.searchParams.set("access_type", "offline");
244
290
  authorizationUrl.searchParams.set("include_granted_scopes", "true");
245
- authorizationUrl.searchParams.set("prompt", "consent select_account");
291
+ authorizationUrl.searchParams.set(
292
+ "prompt",
293
+ input.payload.capability === "publish" ? "consent" : "consent select_account",
294
+ );
295
+ if (input.payload.capability === "publish") {
296
+ authorizationUrl.searchParams.set("trigger_onepick", "true");
297
+ authorizationUrl.searchParams.set("allow_folder_selection", "true");
298
+ authorizationUrl.searchParams.set("allow_multiple", "false");
299
+ authorizationUrl.searchParams.set("mimetypes", GOOGLE_DRIVE_FOLDER_MIME_TYPE);
300
+ }
246
301
  authorizationUrl.searchParams.set("state", state);
247
302
  authorizationUrl.searchParams.set("code_challenge_method", "S256");
248
303
  authorizationUrl.searchParams.set(
@@ -261,9 +316,11 @@ export async function completeGoogleDriveOAuthCallback(
261
316
  code?: string | undefined;
262
317
  state?: string | undefined;
263
318
  error?: string | undefined;
319
+ pickedFileIds?: string | undefined;
264
320
  requestUrl: string;
265
321
  },
266
322
  ): Promise<{ redirectTo: string }> {
323
+ const redirectUri = requireGoogleDriveOAuthCallbackUrl(deps.settings);
267
324
  const baseUrl = integrationBaseUrl(deps.settings.publicBaseUrl, input.requestUrl);
268
325
  const returnBaseUrl = deps.settings.webBaseUrl?.replace(/\/+$/, "") ?? baseUrl;
269
326
  let state: GoogleDriveOAuthState | null = null;
@@ -291,7 +348,6 @@ export async function completeGoogleDriveOAuthCallback(
291
348
  const google = requireGoogleDriveSettings(deps.settings);
292
349
  const key = requireEnvironmentEncryption(deps.settings);
293
350
  const verifier = decryptEnvironmentValue(key, state.encryptedPkceVerifier);
294
- const redirectUri = `${baseUrl}/v1/integrations/google-drive/callback`;
295
351
  const fetchImpl = deps.googleDriveFetch ?? fetch;
296
352
  const token = await exchangeGoogleAuthorizationCode(
297
353
  {
@@ -303,10 +359,15 @@ export async function completeGoogleDriveOAuthCallback(
303
359
  },
304
360
  fetchImpl,
305
361
  );
306
- const scopeDecision = googleDriveOAuthScopeDecision(token.scopes);
362
+ const grantedScopes = [...new Set(token.scopes)].sort();
363
+ const scopeDecision = googleDriveOAuthScopeDecision(grantedScopes);
364
+ const requiredCapability =
365
+ state.capability === "publish" ? "publish_file" : "recursive_source_sync";
307
366
  if (
308
- scopeDecision.accessMode !== "readonly" ||
309
- !scopeDecision.capabilities.includes("recursive_source_sync")
367
+ !scopeDecision.accessMode ||
368
+ !scopeDecision.capabilities.includes(requiredCapability) ||
369
+ (state.capability === "publish" &&
370
+ !scopeDecision.capabilities.includes("recursive_source_sync"))
310
371
  ) {
311
372
  throw new GoogleDriveCallbackError("scope_not_granted");
312
373
  }
@@ -331,6 +392,27 @@ export async function completeGoogleDriveOAuthCallback(
331
392
  const previousMetadata = existing
332
393
  ? GoogleDriveConnectionMetadata.parse(existing.metadata)
333
394
  : null;
395
+ const outputDestination =
396
+ state.capability === "publish"
397
+ ? await verifyPickedGoogleDriveOutputDestination(
398
+ token.accessToken,
399
+ input.pickedFileIds,
400
+ fetchImpl,
401
+ )
402
+ : previousMetadata?.outputDestination;
403
+ if (state.capability === "publish") {
404
+ if (!existing) throw new GoogleDriveCallbackError("connection_conflict");
405
+ await ensureConnectorActionPolicyDefault(deps.db, {
406
+ accountId: state.accountId,
407
+ workspaceId: state.workspaceId,
408
+ subjectId: state.subjectId,
409
+ connectionId: existing.id,
410
+ serverId: GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
411
+ toolName: GOOGLE_DRIVE_PUBLICATION_TOOL_NAME,
412
+ actionName: GOOGLE_DRIVE_PUBLICATION_CREATE_ACTION,
413
+ policy: "ask",
414
+ });
415
+ }
334
416
  let refreshToken = token.refreshToken;
335
417
  if (!refreshToken && existing) {
336
418
  const previousCredential = await loadConnectionCredentialForBroker(deps.db, deps.settings, {
@@ -353,7 +435,7 @@ export async function completeGoogleDriveOAuthCallback(
353
435
  refresh_token: refreshToken,
354
436
  token_type: token.tokenType,
355
437
  ...(token.expiresAt ? { expires_at: token.expiresAt.toISOString() } : {}),
356
- scope: token.scopes.join(" "),
438
+ scope: grantedScopes.join(" "),
357
439
  token_endpoint: GOOGLE_TOKEN_URL,
358
440
  client_id: google.clientId,
359
441
  client_secret: google.clientSecret,
@@ -369,6 +451,7 @@ export async function completeGoogleDriveOAuthCallback(
369
451
  verifiedAt: new Date().toISOString(),
370
452
  accessMode: scopeDecision.accessMode,
371
453
  lifecycle: googleDriveLifecycle("active"),
454
+ ...(outputDestination ? { outputDestination } : {}),
372
455
  ...(previousMetadata?.documentDestination
373
456
  ? { documentDestination: previousMetadata.documentDestination }
374
457
  : {}),
@@ -389,7 +472,7 @@ export async function completeGoogleDriveOAuthCallback(
389
472
  kind: "oauth2",
390
473
  status: "active",
391
474
  credentialEncrypted,
392
- grantedScopes: token.scopes,
475
+ grantedScopes,
393
476
  expiresAt: token.expiresAt,
394
477
  metadata,
395
478
  updatedBySubjectId: state.subjectId,
@@ -401,7 +484,7 @@ export async function completeGoogleDriveOAuthCallback(
401
484
  providerDomain: GOOGLE_DRIVE_PROVIDER_DOMAIN,
402
485
  kind: "oauth2",
403
486
  credentialEncrypted,
404
- grantedScopes: token.scopes,
487
+ grantedScopes,
405
488
  expiresAt: token.expiresAt,
406
489
  metadata,
407
490
  createdBySubjectId: state.subjectId,
@@ -776,43 +859,50 @@ export async function saveGoogleDriveFacetSource(
776
859
  throw new HTTPException(400, { message: "invalid Google Drive source selection" });
777
860
  }
778
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);
779
899
  const context = await requireGoogleDriveIntegrationFacet(deps, input);
780
- const documentDestination = googleDriveDocumentDestination(input, payload);
781
- const verifiedSources = await verifyGoogleDriveSources(deps, {
900
+ googleDriveDocumentDestination(input, payload);
901
+ await verifyGoogleDriveSources(deps, {
782
902
  workspaceId: input.workspaceId,
783
903
  subjectId: input.subjectId,
784
904
  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,
905
+ sources: requestedSources,
816
906
  });
817
907
  return IntegrationFacetMutationResult.parse(
818
908
  await configureIntegrationFacet(deps.db, {
@@ -824,7 +914,7 @@ export async function saveGoogleDriveFacetSource(
824
914
  facetKey: input.facetKey,
825
915
  displayName:
826
916
  context.facet.binding?.displayName ?? `${input.instanceKey} — Google Drive content`,
827
- config,
917
+ config: requestedConfig,
828
918
  ...(payload.expectedVersion !== undefined
829
919
  ? { expectedVersion: payload.expectedVersion }
830
920
  : {}),
@@ -833,6 +923,13 @@ export async function saveGoogleDriveFacetSource(
833
923
  );
834
924
  }
835
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
+
836
933
  export async function saveGoogleDriveSource(
837
934
  deps: ApiRouteDeps,
838
935
  input: {
@@ -935,6 +1032,53 @@ export async function saveGoogleDriveSource(
935
1032
  return updated;
936
1033
  }
937
1034
 
1035
+ async function verifyPickedGoogleDriveOutputDestination(
1036
+ accessToken: string,
1037
+ rawPickedFileIds: string | undefined,
1038
+ fetchImpl: FetchLike,
1039
+ ) {
1040
+ const pickedFileIds = uniqueStrings((rawPickedFileIds ?? "").split(","));
1041
+ if (pickedFileIds.length !== 1) {
1042
+ throw new GoogleDriveCallbackError("output_folder_required");
1043
+ }
1044
+ const folderId = validDriveId(pickedFileIds[0]!, "picked_file_ids");
1045
+ const url = new URL(`${GOOGLE_DRIVE_API_BASE}/files/${encodeURIComponent(folderId)}`);
1046
+ url.searchParams.set("supportsAllDrives", "true");
1047
+ url.searchParams.set("fields", "id,name,mimeType,driveId,trashed,capabilities(canAddChildren)");
1048
+ const response = await providerFetch(fetchImpl, url, {
1049
+ headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
1050
+ });
1051
+ if (!response.ok) {
1052
+ await response.body?.cancel().catch(() => undefined);
1053
+ throw new GoogleDriveCallbackError("output_folder_unavailable");
1054
+ }
1055
+ const record = objectRecord(
1056
+ await readResponseJsonBounded<unknown>(
1057
+ response,
1058
+ GOOGLE_RESPONSE_MAX_BYTES,
1059
+ "Google Drive picked output folder",
1060
+ ),
1061
+ );
1062
+ const driveId = optionalString(record.driveId);
1063
+ const capabilities = objectRecord(record.capabilities);
1064
+ if (
1065
+ requiredString(record.id, "Google Drive folder id") !== folderId ||
1066
+ requiredString(record.mimeType, "Google Drive folder MIME type") !==
1067
+ GOOGLE_DRIVE_FOLDER_MIME_TYPE ||
1068
+ record.trashed === true ||
1069
+ capabilities.canAddChildren !== true
1070
+ ) {
1071
+ throw new GoogleDriveCallbackError("output_folder_unavailable");
1072
+ }
1073
+ return GoogleDriveOutputDestination.parse({
1074
+ folderId,
1075
+ folderName: requiredString(record.name, "Google Drive folder name"),
1076
+ driveId: driveId ?? null,
1077
+ location: driveId ? "shared_drive" : "my_drive",
1078
+ selectedAt: new Date().toISOString(),
1079
+ });
1080
+ }
1081
+
938
1082
  async function requireGoogleDriveIntegrationFacet(
939
1083
  deps: ApiRouteDeps,
940
1084
  input: {
@@ -1008,6 +1152,24 @@ function googleDriveDocumentDestination(
1008
1152
  if (destinationSelection.authorityKind === "personal" && !input.canManagePersonalDestination) {
1009
1153
  throw new HTTPException(403, { message: "personal destination requires the exact actor" });
1010
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
+ };
1011
1173
  return bindConnectorDocumentDestination(destinationSelection, {
1012
1174
  accountId: input.accountId,
1013
1175
  workspaceId: input.workspaceId,
@@ -1015,6 +1177,51 @@ function googleDriveDocumentDestination(
1015
1177
  });
1016
1178
  }
1017
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
+
1018
1225
  async function verifyGoogleDriveSources(
1019
1226
  deps: ApiRouteDeps,
1020
1227
  input: {
@@ -1182,13 +1389,8 @@ async function materializeGoogleDriveKnowledgeSchedules(
1182
1389
  kind: input.connection.kind,
1183
1390
  },
1184
1391
  limits: {
1185
- maxItems: 500,
1186
- maxBytes: 500_000_000,
1187
- maxFileBytes: 100_000_000,
1188
- maxProviderRequests: 1_000,
1189
- maxElapsedSeconds: 300,
1392
+ ...configuredGoogleDriveSyncLimits(deps.settings),
1190
1393
  maxConcurrency: 4,
1191
- maxFailureDetails: 25,
1192
1394
  },
1193
1395
  };
1194
1396
  const schedule = googleDriveSchedule(selectedSource.syncCadence);
@@ -1233,6 +1435,7 @@ async function materializeGoogleDriveKnowledgeSchedules(
1233
1435
  action,
1234
1436
  runMode: "new_session_per_run",
1235
1437
  targetSessionId: null,
1438
+ connectionAuthorities: [],
1236
1439
  agentConfig: {
1237
1440
  prompt: "Knowledge source synchronization",
1238
1441
  resources: [],
@@ -1306,7 +1509,7 @@ async function materializeGoogleDriveKnowledgeSchedules(
1306
1509
  }
1307
1510
  }
1308
1511
 
1309
- export async function revokeKnowledgeSourceScheduleAuthorization(
1512
+ export async function preflightKnowledgeSourceScheduleAuthorization(
1310
1513
  deps: ApiRouteDeps,
1311
1514
  input: { task: ScheduledTask; subjectId: string },
1312
1515
  ): Promise<void> {
@@ -1330,65 +1533,138 @@ export async function revokeKnowledgeSourceScheduleAuthorization(
1330
1533
  if (!externalSourceId) {
1331
1534
  throw new HTTPException(409, { message: "knowledge source schedule identity is incomplete" });
1332
1535
  }
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",
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",
1342
1553
  });
1343
1554
  }
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,
1555
+ if (task.metadata.connectorKind !== "google_drive") {
1556
+ throw new HTTPException(409, {
1557
+ message: "knowledge source schedule connector cannot be durably disabled",
1369
1558
  });
1370
- if (!updated) {
1371
- throw new HTTPException(409, {
1372
- message: "knowledge source connection changed; retry schedule deletion",
1373
- });
1374
- }
1375
- revocationVersion = updated.version;
1376
1559
  }
1377
- 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,
1378
1567
  accountId: task.accountId,
1379
1568
  workspaceId: task.workspaceId,
1569
+ connectionId: task.action.connection.connectionId,
1570
+ connectionVersion: task.action.connection.connectionVersion,
1380
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,
1381
1598
  initiatingSubjectId: input.subjectId,
1382
1599
  });
1383
- if (resolved?.source.lifecycleState === "active") {
1384
- await recordKnowledgeLifecycleEvent(deps.db, {
1385
- accountId: task.accountId,
1386
- 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,
1387
1663
  targetKind: "source",
1388
1664
  targetId: resolved.source.id,
1389
1665
  eventType: "deleted",
1390
1666
  expectedGeneration: resolved.source.lifecycleGeneration,
1391
- operationId: `knowledge-schedule-delete:${task.id}:${revocationVersion}`,
1667
+ operationId: `knowledge-schedule-delete:${input.taskId}:${revocationVersion}`,
1392
1668
  reasonCode: "schedule_deleted",
1393
1669
  actor: {
1394
1670
  kind: "human",
@@ -1396,7 +1672,7 @@ export async function revokeKnowledgeSourceScheduleAuthorization(
1396
1672
  initiatingHumanSubjectId: input.subjectId,
1397
1673
  },
1398
1674
  });
1399
- }
1675
+ });
1400
1676
  }
1401
1677
 
1402
1678
  function knowledgeSourceScheduleControl(task: ScheduledTask): {
@@ -1514,6 +1790,16 @@ function requireGoogleDriveSettings(settings: Settings): {
1514
1790
  return { clientId, clientSecret };
1515
1791
  }
1516
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
+
1517
1803
  function requireGoogleDriveConnection(connection: GoogleDriveConnectionRecord, subjectId: string) {
1518
1804
  const parsed = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
1519
1805
  if (
@@ -1699,6 +1985,13 @@ function readGoogleDriveOAuthState(
1699
1985
  if (iat === undefined || now < iat || now - iat > oauthStateTtlMs / 1000) {
1700
1986
  throw new HTTPException(400, { message: "invalid or expired Google Drive OAuth state" });
1701
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
+ }
1702
1995
  const accountId = requiredString(payload.accountId, "state.accountId");
1703
1996
  const workspaceId = requiredString(payload.workspaceId, "state.workspaceId");
1704
1997
  const subjectId = requiredString(payload.subjectId, "state.subjectId");
@@ -1708,6 +2001,11 @@ function readGoogleDriveOAuthState(
1708
2001
  }
1709
2002
  const connectionId = optionalString(payload.connectionId) ?? undefined;
1710
2003
  const connectionVersion = numberValue(payload.connectionVersion);
2004
+ const capabilityValue = optionalString(payload.capability);
2005
+ const capability = capabilityValue ?? "source_read";
2006
+ if (capability !== "source_read" && capability !== "publish") {
2007
+ throw new HTTPException(400, { message: "invalid Google Drive OAuth capability" });
2008
+ }
1711
2009
  if (
1712
2010
  (connectionVersion !== undefined && !Number.isInteger(connectionVersion)) ||
1713
2011
  Boolean(connectionId) !== Boolean(connectionVersion)
@@ -1718,11 +2016,13 @@ function readGoogleDriveOAuthState(
1718
2016
  accountId,
1719
2017
  workspaceId,
1720
2018
  subjectId,
2019
+ personalOwnerVerified: personalOwnerVerifiedInState(payload),
1721
2020
  returnPath,
1722
2021
  encryptedPkceVerifier: requiredString(
1723
2022
  payload.encryptedPkceVerifier,
1724
2023
  "state.encryptedPkceVerifier",
1725
2024
  ),
2025
+ capability,
1726
2026
  ...(connectionId ? { connectionId, connectionVersion: connectionVersion! } : {}),
1727
2027
  nonce: requiredString(payload.nonce, "state.nonce"),
1728
2028
  iat,
@@ -1733,6 +2033,21 @@ async function requireGoogleDriveCallbackGrant(
1733
2033
  deps: ApiRouteDeps,
1734
2034
  state: GoogleDriveOAuthState,
1735
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
+ }
1736
2051
  const grant = await getWorkspaceGrant(deps.db, state.subjectId, state.workspaceId);
1737
2052
  if (
1738
2053
  !grant ||
@@ -1975,7 +2290,7 @@ async function googleDriveApiRequest(
1975
2290
  if (!resolved || resolved.version !== connectionVersion) {
1976
2291
  throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
1977
2292
  }
1978
- await requireGoogleDriveSourceConnection(deps, resolved, input.subjectId);
2293
+ await requireGoogleDriveApiConnection(deps, resolved, input.subjectId);
1979
2294
  };
1980
2295
  let credential = await resolve(false);
1981
2296
  if (credential.status !== "ok") {