@opengeni/api-router 0.16.5 → 0.20.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-FGNCK7HE.js → chunk-AEVD7E2F.js} +2579 -141
- package/dist/chunk-AEVD7E2F.js.map +1 -0
- package/dist/codex-realtime.d.ts +43 -0
- package/dist/gateway-realtime.d.ts +24 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-drive.d.ts +16 -1
- package/dist/integrations/slack-bot.d.ts +122 -0
- package/dist/integrations/slack-interactions.d.ts +5 -0
- package/dist/mcp/documents.d.ts +3 -3
- package/dist/mcp/server.d.ts +1 -1
- package/dist/session-realtime-context.d.ts +19 -0
- package/dist/workspace-state-projection.d.ts +29 -1
- package/package.json +12 -12
- package/src/app.ts +33 -2
- package/src/codex-realtime.ts +371 -0
- package/src/gateway-realtime.ts +143 -0
- package/src/index.ts +3 -2
- package/src/integrations/google-drive.ts +399 -24
- package/src/integrations/slack-bot.ts +536 -0
- package/src/integrations/slack-interactions.ts +467 -2
- package/src/mcp/documents.ts +49 -27
- package/src/routes/connections.ts +63 -11
- package/src/routes/documents.ts +134 -43
- package/src/routes/sessions.ts +622 -11
- package/src/routes/workspace-instruction-policies.ts +22 -0
- package/src/routes/workspace-state.ts +46 -1
- package/src/routes/workspaces.ts +56 -1
- package/src/session-realtime-context.ts +134 -0
- package/src/workspace-state-projection.ts +203 -4
- package/dist/chunk-FGNCK7HE.js.map +0 -1
|
@@ -3,28 +3,37 @@ import type { Settings } from "@opengeni/config";
|
|
|
3
3
|
import {
|
|
4
4
|
GOOGLE_DRIVE_CREDENTIAL_LABEL,
|
|
5
5
|
GOOGLE_DRIVE_CREDENTIAL_ROLE,
|
|
6
|
-
GOOGLE_DRIVE_METADATA_READONLY_SCOPE,
|
|
7
6
|
GOOGLE_DRIVE_PROVIDER_DOMAIN,
|
|
8
7
|
GOOGLE_DRIVE_READONLY_SCOPE,
|
|
9
8
|
GoogleDriveBrowseItem,
|
|
10
9
|
GoogleDriveBrowseResponse,
|
|
10
|
+
GoogleDriveConnectionLifecycle,
|
|
11
11
|
GoogleDriveConnectionMetadata,
|
|
12
12
|
GoogleDriveOAuthStartResponse,
|
|
13
13
|
SaveGoogleDriveSourceRequest,
|
|
14
|
+
googleDriveOAuthScopeDecision,
|
|
15
|
+
googleDriveScopesAllowCapability,
|
|
16
|
+
type GoogleDriveDisconnectRequest,
|
|
17
|
+
type GoogleDriveLifecycleActionRequest,
|
|
14
18
|
type GoogleDriveOAuthStartRequest,
|
|
15
19
|
} from "@opengeni/contracts/google-drive";
|
|
16
20
|
import { hasPermission, requireEnvironmentEncryption } from "@opengeni/core";
|
|
17
21
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
18
22
|
import {
|
|
19
23
|
buildConnectionTokenResolver,
|
|
24
|
+
ConnectionDisconnectGenerationError,
|
|
25
|
+
ConnectionDisconnectIdempotencyError,
|
|
20
26
|
consumeIntegrationOAuthStateNonce,
|
|
21
27
|
createConnection,
|
|
22
28
|
decryptEnvironmentValue,
|
|
29
|
+
disconnectConnectionIdempotently,
|
|
23
30
|
encryptEnvironmentValue,
|
|
24
31
|
getConnectionMetadata,
|
|
25
32
|
getWorkspaceGrant,
|
|
26
33
|
loadConnectionCredentialForBroker,
|
|
34
|
+
transitionConnectionState,
|
|
27
35
|
updateConnection,
|
|
36
|
+
type PermanentConnectionRefreshFailure,
|
|
28
37
|
} from "@opengeni/db";
|
|
29
38
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
30
39
|
import { readResponseJsonBounded, type FetchLike } from "@opengeni/network";
|
|
@@ -43,6 +52,12 @@ const GOOGLE_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
43
52
|
const GOOGLE_REQUEST_TIMEOUT_MS = 10_000;
|
|
44
53
|
const GOOGLE_DRIVE_PAGE_SIZE = 100;
|
|
45
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
|
+
]);
|
|
46
61
|
|
|
47
62
|
type GoogleDriveOAuthState = {
|
|
48
63
|
accountId: string;
|
|
@@ -64,6 +79,8 @@ type GoogleTokenResponse = {
|
|
|
64
79
|
scopes: string[];
|
|
65
80
|
};
|
|
66
81
|
|
|
82
|
+
type GoogleDriveConnectionRecord = NonNullable<Awaited<ReturnType<typeof getConnectionMetadata>>>;
|
|
83
|
+
|
|
67
84
|
export async function startGoogleDriveOAuth(
|
|
68
85
|
deps: ApiRouteDeps,
|
|
69
86
|
input: {
|
|
@@ -170,7 +187,11 @@ export async function completeGoogleDriveOAuthCallback(
|
|
|
170
187
|
},
|
|
171
188
|
fetchImpl,
|
|
172
189
|
);
|
|
173
|
-
|
|
190
|
+
const scopeDecision = googleDriveOAuthScopeDecision(token.scopes);
|
|
191
|
+
if (
|
|
192
|
+
scopeDecision.accessMode !== "readonly" ||
|
|
193
|
+
!scopeDecision.capabilities.includes("recursive_source_sync")
|
|
194
|
+
) {
|
|
174
195
|
throw new GoogleDriveCallbackError("scope_not_granted");
|
|
175
196
|
}
|
|
176
197
|
const identity = await verifyGoogleDriveIdentity(token.accessToken, fetchImpl);
|
|
@@ -230,7 +251,8 @@ export async function completeGoogleDriveOAuthCallback(
|
|
|
230
251
|
googleEmail: identity.emailAddress,
|
|
231
252
|
googleDisplayName: identity.displayName,
|
|
232
253
|
verifiedAt: new Date().toISOString(),
|
|
233
|
-
accessMode:
|
|
254
|
+
accessMode: scopeDecision.accessMode,
|
|
255
|
+
lifecycle: googleDriveLifecycle("active"),
|
|
234
256
|
...(previousMetadata?.selectedSources
|
|
235
257
|
? { selectedSources: previousMetadata.selectedSources }
|
|
236
258
|
: previousMetadata?.selectedSource
|
|
@@ -283,6 +305,123 @@ export async function completeGoogleDriveOAuthCallback(
|
|
|
283
305
|
}
|
|
284
306
|
}
|
|
285
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
|
+
|
|
286
425
|
export async function browseGoogleDrive(
|
|
287
426
|
deps: ApiRouteDeps,
|
|
288
427
|
input: {
|
|
@@ -302,7 +441,7 @@ export async function browseGoogleDrive(
|
|
|
302
441
|
if (!connection) {
|
|
303
442
|
throw new HTTPException(404, { message: "Google Drive connection not found" });
|
|
304
443
|
}
|
|
305
|
-
|
|
444
|
+
await requireGoogleDriveSourceConnection(deps, connection, input.subjectId);
|
|
306
445
|
const parentId = validDriveId(input.parentId, "parentId");
|
|
307
446
|
const currentItem = await resolveGoogleDriveBoundaryItem(deps, {
|
|
308
447
|
workspaceId: input.workspaceId,
|
|
@@ -377,7 +516,7 @@ export async function saveGoogleDriveSource(
|
|
|
377
516
|
if (!existing) {
|
|
378
517
|
throw new HTTPException(404, { message: "Google Drive connection not found" });
|
|
379
518
|
}
|
|
380
|
-
|
|
519
|
+
await requireGoogleDriveSourceConnection(deps, existing, input.subjectId);
|
|
381
520
|
const verifiedSources = [];
|
|
382
521
|
for (const source of payload.sources) {
|
|
383
522
|
const sourceId = validDriveId(source.id, "source.id");
|
|
@@ -405,8 +544,8 @@ export async function saveGoogleDriveSource(
|
|
|
405
544
|
input.connectionId,
|
|
406
545
|
input.subjectId,
|
|
407
546
|
)) ?? existing;
|
|
408
|
-
const latestMetadata =
|
|
409
|
-
const updated = await
|
|
547
|
+
const latestMetadata = await requireGoogleDriveSourceConnection(deps, latest, input.subjectId);
|
|
548
|
+
const updated = await transitionConnectionState(deps.db, {
|
|
410
549
|
workspaceId: input.workspaceId,
|
|
411
550
|
connectionId: latest.id,
|
|
412
551
|
visibleToSubjectId: input.subjectId,
|
|
@@ -527,16 +666,7 @@ function requireGoogleDriveSettings(settings: Settings): {
|
|
|
527
666
|
return { clientId, clientSecret };
|
|
528
667
|
}
|
|
529
668
|
|
|
530
|
-
function requireGoogleDriveConnection(
|
|
531
|
-
connection: {
|
|
532
|
-
subjectId: string | null;
|
|
533
|
-
providerDomain: string;
|
|
534
|
-
kind: string;
|
|
535
|
-
grantedScopes: string[];
|
|
536
|
-
metadata: Record<string, unknown>;
|
|
537
|
-
},
|
|
538
|
-
subjectId: string,
|
|
539
|
-
) {
|
|
669
|
+
function requireGoogleDriveConnection(connection: GoogleDriveConnectionRecord, subjectId: string) {
|
|
540
670
|
const parsed = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
|
|
541
671
|
if (
|
|
542
672
|
connection.subjectId !== subjectId ||
|
|
@@ -546,15 +676,94 @@ function requireGoogleDriveConnection(
|
|
|
546
676
|
) {
|
|
547
677
|
throw new HTTPException(422, { message: "connection is not this user's Google Drive" });
|
|
548
678
|
}
|
|
679
|
+
return parsed.data;
|
|
680
|
+
}
|
|
681
|
+
|
|
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,
|
|
705
|
+
subjectId: string,
|
|
706
|
+
lifecycle: GoogleDriveConnectionLifecycle,
|
|
707
|
+
status: "active" | "needs_reauth" | "error",
|
|
708
|
+
lastError: string | null,
|
|
709
|
+
) {
|
|
710
|
+
const metadata = requireGoogleDriveConnection(connection, subjectId);
|
|
549
711
|
if (
|
|
550
|
-
|
|
551
|
-
|
|
712
|
+
connection.status === status &&
|
|
713
|
+
metadata.lifecycle?.state === lifecycle.state &&
|
|
714
|
+
metadata.lifecycle.recoverable === lifecycle.recoverable
|
|
552
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") {
|
|
553
744
|
throw new HTTPException(401, {
|
|
554
|
-
message:
|
|
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",
|
|
555
751
|
});
|
|
556
752
|
}
|
|
557
|
-
|
|
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
|
+
);
|
|
762
|
+
throw new HTTPException(401, {
|
|
763
|
+
message: "Google Drive needs permission re-consent for selected-source read access",
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
return metadata;
|
|
558
767
|
}
|
|
559
768
|
|
|
560
769
|
function readGoogleDriveOAuthState(
|
|
@@ -695,6 +904,99 @@ async function verifyGoogleDriveIdentity(accessToken: string, fetchImpl: FetchLi
|
|
|
695
904
|
};
|
|
696
905
|
}
|
|
697
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
|
+
|
|
698
1000
|
async function googleDriveApiRequest(
|
|
699
1001
|
deps: ApiRouteDeps,
|
|
700
1002
|
input: {
|
|
@@ -705,7 +1007,21 @@ async function googleDriveApiRequest(
|
|
|
705
1007
|
label: string;
|
|
706
1008
|
},
|
|
707
1009
|
): Promise<unknown> {
|
|
708
|
-
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
|
+
});
|
|
709
1025
|
const resolve = async (forceRefresh: boolean) =>
|
|
710
1026
|
await resolver({
|
|
711
1027
|
workspaceId: input.workspaceId,
|
|
@@ -725,6 +1041,10 @@ async function googleDriveApiRequest(
|
|
|
725
1041
|
if (credential.status !== "ok") {
|
|
726
1042
|
throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
|
|
727
1043
|
}
|
|
1044
|
+
let providerConnectionVersion = credential.connectionVersion;
|
|
1045
|
+
if (providerConnectionVersion === undefined) {
|
|
1046
|
+
throw new Error("Google Drive credential resolver omitted the connection version");
|
|
1047
|
+
}
|
|
728
1048
|
const fetchImpl = deps.googleDriveFetch ?? fetch;
|
|
729
1049
|
let response = await providerFetch(fetchImpl, input.url, {
|
|
730
1050
|
headers: { ...credential.headers, accept: "application/json" },
|
|
@@ -735,16 +1055,52 @@ async function googleDriveApiRequest(
|
|
|
735
1055
|
if (credential.status !== "ok") {
|
|
736
1056
|
throw new HTTPException(401, { message: "Google Drive needs to be reconnected" });
|
|
737
1057
|
}
|
|
1058
|
+
providerConnectionVersion = credential.connectionVersion;
|
|
1059
|
+
if (providerConnectionVersion === undefined) {
|
|
1060
|
+
throw new Error("Google Drive credential resolver omitted the connection version");
|
|
1061
|
+
}
|
|
738
1062
|
response = await providerFetch(fetchImpl, input.url, {
|
|
739
1063
|
headers: { ...credential.headers, accept: "application/json" },
|
|
740
1064
|
});
|
|
741
1065
|
}
|
|
742
1066
|
if (!response.ok) {
|
|
743
|
-
|
|
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
|
+
}
|
|
744
1100
|
throw new HTTPException(response.status === 403 ? 403 : 502, {
|
|
745
1101
|
message:
|
|
746
1102
|
response.status === 403
|
|
747
|
-
? "Google Drive denied metadata access;
|
|
1103
|
+
? "Google Drive denied metadata access; re-consent may be required"
|
|
748
1104
|
: "Google Drive metadata request failed",
|
|
749
1105
|
});
|
|
750
1106
|
}
|
|
@@ -767,6 +1123,25 @@ async function providerFetch(
|
|
|
767
1123
|
}
|
|
768
1124
|
}
|
|
769
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
|
+
|
|
770
1145
|
function parseDriveItem(value: unknown): GoogleDriveBrowseItem | null {
|
|
771
1146
|
const item = objectRecord(value);
|
|
772
1147
|
const id = optionalString(item.id);
|