@opengeni/api-router 0.17.0 → 0.21.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/app.js +1 -1
- package/dist/{chunk-MWBF2GXL.js → chunk-5EEC7Q6C.js} +852 -111
- package/dist/chunk-5EEC7Q6C.js.map +1 -0
- package/dist/codex-realtime.d.ts +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-drive.d.ts +16 -1
- package/dist/mcp/documents.d.ts +3 -3
- package/dist/workspace-state-projection.d.ts +29 -1
- package/package.json +11 -11
- package/src/app.ts +33 -2
- package/src/codex-realtime.ts +4 -0
- package/src/index.ts +3 -2
- package/src/integrations/google-drive.ts +385 -20
- package/src/mcp/documents.ts +49 -27
- package/src/routes/connections.ts +61 -9
- package/src/routes/documents.ts +134 -43
- package/src/routes/workspace-instruction-policies.ts +118 -0
- package/src/routes/workspace-state.ts +46 -1
- package/src/workspace-state-projection.ts +203 -4
- package/dist/chunk-MWBF2GXL.js.map +0 -1
|
@@ -7,25 +7,33 @@ import {
|
|
|
7
7
|
GOOGLE_DRIVE_READONLY_SCOPE,
|
|
8
8
|
GoogleDriveBrowseItem,
|
|
9
9
|
GoogleDriveBrowseResponse,
|
|
10
|
+
GoogleDriveConnectionLifecycle,
|
|
10
11
|
GoogleDriveConnectionMetadata,
|
|
11
12
|
GoogleDriveOAuthStartResponse,
|
|
12
13
|
SaveGoogleDriveSourceRequest,
|
|
13
14
|
googleDriveOAuthScopeDecision,
|
|
14
15
|
googleDriveScopesAllowCapability,
|
|
16
|
+
type GoogleDriveDisconnectRequest,
|
|
17
|
+
type GoogleDriveLifecycleActionRequest,
|
|
15
18
|
type GoogleDriveOAuthStartRequest,
|
|
16
19
|
} from "@opengeni/contracts/google-drive";
|
|
17
20
|
import { hasPermission, requireEnvironmentEncryption } from "@opengeni/core";
|
|
18
21
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
19
22
|
import {
|
|
20
23
|
buildConnectionTokenResolver,
|
|
24
|
+
ConnectionDisconnectGenerationError,
|
|
25
|
+
ConnectionDisconnectIdempotencyError,
|
|
21
26
|
consumeIntegrationOAuthStateNonce,
|
|
22
27
|
createConnection,
|
|
23
28
|
decryptEnvironmentValue,
|
|
29
|
+
disconnectConnectionIdempotently,
|
|
24
30
|
encryptEnvironmentValue,
|
|
25
31
|
getConnectionMetadata,
|
|
26
32
|
getWorkspaceGrant,
|
|
27
33
|
loadConnectionCredentialForBroker,
|
|
34
|
+
transitionConnectionState,
|
|
28
35
|
updateConnection,
|
|
36
|
+
type PermanentConnectionRefreshFailure,
|
|
29
37
|
} from "@opengeni/db";
|
|
30
38
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
31
39
|
import { readResponseJsonBounded, type FetchLike } from "@opengeni/network";
|
|
@@ -44,6 +52,12 @@ const GOOGLE_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
44
52
|
const GOOGLE_REQUEST_TIMEOUT_MS = 10_000;
|
|
45
53
|
const GOOGLE_DRIVE_PAGE_SIZE = 100;
|
|
46
54
|
const GOOGLE_DRIVE_RETURN_PATH = (workspaceId: string) => `/workspaces/${workspaceId}/capabilities`;
|
|
55
|
+
const GOOGLE_DRIVE_RECONSENT_ERROR_CODES = new Set([
|
|
56
|
+
"appNotAuthorizedToFile",
|
|
57
|
+
"authError",
|
|
58
|
+
"insufficientFilePermissions",
|
|
59
|
+
"insufficientPermissions",
|
|
60
|
+
]);
|
|
47
61
|
|
|
48
62
|
type GoogleDriveOAuthState = {
|
|
49
63
|
accountId: string;
|
|
@@ -65,6 +79,8 @@ type GoogleTokenResponse = {
|
|
|
65
79
|
scopes: string[];
|
|
66
80
|
};
|
|
67
81
|
|
|
82
|
+
type GoogleDriveConnectionRecord = NonNullable<Awaited<ReturnType<typeof getConnectionMetadata>>>;
|
|
83
|
+
|
|
68
84
|
export async function startGoogleDriveOAuth(
|
|
69
85
|
deps: ApiRouteDeps,
|
|
70
86
|
input: {
|
|
@@ -236,6 +252,7 @@ export async function completeGoogleDriveOAuthCallback(
|
|
|
236
252
|
googleDisplayName: identity.displayName,
|
|
237
253
|
verifiedAt: new Date().toISOString(),
|
|
238
254
|
accessMode: scopeDecision.accessMode,
|
|
255
|
+
lifecycle: googleDriveLifecycle("active"),
|
|
239
256
|
...(previousMetadata?.selectedSources
|
|
240
257
|
? { selectedSources: previousMetadata.selectedSources }
|
|
241
258
|
: previousMetadata?.selectedSource
|
|
@@ -288,6 +305,123 @@ export async function completeGoogleDriveOAuthCallback(
|
|
|
288
305
|
}
|
|
289
306
|
}
|
|
290
307
|
|
|
308
|
+
export async function transitionGoogleDriveLifecycle(
|
|
309
|
+
deps: ApiRouteDeps,
|
|
310
|
+
input: {
|
|
311
|
+
workspaceId: string;
|
|
312
|
+
subjectId: string;
|
|
313
|
+
connectionId: string;
|
|
314
|
+
payload: GoogleDriveLifecycleActionRequest;
|
|
315
|
+
},
|
|
316
|
+
) {
|
|
317
|
+
const existing = await getConnectionMetadata(
|
|
318
|
+
deps.db,
|
|
319
|
+
input.workspaceId,
|
|
320
|
+
input.connectionId,
|
|
321
|
+
input.subjectId,
|
|
322
|
+
);
|
|
323
|
+
if (!existing) {
|
|
324
|
+
throw new HTTPException(404, { message: "Google Drive connection not found" });
|
|
325
|
+
}
|
|
326
|
+
const metadata = requireGoogleDriveConnection(existing, input.subjectId);
|
|
327
|
+
const lifecycle = effectiveGoogleDriveLifecycle(existing, metadata);
|
|
328
|
+
const targetState = input.payload.action === "pause" ? "paused" : "active";
|
|
329
|
+
|
|
330
|
+
// Natural convergence makes retried pause/resume requests idempotent even if
|
|
331
|
+
// the caller still carries the pre-transition version.
|
|
332
|
+
if (existing.status === "active" && lifecycle.state === targetState) {
|
|
333
|
+
return existing;
|
|
334
|
+
}
|
|
335
|
+
if (existing.status === "revoked") {
|
|
336
|
+
throw new HTTPException(409, {
|
|
337
|
+
message: "Google Drive is disconnected; connect it again instead",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
if (input.payload.action === "pause" && lifecycle.state !== "active") {
|
|
341
|
+
throw new HTTPException(409, {
|
|
342
|
+
message: "Google Drive must be reconnected before it can be paused",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (input.payload.action === "resume" && lifecycle.state !== "paused") {
|
|
346
|
+
throw new HTTPException(409, {
|
|
347
|
+
message: "Google Drive must be reconnected or re-consented before it can resume",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (existing.status !== "active" || existing.version !== input.payload.expectedVersion) {
|
|
351
|
+
throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const updated = await transitionConnectionState(deps.db, {
|
|
355
|
+
workspaceId: input.workspaceId,
|
|
356
|
+
connectionId: existing.id,
|
|
357
|
+
visibleToSubjectId: input.subjectId,
|
|
358
|
+
expectedVersion: existing.version,
|
|
359
|
+
status: "active",
|
|
360
|
+
metadata: GoogleDriveConnectionMetadata.parse({
|
|
361
|
+
...metadata,
|
|
362
|
+
lifecycle: googleDriveLifecycle(targetState),
|
|
363
|
+
}),
|
|
364
|
+
lastError: null,
|
|
365
|
+
updatedBySubjectId: input.subjectId,
|
|
366
|
+
});
|
|
367
|
+
if (!updated) {
|
|
368
|
+
const converged = await getConnectionMetadata(
|
|
369
|
+
deps.db,
|
|
370
|
+
input.workspaceId,
|
|
371
|
+
input.connectionId,
|
|
372
|
+
input.subjectId,
|
|
373
|
+
);
|
|
374
|
+
if (converged?.status === "active") {
|
|
375
|
+
const convergedMetadata = requireGoogleDriveConnection(converged, input.subjectId);
|
|
376
|
+
if (effectiveGoogleDriveLifecycle(converged, convergedMetadata).state === targetState) {
|
|
377
|
+
return converged;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
throw new HTTPException(409, { message: "Google Drive connection changed; try again" });
|
|
381
|
+
}
|
|
382
|
+
return updated;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export async function disconnectGoogleDrive(
|
|
386
|
+
deps: ApiRouteDeps,
|
|
387
|
+
input: {
|
|
388
|
+
workspaceId: string;
|
|
389
|
+
subjectId: string;
|
|
390
|
+
connection: GoogleDriveConnectionRecord;
|
|
391
|
+
payload: GoogleDriveDisconnectRequest;
|
|
392
|
+
},
|
|
393
|
+
) {
|
|
394
|
+
const metadata = requireGoogleDriveConnection(input.connection, input.subjectId);
|
|
395
|
+
try {
|
|
396
|
+
return await disconnectConnectionIdempotently(deps.db, {
|
|
397
|
+
accountId: input.connection.accountId,
|
|
398
|
+
workspaceId: input.workspaceId,
|
|
399
|
+
subjectId: input.subjectId,
|
|
400
|
+
connectionId: input.connection.id,
|
|
401
|
+
expectedVersion: input.payload.expectedVersion,
|
|
402
|
+
idempotencyKey: input.payload.idempotencyKey,
|
|
403
|
+
metadata: GoogleDriveConnectionMetadata.parse({
|
|
404
|
+
...metadata,
|
|
405
|
+
lifecycle: googleDriveLifecycle("disconnected"),
|
|
406
|
+
}),
|
|
407
|
+
lastError: null,
|
|
408
|
+
updatedBySubjectId: input.subjectId,
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (error instanceof ConnectionDisconnectIdempotencyError) {
|
|
412
|
+
throw new HTTPException(409, {
|
|
413
|
+
message: "Google Drive disconnect key was already used for another operation",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (error instanceof ConnectionDisconnectGenerationError) {
|
|
417
|
+
throw new HTTPException(409, {
|
|
418
|
+
message: "Google Drive connection changed; refresh before disconnecting",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
291
425
|
export async function browseGoogleDrive(
|
|
292
426
|
deps: ApiRouteDeps,
|
|
293
427
|
input: {
|
|
@@ -307,7 +441,7 @@ export async function browseGoogleDrive(
|
|
|
307
441
|
if (!connection) {
|
|
308
442
|
throw new HTTPException(404, { message: "Google Drive connection not found" });
|
|
309
443
|
}
|
|
310
|
-
requireGoogleDriveSourceConnection(connection, input.subjectId);
|
|
444
|
+
await requireGoogleDriveSourceConnection(deps, connection, input.subjectId);
|
|
311
445
|
const parentId = validDriveId(input.parentId, "parentId");
|
|
312
446
|
const currentItem = await resolveGoogleDriveBoundaryItem(deps, {
|
|
313
447
|
workspaceId: input.workspaceId,
|
|
@@ -382,7 +516,7 @@ export async function saveGoogleDriveSource(
|
|
|
382
516
|
if (!existing) {
|
|
383
517
|
throw new HTTPException(404, { message: "Google Drive connection not found" });
|
|
384
518
|
}
|
|
385
|
-
requireGoogleDriveSourceConnection(existing, input.subjectId);
|
|
519
|
+
await requireGoogleDriveSourceConnection(deps, existing, input.subjectId);
|
|
386
520
|
const verifiedSources = [];
|
|
387
521
|
for (const source of payload.sources) {
|
|
388
522
|
const sourceId = validDriveId(source.id, "source.id");
|
|
@@ -410,8 +544,8 @@ export async function saveGoogleDriveSource(
|
|
|
410
544
|
input.connectionId,
|
|
411
545
|
input.subjectId,
|
|
412
546
|
)) ?? existing;
|
|
413
|
-
const latestMetadata = requireGoogleDriveSourceConnection(latest, input.subjectId);
|
|
414
|
-
const updated = await
|
|
547
|
+
const latestMetadata = await requireGoogleDriveSourceConnection(deps, latest, input.subjectId);
|
|
548
|
+
const updated = await transitionConnectionState(deps.db, {
|
|
415
549
|
workspaceId: input.workspaceId,
|
|
416
550
|
connectionId: latest.id,
|
|
417
551
|
visibleToSubjectId: input.subjectId,
|
|
@@ -532,16 +666,7 @@ function requireGoogleDriveSettings(settings: Settings): {
|
|
|
532
666
|
return { clientId, clientSecret };
|
|
533
667
|
}
|
|
534
668
|
|
|
535
|
-
function requireGoogleDriveConnection(
|
|
536
|
-
connection: {
|
|
537
|
-
subjectId: string | null;
|
|
538
|
-
providerDomain: string;
|
|
539
|
-
kind: string;
|
|
540
|
-
grantedScopes: string[];
|
|
541
|
-
metadata: Record<string, unknown>;
|
|
542
|
-
},
|
|
543
|
-
subjectId: string,
|
|
544
|
-
) {
|
|
669
|
+
function requireGoogleDriveConnection(connection: GoogleDriveConnectionRecord, subjectId: string) {
|
|
545
670
|
const parsed = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
|
|
546
671
|
if (
|
|
547
672
|
connection.subjectId !== subjectId ||
|
|
@@ -554,14 +679,88 @@ function requireGoogleDriveConnection(
|
|
|
554
679
|
return parsed.data;
|
|
555
680
|
}
|
|
556
681
|
|
|
557
|
-
function
|
|
558
|
-
|
|
682
|
+
function googleDriveLifecycle(
|
|
683
|
+
state: GoogleDriveConnectionLifecycle["state"],
|
|
684
|
+
): GoogleDriveConnectionLifecycle {
|
|
685
|
+
return GoogleDriveConnectionLifecycle.parse({
|
|
686
|
+
state,
|
|
687
|
+
recoverable: state !== "app_removed",
|
|
688
|
+
observedAt: new Date().toISOString(),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function effectiveGoogleDriveLifecycle(
|
|
693
|
+
connection: GoogleDriveConnectionRecord,
|
|
694
|
+
metadata: ReturnType<typeof requireGoogleDriveConnection>,
|
|
695
|
+
): GoogleDriveConnectionLifecycle {
|
|
696
|
+
if (metadata.lifecycle) return metadata.lifecycle;
|
|
697
|
+
if (connection.status === "revoked") return googleDriveLifecycle("disconnected");
|
|
698
|
+
if (connection.status === "active") return googleDriveLifecycle("active");
|
|
699
|
+
return googleDriveLifecycle("reconnect_required");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function transitionGoogleDriveConnectionLifecycle(
|
|
703
|
+
deps: ApiRouteDeps,
|
|
704
|
+
connection: GoogleDriveConnectionRecord,
|
|
559
705
|
subjectId: string,
|
|
706
|
+
lifecycle: GoogleDriveConnectionLifecycle,
|
|
707
|
+
status: "active" | "needs_reauth" | "error",
|
|
708
|
+
lastError: string | null,
|
|
560
709
|
) {
|
|
561
710
|
const metadata = requireGoogleDriveConnection(connection, subjectId);
|
|
711
|
+
if (
|
|
712
|
+
connection.status === status &&
|
|
713
|
+
metadata.lifecycle?.state === lifecycle.state &&
|
|
714
|
+
metadata.lifecycle.recoverable === lifecycle.recoverable
|
|
715
|
+
) {
|
|
716
|
+
return connection;
|
|
717
|
+
}
|
|
718
|
+
return await transitionConnectionState(deps.db, {
|
|
719
|
+
workspaceId: connection.workspaceId,
|
|
720
|
+
connectionId: connection.id,
|
|
721
|
+
visibleToSubjectId: subjectId,
|
|
722
|
+
expectedVersion: connection.version,
|
|
723
|
+
status,
|
|
724
|
+
metadata: GoogleDriveConnectionMetadata.parse({ ...metadata, lifecycle }),
|
|
725
|
+
lastError,
|
|
726
|
+
updatedBySubjectId: subjectId,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function requireGoogleDriveSourceConnection(
|
|
731
|
+
deps: ApiRouteDeps,
|
|
732
|
+
connection: GoogleDriveConnectionRecord,
|
|
733
|
+
subjectId: string,
|
|
734
|
+
) {
|
|
735
|
+
const metadata = requireGoogleDriveConnection(connection, subjectId);
|
|
736
|
+
const lifecycle = effectiveGoogleDriveLifecycle(connection, metadata);
|
|
737
|
+
if (connection.status === "revoked") {
|
|
738
|
+
throw new HTTPException(409, { message: "Google Drive is disconnected" });
|
|
739
|
+
}
|
|
740
|
+
if (lifecycle.state === "paused") {
|
|
741
|
+
throw new HTTPException(409, { message: "Google Drive is paused" });
|
|
742
|
+
}
|
|
743
|
+
if (connection.status !== "active" || lifecycle.state !== "active") {
|
|
744
|
+
throw new HTTPException(401, {
|
|
745
|
+
message:
|
|
746
|
+
lifecycle.state === "reconsent_required"
|
|
747
|
+
? "Google Drive needs permission re-consent"
|
|
748
|
+
: lifecycle.state === "app_removed"
|
|
749
|
+
? "Google Drive app access is unavailable"
|
|
750
|
+
: "Google Drive needs to be reconnected",
|
|
751
|
+
});
|
|
752
|
+
}
|
|
562
753
|
if (!googleDriveScopesAllowCapability(connection.grantedScopes, "recursive_source_sync")) {
|
|
754
|
+
await transitionGoogleDriveConnectionLifecycle(
|
|
755
|
+
deps,
|
|
756
|
+
connection,
|
|
757
|
+
subjectId,
|
|
758
|
+
googleDriveLifecycle("reconsent_required"),
|
|
759
|
+
"needs_reauth",
|
|
760
|
+
"google_drive_reconsent_required",
|
|
761
|
+
);
|
|
563
762
|
throw new HTTPException(401, {
|
|
564
|
-
message: "Google Drive needs
|
|
763
|
+
message: "Google Drive needs permission re-consent for selected-source read access",
|
|
565
764
|
});
|
|
566
765
|
}
|
|
567
766
|
return metadata;
|
|
@@ -705,6 +904,99 @@ async function verifyGoogleDriveIdentity(accessToken: string, fetchImpl: FetchLi
|
|
|
705
904
|
};
|
|
706
905
|
}
|
|
707
906
|
|
|
907
|
+
function googleDriveRefreshFailureLifecycle(failure: PermanentConnectionRefreshFailure): {
|
|
908
|
+
lifecycle: GoogleDriveConnectionLifecycle;
|
|
909
|
+
status: "needs_reauth" | "error";
|
|
910
|
+
lastError: string;
|
|
911
|
+
} {
|
|
912
|
+
const code = failure.oauthErrorCode?.toLowerCase() ?? null;
|
|
913
|
+
if (code === "invalid_client" || code === "unauthorized_client") {
|
|
914
|
+
return {
|
|
915
|
+
lifecycle: googleDriveLifecycle("app_removed"),
|
|
916
|
+
status: "error",
|
|
917
|
+
lastError: "google_drive_app_removed",
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
if (code === "invalid_scope" || code === "insufficient_scope") {
|
|
921
|
+
return {
|
|
922
|
+
lifecycle: googleDriveLifecycle("reconsent_required"),
|
|
923
|
+
status: "needs_reauth",
|
|
924
|
+
lastError: "google_drive_reconsent_required",
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
if (code === "invalid_grant") {
|
|
928
|
+
return {
|
|
929
|
+
lifecycle: googleDriveLifecycle("token_revoked"),
|
|
930
|
+
status: "needs_reauth",
|
|
931
|
+
lastError: "google_drive_token_revoked",
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
lifecycle: googleDriveLifecycle("reconnect_required"),
|
|
936
|
+
status: "needs_reauth",
|
|
937
|
+
lastError: "google_drive_reconnect_required",
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function transitionGoogleDrivePermanentRefreshFailure(
|
|
942
|
+
deps: ApiRouteDeps,
|
|
943
|
+
failure: PermanentConnectionRefreshFailure,
|
|
944
|
+
): Promise<boolean> {
|
|
945
|
+
if (failure.providerDomain !== GOOGLE_DRIVE_PROVIDER_DOMAIN || !failure.subjectId) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
const connection = await getConnectionMetadata(
|
|
949
|
+
deps.db,
|
|
950
|
+
failure.workspaceId,
|
|
951
|
+
failure.connectionId,
|
|
952
|
+
failure.subjectId,
|
|
953
|
+
);
|
|
954
|
+
if (!connection || connection.version !== failure.connectionVersion) {
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
const transition = googleDriveRefreshFailureLifecycle(failure);
|
|
958
|
+
await transitionGoogleDriveConnectionLifecycle(
|
|
959
|
+
deps,
|
|
960
|
+
connection,
|
|
961
|
+
failure.subjectId,
|
|
962
|
+
transition.lifecycle,
|
|
963
|
+
transition.status,
|
|
964
|
+
transition.lastError,
|
|
965
|
+
);
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function transitionGoogleDriveProviderResponseFailure(
|
|
970
|
+
deps: ApiRouteDeps,
|
|
971
|
+
input: {
|
|
972
|
+
workspaceId: string;
|
|
973
|
+
subjectId: string;
|
|
974
|
+
connectionId: string;
|
|
975
|
+
connectionVersion: number;
|
|
976
|
+
lifecycle: GoogleDriveConnectionLifecycle;
|
|
977
|
+
status: "needs_reauth" | "error";
|
|
978
|
+
lastError: string;
|
|
979
|
+
},
|
|
980
|
+
): Promise<void> {
|
|
981
|
+
const latest = await getConnectionMetadata(
|
|
982
|
+
deps.db,
|
|
983
|
+
input.workspaceId,
|
|
984
|
+
input.connectionId,
|
|
985
|
+
input.subjectId,
|
|
986
|
+
);
|
|
987
|
+
if (!latest || latest.version !== input.connectionVersion || latest.status !== "active") {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
await transitionGoogleDriveConnectionLifecycle(
|
|
991
|
+
deps,
|
|
992
|
+
latest,
|
|
993
|
+
input.subjectId,
|
|
994
|
+
input.lifecycle,
|
|
995
|
+
input.status,
|
|
996
|
+
input.lastError,
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
708
1000
|
async function googleDriveApiRequest(
|
|
709
1001
|
deps: ApiRouteDeps,
|
|
710
1002
|
input: {
|
|
@@ -715,7 +1007,21 @@ async function googleDriveApiRequest(
|
|
|
715
1007
|
label: string;
|
|
716
1008
|
},
|
|
717
1009
|
): Promise<unknown> {
|
|
718
|
-
const
|
|
1010
|
+
const current = await getConnectionMetadata(
|
|
1011
|
+
deps.db,
|
|
1012
|
+
input.workspaceId,
|
|
1013
|
+
input.connectionId,
|
|
1014
|
+
input.subjectId,
|
|
1015
|
+
);
|
|
1016
|
+
if (!current) {
|
|
1017
|
+
throw new HTTPException(404, { message: "Google Drive connection not found" });
|
|
1018
|
+
}
|
|
1019
|
+
await requireGoogleDriveSourceConnection(deps, current, input.subjectId);
|
|
1020
|
+
const resolver = buildConnectionTokenResolver(deps.db, deps.settings, undefined, {
|
|
1021
|
+
...(deps.googleDriveFetch ? { refreshTransport: { fetchImpl: deps.googleDriveFetch } } : {}),
|
|
1022
|
+
transitionPermanentRefreshFailure: async (failure) =>
|
|
1023
|
+
await transitionGoogleDrivePermanentRefreshFailure(deps, failure),
|
|
1024
|
+
});
|
|
719
1025
|
const resolve = async (forceRefresh: boolean) =>
|
|
720
1026
|
await resolver({
|
|
721
1027
|
workspaceId: input.workspaceId,
|
|
@@ -735,6 +1041,10 @@ async function googleDriveApiRequest(
|
|
|
735
1041
|
if (credential.status !== "ok") {
|
|
736
1042
|
throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
|
|
737
1043
|
}
|
|
1044
|
+
let providerConnectionVersion = credential.connectionVersion;
|
|
1045
|
+
if (providerConnectionVersion === undefined) {
|
|
1046
|
+
throw new Error("Google Drive credential resolver omitted the connection version");
|
|
1047
|
+
}
|
|
738
1048
|
const fetchImpl = deps.googleDriveFetch ?? fetch;
|
|
739
1049
|
let response = await providerFetch(fetchImpl, input.url, {
|
|
740
1050
|
headers: { ...credential.headers, accept: "application/json" },
|
|
@@ -745,16 +1055,52 @@ async function googleDriveApiRequest(
|
|
|
745
1055
|
if (credential.status !== "ok") {
|
|
746
1056
|
throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
|
|
747
1057
|
}
|
|
1058
|
+
providerConnectionVersion = credential.connectionVersion;
|
|
1059
|
+
if (providerConnectionVersion === undefined) {
|
|
1060
|
+
throw new Error("Google Drive credential resolver omitted the connection version");
|
|
1061
|
+
}
|
|
748
1062
|
response = await providerFetch(fetchImpl, input.url, {
|
|
749
1063
|
headers: { ...credential.headers, accept: "application/json" },
|
|
750
1064
|
});
|
|
751
1065
|
}
|
|
752
1066
|
if (!response.ok) {
|
|
753
|
-
|
|
1067
|
+
if (response.status === 401) {
|
|
1068
|
+
await response.body?.cancel().catch(() => undefined);
|
|
1069
|
+
await transitionGoogleDriveProviderResponseFailure(deps, {
|
|
1070
|
+
workspaceId: input.workspaceId,
|
|
1071
|
+
subjectId: input.subjectId,
|
|
1072
|
+
connectionId: input.connectionId,
|
|
1073
|
+
connectionVersion: providerConnectionVersion,
|
|
1074
|
+
lifecycle: googleDriveLifecycle("reconnect_required"),
|
|
1075
|
+
status: "needs_reauth",
|
|
1076
|
+
lastError: "google_drive_reconnect_required",
|
|
1077
|
+
});
|
|
1078
|
+
throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
|
|
1079
|
+
}
|
|
1080
|
+
const providerErrorCode =
|
|
1081
|
+
response.status === 403 ? await readGoogleDriveProviderErrorCode(response) : null;
|
|
1082
|
+
if (response.status !== 403) {
|
|
1083
|
+
await response.body?.cancel().catch(() => undefined);
|
|
1084
|
+
}
|
|
1085
|
+
if (
|
|
1086
|
+
response.status === 403 &&
|
|
1087
|
+
providerErrorCode &&
|
|
1088
|
+
GOOGLE_DRIVE_RECONSENT_ERROR_CODES.has(providerErrorCode)
|
|
1089
|
+
) {
|
|
1090
|
+
await transitionGoogleDriveProviderResponseFailure(deps, {
|
|
1091
|
+
workspaceId: input.workspaceId,
|
|
1092
|
+
subjectId: input.subjectId,
|
|
1093
|
+
connectionId: input.connectionId,
|
|
1094
|
+
connectionVersion: providerConnectionVersion,
|
|
1095
|
+
lifecycle: googleDriveLifecycle("reconsent_required"),
|
|
1096
|
+
status: "needs_reauth",
|
|
1097
|
+
lastError: "google_drive_reconsent_required",
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
754
1100
|
throw new HTTPException(response.status === 403 ? 403 : 502, {
|
|
755
1101
|
message:
|
|
756
1102
|
response.status === 403
|
|
757
|
-
? "Google Drive denied metadata access;
|
|
1103
|
+
? "Google Drive denied metadata access; re-consent may be required"
|
|
758
1104
|
: "Google Drive metadata request failed",
|
|
759
1105
|
});
|
|
760
1106
|
}
|
|
@@ -777,6 +1123,25 @@ async function providerFetch(
|
|
|
777
1123
|
}
|
|
778
1124
|
}
|
|
779
1125
|
|
|
1126
|
+
async function readGoogleDriveProviderErrorCode(response: Response): Promise<string | null> {
|
|
1127
|
+
try {
|
|
1128
|
+
const payload = objectRecord(
|
|
1129
|
+
await readResponseJsonBounded<unknown>(
|
|
1130
|
+
response,
|
|
1131
|
+
GOOGLE_RESPONSE_MAX_BYTES,
|
|
1132
|
+
"Google Drive error response",
|
|
1133
|
+
),
|
|
1134
|
+
);
|
|
1135
|
+
const error = objectRecord(payload.error);
|
|
1136
|
+
const first = Array.isArray(error.errors) ? objectRecord(error.errors[0]) : {};
|
|
1137
|
+
const code = optionalString(first.reason) ?? optionalString(error.status);
|
|
1138
|
+
return code && /^[A-Za-z0-9_.-]{1,64}$/.test(code) ? code : null;
|
|
1139
|
+
} catch {
|
|
1140
|
+
await response.body?.cancel().catch(() => undefined);
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
780
1145
|
function parseDriveItem(value: unknown): GoogleDriveBrowseItem | null {
|
|
781
1146
|
const item = objectRecord(value);
|
|
782
1147
|
const id = optionalString(item.id);
|
package/src/mcp/documents.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { DocumentSearchResponse } from "@opengeni/contracts";
|
|
1
2
|
import {
|
|
2
3
|
getDocumentChunk,
|
|
3
4
|
listDocumentBases,
|
|
4
|
-
|
|
5
|
+
searchEffectiveDocuments,
|
|
5
6
|
type DocumentAccessFilter,
|
|
6
7
|
type DocumentServices,
|
|
7
8
|
} from "@opengeni/documents";
|
|
@@ -47,9 +48,9 @@ export function buildDocumentsMcpServer(
|
|
|
47
48
|
documentServices: DocumentServices,
|
|
48
49
|
options: {
|
|
49
50
|
createdBySessionId?: string | undefined;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
}
|
|
51
|
+
/** Immutable human subject whose agent is making this retrieval request. */
|
|
52
|
+
initiatingSubjectId: string;
|
|
53
|
+
},
|
|
53
54
|
): McpServer {
|
|
54
55
|
const server = new McpServer({
|
|
55
56
|
name: "opengeni-documents",
|
|
@@ -60,7 +61,7 @@ export function buildDocumentsMcpServer(
|
|
|
60
61
|
// documents are available only to the creating subject's agent.
|
|
61
62
|
const agentAccess: DocumentAccessFilter = {
|
|
62
63
|
agentOnly: true,
|
|
63
|
-
|
|
64
|
+
viewerSubjectId: options.initiatingSubjectId,
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
server.registerTool(
|
|
@@ -80,17 +81,35 @@ export function buildDocumentsMcpServer(
|
|
|
80
81
|
description: "Search indexed documents with hybrid, vector, or keyword retrieval.",
|
|
81
82
|
inputSchema: SearchInputSchema,
|
|
82
83
|
},
|
|
83
|
-
async (input) =>
|
|
84
|
+
async (input) =>
|
|
85
|
+
searchContent(
|
|
86
|
+
db,
|
|
87
|
+
accountId,
|
|
88
|
+
workspaceId,
|
|
89
|
+
documentServices,
|
|
90
|
+
input,
|
|
91
|
+
options.initiatingSubjectId,
|
|
92
|
+
false,
|
|
93
|
+
),
|
|
84
94
|
);
|
|
85
95
|
|
|
86
96
|
server.registerTool(
|
|
87
97
|
"knowledge_search",
|
|
88
98
|
{
|
|
89
99
|
description:
|
|
90
|
-
"Search
|
|
100
|
+
"Search the effective authorized organization, current-workspace, and immutable initiating-user personal document scope. Authorization is applied before ranking and every result retains source and authority provenance.",
|
|
91
101
|
inputSchema: SearchInputSchema,
|
|
92
102
|
},
|
|
93
|
-
async (input) =>
|
|
103
|
+
async (input) =>
|
|
104
|
+
searchContent(
|
|
105
|
+
db,
|
|
106
|
+
accountId,
|
|
107
|
+
workspaceId,
|
|
108
|
+
documentServices,
|
|
109
|
+
input,
|
|
110
|
+
options.initiatingSubjectId,
|
|
111
|
+
true,
|
|
112
|
+
),
|
|
94
113
|
);
|
|
95
114
|
|
|
96
115
|
server.registerTool(
|
|
@@ -102,7 +121,7 @@ export function buildDocumentsMcpServer(
|
|
|
102
121
|
},
|
|
103
122
|
},
|
|
104
123
|
async ({ chunkId }) => {
|
|
105
|
-
const found = await getDocumentChunk(db, workspaceId, chunkId, agentAccess);
|
|
124
|
+
const found = await getDocumentChunk(db, accountId, workspaceId, chunkId, agentAccess);
|
|
106
125
|
return {
|
|
107
126
|
content: [
|
|
108
127
|
{ type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
|
|
@@ -121,7 +140,7 @@ export function buildDocumentsMcpServer(
|
|
|
121
140
|
},
|
|
122
141
|
},
|
|
123
142
|
async ({ chunkId }) => {
|
|
124
|
-
const found = await getDocumentChunk(db, workspaceId, chunkId, agentAccess);
|
|
143
|
+
const found = await getDocumentChunk(db, accountId, workspaceId, chunkId, agentAccess);
|
|
125
144
|
return {
|
|
126
145
|
content: [
|
|
127
146
|
{ type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
|
|
@@ -205,6 +224,7 @@ export function buildDocumentsMcpServer(
|
|
|
205
224
|
|
|
206
225
|
async function searchContent(
|
|
207
226
|
db: Database,
|
|
227
|
+
accountId: string,
|
|
208
228
|
workspaceId: string,
|
|
209
229
|
documentServices: DocumentServices,
|
|
210
230
|
input: {
|
|
@@ -226,28 +246,30 @@ async function searchContent(
|
|
|
226
246
|
| undefined;
|
|
227
247
|
aclTags?: string[] | undefined;
|
|
228
248
|
},
|
|
229
|
-
|
|
249
|
+
initiatingSubjectId: string,
|
|
250
|
+
wrapResponse: boolean,
|
|
230
251
|
) {
|
|
252
|
+
const results = await searchEffectiveDocuments(
|
|
253
|
+
db,
|
|
254
|
+
{
|
|
255
|
+
accountId,
|
|
256
|
+
workspaceId,
|
|
257
|
+
query: input.query,
|
|
258
|
+
...(input.baseIds ? { baseIds: input.baseIds } : {}),
|
|
259
|
+
...(input.limit ? { limit: input.limit } : {}),
|
|
260
|
+
...(input.mode ? { mode: input.mode } : {}),
|
|
261
|
+
...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
|
|
262
|
+
...(input.aclTags ? { aclTags: input.aclTags } : {}),
|
|
263
|
+
initiatingSubjectId,
|
|
264
|
+
surface: "agent",
|
|
265
|
+
},
|
|
266
|
+
documentServices,
|
|
267
|
+
);
|
|
231
268
|
return {
|
|
232
269
|
content: [
|
|
233
270
|
{
|
|
234
271
|
type: "text" as const,
|
|
235
|
-
text: JSON.stringify(
|
|
236
|
-
await searchDocuments(
|
|
237
|
-
db,
|
|
238
|
-
{
|
|
239
|
-
workspaceId,
|
|
240
|
-
query: input.query,
|
|
241
|
-
...(input.baseIds ? { baseIds: input.baseIds } : {}),
|
|
242
|
-
...(input.limit ? { limit: input.limit } : {}),
|
|
243
|
-
...(input.mode ? { mode: input.mode } : {}),
|
|
244
|
-
...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
|
|
245
|
-
...(input.aclTags ? { aclTags: input.aclTags } : {}),
|
|
246
|
-
access,
|
|
247
|
-
},
|
|
248
|
-
documentServices,
|
|
249
|
-
),
|
|
250
|
-
),
|
|
272
|
+
text: JSON.stringify(wrapResponse ? DocumentSearchResponse.parse({ results }) : results),
|
|
251
273
|
},
|
|
252
274
|
],
|
|
253
275
|
};
|