@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.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-2PQMSRCB.js → chunk-QKDFBBUE.js} +29110 -18137
- package/dist/chunk-QKDFBBUE.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-office-import.d.ts +7 -4
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +364 -86
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +19 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +149 -9
- package/dist/integrations/slack-interactions.d.ts +10 -4
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +17 -3
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +160 -27
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +77 -9
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +416 -101
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +337 -57
- package/src/integrations/slack-interactions.ts +1274 -214
- package/src/integrations/slack-routing.ts +324 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +22 -2
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +388 -98
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +145 -34
- package/src/routes/documents.ts +221 -3
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +152 -27
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +777 -40
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-2PQMSRCB.js.map +0 -1
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
309
|
-
!scopeDecision.capabilities.includes(
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
781
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
|
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 (
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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:${
|
|
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
|
|
2293
|
+
await requireGoogleDriveApiConnection(deps, resolved, input.subjectId);
|
|
1979
2294
|
};
|
|
1980
2295
|
let credential = await resolve(false);
|
|
1981
2296
|
if (credential.status !== "ok") {
|