From ff1af41b37f72e2e15e0cccf2a7d8bbce4b557e1 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Thu, 8 Jan 2026 15:11:50 -0800 Subject: [PATCH 1/9] new error throw and improvement --- apps/sim/app/api/auth/oauth/token/route.ts | 17 +++- apps/sim/app/api/auth/oauth/utils.ts | 37 +++++--- apps/sim/blocks/blocks/google_vault.ts | 91 +++++++++++++++++++ apps/sim/lib/oauth/oauth.ts | 29 +++++- .../google_vault/create_matters_export.ts | 19 +++- .../google_vault/create_matters_holds.ts | 45 +++++++++ apps/sim/tools/google_vault/types.ts | 7 +- 7 files changed, 222 insertions(+), 23 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index f5c8d7b617..6e65cefb9f 100644 --- a/apps/sim/app/api/auth/oauth/token/route.ts +++ b/apps/sim/app/api/auth/oauth/token/route.ts @@ -129,8 +129,12 @@ export async function POST(request: NextRequest) { { status: 200 } ) } catch (error) { - logger.error(`[${requestId}] Failed to refresh access token:`, error) - return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) + const errorMessage = error instanceof Error ? error.message : 'Failed to refresh access token' + logger.error(`[${requestId}] Failed to refresh access token:`, { + error: errorMessage, + stack: error instanceof Error ? error.stack : undefined, + }) + return NextResponse.json({ error: errorMessage }, { status: 401 }) } } catch (error) { logger.error(`[${requestId}] Error getting access token`, error) @@ -207,8 +211,13 @@ export async function GET(request: NextRequest) { }, { status: 200 } ) - } catch (_error) { - return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to refresh access token' + logger.error(`[${requestId}] Failed to refresh access token:`, { + error: errorMessage, + stack: error instanceof Error ? error.stack : undefined, + }) + return NextResponse.json({ error: errorMessage }, { status: 401 }) } } catch (error) { logger.error(`[${requestId}] Error fetching access token`, error) diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index 08dd16fdff..6ed65298b7 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -135,11 +135,14 @@ export async function getOAuthToken(userId: string, providerId: string): Promise const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!) if (!refreshResult) { - logger.error(`Failed to refresh token for user ${userId}, provider ${providerId}`, { - providerId, - userId, - hasRefreshToken: !!credential.refreshToken, - }) + logger.error( + `Failed to refresh token for user ${userId}, provider ${providerId} - no result returned`, + { + providerId, + userId, + hasRefreshToken: !!credential.refreshToken, + } + ) return null } @@ -170,7 +173,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise providerId, userId, }) - return null + throw error } } @@ -221,12 +224,15 @@ export async function refreshAccessTokenIfNeeded( ) if (!refreshedToken) { - logger.error(`[${requestId}] Failed to refresh token for credential: ${credentialId}`, { - credentialId, - providerId: credential.providerId, - userId: credential.userId, - hasRefreshToken: !!credential.refreshToken, - }) + logger.error( + `[${requestId}] Failed to refresh token for credential: ${credentialId} - no result returned`, + { + credentialId, + providerId: credential.providerId, + userId: credential.userId, + hasRefreshToken: !!credential.refreshToken, + } + ) return null } @@ -249,6 +255,7 @@ export async function refreshAccessTokenIfNeeded( logger.info(`[${requestId}] Successfully refreshed access token for credential`) return refreshedToken.accessToken } catch (error) { + // Re-throw the error to propagate detailed error messages (e.g., session expiry instructions) logger.error(`[${requestId}] Error refreshing token for credential`, { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, @@ -256,7 +263,7 @@ export async function refreshAccessTokenIfNeeded( credentialId, userId: credential.userId, }) - return null + throw error } } else if (!accessToken) { // We have no access token and either no refresh token or not eligible to refresh @@ -292,8 +299,8 @@ export async function refreshTokenIfNeeded( const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!) if (!refreshResult) { - logger.error(`[${requestId}] Failed to refresh token for credential`) - throw new Error('Failed to refresh token') + logger.error(`[${requestId}] Failed to refresh token for credential - no result returned`) + throw new Error('Failed to refresh token: no result returned from provider') } const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult diff --git a/apps/sim/blocks/blocks/google_vault.ts b/apps/sim/blocks/blocks/google_vault.ts index c54098aad3..3de897bb16 100644 --- a/apps/sim/blocks/blocks/google_vault.ts +++ b/apps/sim/blocks/blocks/google_vault.ts @@ -159,6 +159,90 @@ Return ONLY the hold name - no explanations, no quotes, no extra text.`, placeholder: 'Org Unit ID (alternative to emails)', condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] }, }, + // Date filtering for exports and holds (holds only support MAIL and GROUPS corpus) + { + id: 'startTime', + title: 'Start Time', + type: 'short-input', + placeholder: 'YYYY-MM-DDTHH:mm:ssZ', + condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering. +The timestamp should be in the format: YYYY-MM-DDTHH:mm:ssZ (UTC timezone). +Note: Google Vault rounds times to 12 AM on the specified date. +Examples: +- "yesterday" -> Calculate yesterday's date at 00:00:00Z +- "last week" -> Calculate 7 days ago at 00:00:00Z +- "beginning of this month" -> Calculate the 1st of current month at 00:00:00Z +- "January 1, 2024" -> 2024-01-01T00:00:00Z + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the start date (e.g., "last month", "January 1, 2024")...', + generationType: 'timestamp', + }, + }, + { + id: 'endTime', + title: 'End Time', + type: 'short-input', + placeholder: 'YYYY-MM-DDTHH:mm:ssZ', + condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering. +The timestamp should be in the format: YYYY-MM-DDTHH:mm:ssZ (UTC timezone). +Note: Google Vault rounds times to 12 AM on the specified date. +Examples: +- "now" -> Current timestamp +- "today" -> Today's date at 23:59:59Z +- "end of last month" -> Last day of previous month at 23:59:59Z +- "December 31, 2024" -> 2024-12-31T23:59:59Z + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the end date (e.g., "today", "end of last quarter")...', + generationType: 'timestamp', + }, + }, + { + id: 'terms', + title: 'Search Terms', + type: 'long-input', + placeholder: 'Enter search query (e.g., from:user@example.com subject:confidential)', + condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] }, + wandConfig: { + enabled: true, + prompt: `Generate a Google Vault search query based on the user's description. +The query can use Gmail-style search operators for MAIL corpus: +- from:user@example.com - emails from specific sender +- to:user@example.com - emails to specific recipient +- subject:keyword - emails with keyword in subject +- has:attachment - emails with attachments +- filename:pdf - emails with PDF attachments +- before:YYYY/MM/DD - emails before date +- after:YYYY/MM/DD - emails after date + +For DRIVE corpus, use Drive search operators: +- owner:user@example.com - files owned by user +- type:document - specific file types + +For holds, date filtering only works with MAIL and GROUPS corpus. + +Return ONLY the search query - no explanations, no quotes, no extra text.`, + placeholder: 'Describe what content to search for...', + }, + }, + // Drive-specific option for holds + { + id: 'includeSharedDrives', + title: 'Include Shared Drives', + type: 'switch', + condition: { + field: 'operation', + value: 'create_matters_holds', + and: { field: 'corpus', value: 'DRIVE' }, + }, + }, { id: 'exportId', title: 'Export ID', @@ -296,9 +380,16 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, corpus: { type: 'string', description: 'Data corpus (MAIL, DRIVE, GROUPS, etc.)' }, accountEmails: { type: 'string', description: 'Comma-separated account emails' }, orgUnitId: { type: 'string', description: 'Organization unit ID' }, + startTime: { type: 'string', description: 'Start time for date filtering (ISO 8601 format)' }, + endTime: { type: 'string', description: 'End time for date filtering (ISO 8601 format)' }, + terms: { type: 'string', description: 'Search query terms' }, // Create hold inputs holdName: { type: 'string', description: 'Name for the hold' }, + includeSharedDrives: { + type: 'boolean', + description: 'Include files in shared drives (for DRIVE corpus holds)', + }, // Download export file inputs bucketName: { type: 'string', description: 'GCS bucket name from export' }, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 94496a24b0..a78e2935c5 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -1171,7 +1171,7 @@ export async function refreshOAuthToken( if (!response.ok) { const errorText = await response.text() - let errorData = errorText + let errorData: any = errorText try { errorData = JSON.parse(errorText) @@ -1191,6 +1191,29 @@ export async function refreshOAuthToken( hasRefreshToken: !!refreshToken, refreshTokenPrefix: refreshToken ? `${refreshToken.substring(0, 10)}...` : 'none', }) + + // Check for Google Workspace session control errors (RAPT - Reauthentication Policy Token) + // This occurs when the organization enforces periodic re-authentication + if ( + typeof errorData === 'object' && + (errorData.error_subtype === 'invalid_rapt' || + errorData.error_description?.includes('reauth related error')) + ) { + throw new Error( + `Session expired due to organization security policy. Please reconnect your ${providerId} account to continue. Alternatively, ask your Google Workspace admin to exempt this app from session control: Admin Console → Security → Google Cloud session control → "Exempt trusted apps".` + ) + } + + if ( + typeof errorData === 'object' && + errorData.error === 'invalid_grant' && + !errorData.error_subtype + ) { + throw new Error( + `Access has been revoked or the refresh token is no longer valid. Please reconnect your ${providerId} account.` + ) + } + throw new Error(`Failed to refresh token: ${response.status} ${errorText}`) } @@ -1224,6 +1247,8 @@ export async function refreshOAuthToken( } } catch (error) { logger.error('Error refreshing token:', { error }) - return null + // Re-throw specific errors so they propagate with their detailed messages + // Only return null for truly unexpected errors without useful messages + throw error } } diff --git a/apps/sim/tools/google_vault/create_matters_export.ts b/apps/sim/tools/google_vault/create_matters_export.ts index f468fc7ab7..3f443ce6d6 100644 --- a/apps/sim/tools/google_vault/create_matters_export.ts +++ b/apps/sim/tools/google_vault/create_matters_export.ts @@ -36,6 +36,24 @@ export const createMattersExportTool: ToolConfig Date: Thu, 8 Jan 2026 15:50:25 -0800 Subject: [PATCH 2/9] fixed critical issues --- apps/sim/app/api/auth/oauth/utils.ts | 9 ++- apps/sim/blocks/blocks/google_vault.ts | 87 ++++++++++++++++++++++++-- apps/sim/lib/oauth/oauth.ts | 2 - 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index 6ed65298b7..003c8b46ef 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -173,7 +173,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise providerId, userId, }) - throw error + return null } } @@ -255,7 +255,6 @@ export async function refreshAccessTokenIfNeeded( logger.info(`[${requestId}] Successfully refreshed access token for credential`) return refreshedToken.accessToken } catch (error) { - // Re-throw the error to propagate detailed error messages (e.g., session expiry instructions) logger.error(`[${requestId}] Error refreshing token for credential`, { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, @@ -263,7 +262,7 @@ export async function refreshAccessTokenIfNeeded( credentialId, userId: credential.userId, }) - throw error + return null } } else if (!accessToken) { // We have no access token and either no refresh token or not eligible to refresh @@ -299,8 +298,8 @@ export async function refreshTokenIfNeeded( const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!) if (!refreshResult) { - logger.error(`[${requestId}] Failed to refresh token for credential - no result returned`) - throw new Error('Failed to refresh token: no result returned from provider') + logger.error(`[${requestId}] Failed to refresh token for credential`) + throw new Error('Failed to refresh token') } const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult diff --git a/apps/sim/blocks/blocks/google_vault.ts b/apps/sim/blocks/blocks/google_vault.ts index 3de897bb16..ecc5f9ff4c 100644 --- a/apps/sim/blocks/blocks/google_vault.ts +++ b/apps/sim/blocks/blocks/google_vault.ts @@ -159,13 +159,62 @@ Return ONLY the hold name - no explanations, no quotes, no extra text.`, placeholder: 'Org Unit ID (alternative to emails)', condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] }, }, - // Date filtering for exports and holds (holds only support MAIL and GROUPS corpus) + // Date filtering for exports (works with all corpus types) { id: 'startTime', title: 'Start Time', type: 'short-input', placeholder: 'YYYY-MM-DDTHH:mm:ssZ', - condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] }, + condition: { field: 'operation', value: 'create_matters_export' }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering. +The timestamp should be in the format: YYYY-MM-DDTHH:mm:ssZ (UTC timezone). +Note: Google Vault rounds times to 12 AM on the specified date. +Examples: +- "yesterday" -> Calculate yesterday's date at 00:00:00Z +- "last week" -> Calculate 7 days ago at 00:00:00Z +- "beginning of this month" -> Calculate the 1st of current month at 00:00:00Z +- "January 1, 2024" -> 2024-01-01T00:00:00Z + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the start date (e.g., "last month", "January 1, 2024")...', + generationType: 'timestamp', + }, + }, + { + id: 'endTime', + title: 'End Time', + type: 'short-input', + placeholder: 'YYYY-MM-DDTHH:mm:ssZ', + condition: { field: 'operation', value: 'create_matters_export' }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering. +The timestamp should be in the format: YYYY-MM-DDTHH:mm:ssZ (UTC timezone). +Note: Google Vault rounds times to 12 AM on the specified date. +Examples: +- "now" -> Current timestamp +- "today" -> Today's date at 23:59:59Z +- "end of last month" -> Last day of previous month at 23:59:59Z +- "December 31, 2024" -> 2024-12-31T23:59:59Z + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the end date (e.g., "today", "end of last quarter")...', + generationType: 'timestamp', + }, + }, + // Date filtering for holds (only works with MAIL and GROUPS corpus) + { + id: 'startTime', + title: 'Start Time', + type: 'short-input', + placeholder: 'YYYY-MM-DDTHH:mm:ssZ', + condition: { + field: 'operation', + value: 'create_matters_holds', + and: { field: 'corpus', value: ['MAIL', 'GROUPS'] }, + }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering. @@ -187,7 +236,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'End Time', type: 'short-input', placeholder: 'YYYY-MM-DDTHH:mm:ssZ', - condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] }, + condition: { + field: 'operation', + value: 'create_matters_holds', + and: { field: 'corpus', value: ['MAIL', 'GROUPS'] }, + }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering. @@ -204,12 +257,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, generationType: 'timestamp', }, }, + // Search terms for exports (works with all corpus types) { id: 'terms', title: 'Search Terms', type: 'long-input', placeholder: 'Enter search query (e.g., from:user@example.com subject:confidential)', - condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] }, + condition: { field: 'operation', value: 'create_matters_export' }, wandConfig: { enabled: true, prompt: `Generate a Google Vault search query based on the user's description. @@ -226,7 +280,30 @@ For DRIVE corpus, use Drive search operators: - owner:user@example.com - files owned by user - type:document - specific file types -For holds, date filtering only works with MAIL and GROUPS corpus. +Return ONLY the search query - no explanations, no quotes, no extra text.`, + placeholder: 'Describe what content to search for...', + }, + }, + // Search terms for holds (only works with MAIL and GROUPS corpus) + { + id: 'terms', + title: 'Search Terms', + type: 'long-input', + placeholder: 'Enter search query (e.g., from:user@example.com subject:confidential)', + condition: { + field: 'operation', + value: 'create_matters_holds', + and: { field: 'corpus', value: ['MAIL', 'GROUPS'] }, + }, + wandConfig: { + enabled: true, + prompt: `Generate a Google Vault search query based on the user's description. +The query can use Gmail-style search operators: +- from:user@example.com - emails from specific sender +- to:user@example.com - emails to specific recipient +- subject:keyword - emails with keyword in subject +- has:attachment - emails with attachments +- filename:pdf - emails with PDF attachments Return ONLY the search query - no explanations, no quotes, no extra text.`, placeholder: 'Describe what content to search for...', diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index a78e2935c5..33880ea36e 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -1247,8 +1247,6 @@ export async function refreshOAuthToken( } } catch (error) { logger.error('Error refreshing token:', { error }) - // Re-throw specific errors so they propagate with their detailed messages - // Only return null for truly unexpected errors without useful messages throw error } } From 1fee00aae1ba8b76e34dfb34a27edb7068c9eca2 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Thu, 8 Jan 2026 16:02:24 -0800 Subject: [PATCH 3/9] restore error thorwing --- apps/sim/app/api/auth/oauth/token/route.ts | 17 ++++---------- apps/sim/lib/oauth/oauth.ts | 27 ++-------------------- apps/sim/tools/google_vault/types.ts | 4 +--- 3 files changed, 7 insertions(+), 41 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index 6e65cefb9f..f5c8d7b617 100644 --- a/apps/sim/app/api/auth/oauth/token/route.ts +++ b/apps/sim/app/api/auth/oauth/token/route.ts @@ -129,12 +129,8 @@ export async function POST(request: NextRequest) { { status: 200 } ) } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to refresh access token' - logger.error(`[${requestId}] Failed to refresh access token:`, { - error: errorMessage, - stack: error instanceof Error ? error.stack : undefined, - }) - return NextResponse.json({ error: errorMessage }, { status: 401 }) + logger.error(`[${requestId}] Failed to refresh access token:`, error) + return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) } } catch (error) { logger.error(`[${requestId}] Error getting access token`, error) @@ -211,13 +207,8 @@ export async function GET(request: NextRequest) { }, { status: 200 } ) - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to refresh access token' - logger.error(`[${requestId}] Failed to refresh access token:`, { - error: errorMessage, - stack: error instanceof Error ? error.stack : undefined, - }) - return NextResponse.json({ error: errorMessage }, { status: 401 }) + } catch (_error) { + return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) } } catch (error) { logger.error(`[${requestId}] Error fetching access token`, error) diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 33880ea36e..94496a24b0 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -1171,7 +1171,7 @@ export async function refreshOAuthToken( if (!response.ok) { const errorText = await response.text() - let errorData: any = errorText + let errorData = errorText try { errorData = JSON.parse(errorText) @@ -1191,29 +1191,6 @@ export async function refreshOAuthToken( hasRefreshToken: !!refreshToken, refreshTokenPrefix: refreshToken ? `${refreshToken.substring(0, 10)}...` : 'none', }) - - // Check for Google Workspace session control errors (RAPT - Reauthentication Policy Token) - // This occurs when the organization enforces periodic re-authentication - if ( - typeof errorData === 'object' && - (errorData.error_subtype === 'invalid_rapt' || - errorData.error_description?.includes('reauth related error')) - ) { - throw new Error( - `Session expired due to organization security policy. Please reconnect your ${providerId} account to continue. Alternatively, ask your Google Workspace admin to exempt this app from session control: Admin Console → Security → Google Cloud session control → "Exempt trusted apps".` - ) - } - - if ( - typeof errorData === 'object' && - errorData.error === 'invalid_grant' && - !errorData.error_subtype - ) { - throw new Error( - `Access has been revoked or the refresh token is no longer valid. Please reconnect your ${providerId} account.` - ) - } - throw new Error(`Failed to refresh token: ${response.status} ${errorText}`) } @@ -1247,6 +1224,6 @@ export async function refreshOAuthToken( } } catch (error) { logger.error('Error refreshing token:', { error }) - throw error + return null } } diff --git a/apps/sim/tools/google_vault/types.ts b/apps/sim/tools/google_vault/types.ts index a9583f78ec..344bb02eaf 100644 --- a/apps/sim/tools/google_vault/types.ts +++ b/apps/sim/tools/google_vault/types.ts @@ -36,13 +36,11 @@ export type GoogleVaultCorpus = 'MAIL' | 'DRIVE' | 'GROUPS' | 'HANGOUTS_CHAT' | export interface GoogleVaultCreateMattersHoldsParams extends GoogleVaultCommonParams { holdName: string corpus: GoogleVaultCorpus - accountEmails?: string // Comma-separated list or array handled in the tool + accountEmails?: string orgUnitId?: string - // Query parameters for MAIL and GROUPS corpus (date filtering) terms?: string startTime?: string endTime?: string - // Drive-specific option includeSharedDrives?: boolean } From a00ec627ec6fe1679e95dbf07f90b4df80168304 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Thu, 8 Jan 2026 16:03:12 -0800 Subject: [PATCH 4/9] restore --- apps/sim/app/api/auth/oauth/utils.ts | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index 003c8b46ef..08dd16fdff 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -135,14 +135,11 @@ export async function getOAuthToken(userId: string, providerId: string): Promise const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!) if (!refreshResult) { - logger.error( - `Failed to refresh token for user ${userId}, provider ${providerId} - no result returned`, - { - providerId, - userId, - hasRefreshToken: !!credential.refreshToken, - } - ) + logger.error(`Failed to refresh token for user ${userId}, provider ${providerId}`, { + providerId, + userId, + hasRefreshToken: !!credential.refreshToken, + }) return null } @@ -224,15 +221,12 @@ export async function refreshAccessTokenIfNeeded( ) if (!refreshedToken) { - logger.error( - `[${requestId}] Failed to refresh token for credential: ${credentialId} - no result returned`, - { - credentialId, - providerId: credential.providerId, - userId: credential.userId, - hasRefreshToken: !!credential.refreshToken, - } - ) + logger.error(`[${requestId}] Failed to refresh token for credential: ${credentialId}`, { + credentialId, + providerId: credential.providerId, + userId: credential.userId, + hasRefreshToken: !!credential.refreshToken, + }) return null } From 59aca33ee330a349f19b80bdde220debca3a9fd5 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Thu, 8 Jan 2026 16:17:19 -0800 Subject: [PATCH 5/9] added handler for vault --- .../google-vault/google-vault-handler.ts | 198 ++++++++++++++++++ apps/sim/executor/handlers/index.ts | 2 + apps/sim/executor/handlers/registry.ts | 2 + 3 files changed, 202 insertions(+) create mode 100644 apps/sim/executor/handlers/google-vault/google-vault-handler.ts diff --git a/apps/sim/executor/handlers/google-vault/google-vault-handler.ts b/apps/sim/executor/handlers/google-vault/google-vault-handler.ts new file mode 100644 index 0000000000..abdd86eb09 --- /dev/null +++ b/apps/sim/executor/handlers/google-vault/google-vault-handler.ts @@ -0,0 +1,198 @@ +/** + * Google Vault Block Handler + * + * Specialized handler for Google Vault blocks that provides enhanced error + * messages for credential-related issues specific to Google Vault's + * administrative requirements. + */ + +import { createLogger } from '@sim/logger' +import { getBlock } from '@/blocks/index' +import type { BlockHandler, ExecutionContext } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' +import { executeTool } from '@/tools' +import { getTool } from '@/tools/utils' + +const logger = createLogger('GoogleVaultBlockHandler') + +/** + * Detects Google Vault credential/reauthentication errors + * These can manifest as: + * - RAPT (reauthentication policy) errors when Google Workspace admin requires reauth + * - Generic "failed to refresh token" errors which often wrap RAPT errors + */ +function isCredentialRefreshError(errorMessage: string): boolean { + const lowerMessage = errorMessage.toLowerCase() + return ( + lowerMessage.includes('invalid_rapt') || + lowerMessage.includes('reauth related error') || + (lowerMessage.includes('invalid_grant') && lowerMessage.includes('rapt')) || + lowerMessage.includes('failed to refresh token') || + (lowerMessage.includes('failed to fetch access token') && lowerMessage.includes('401')) + ) +} + +/** + * Enhances error messages for Google Vault credential failures + * Provides actionable workaround instructions for administrators + */ +function enhanceCredentialError(originalError: string): string { + if (isCredentialRefreshError(originalError)) { + return ( + `Google Vault authentication failed (likely due to reauthentication policy). ` + + `To resolve this, try disconnecting and reconnecting your Google Vault credential ` + + `in the Credentials settings. If the issue persists, ask your Google Workspace ` + + `administrator to disable "Reauthentication policy" for Sim Studio in the Google ` + + `Admin Console (Security > Access and data control > Context-Aware Access > ` + + `Reauthentication policy), or exempt Sim Studio from reauthentication requirements. ` + + `Learn more: https://support.google.com/a/answer/9368756` + ) + } + return originalError +} + +export class GoogleVaultBlockHandler implements BlockHandler { + canHandle(block: SerializedBlock): boolean { + return block.metadata?.id === 'google_vault' + } + + async execute( + ctx: ExecutionContext, + block: SerializedBlock, + inputs: Record + ): Promise { + const tool = getTool(block.config.tool) + if (!tool) { + throw new Error(`Tool not found: ${block.config.tool}`) + } + + let finalInputs = { ...inputs } + + const blockType = block.metadata?.id + if (blockType) { + const blockConfig = getBlock(blockType) + if (blockConfig?.tools?.config?.params) { + try { + const transformedParams = blockConfig.tools.config.params(inputs) + finalInputs = { ...inputs, ...transformedParams } + } catch (error) { + logger.warn(`Failed to apply parameter transformation for block type ${blockType}:`, { + error: error instanceof Error ? error.message : String(error), + }) + } + } + + if (blockConfig?.inputs) { + for (const [key, inputSchema] of Object.entries(blockConfig.inputs)) { + const value = finalInputs[key] + if (typeof value === 'string' && value.trim().length > 0) { + const inputType = typeof inputSchema === 'object' ? inputSchema.type : inputSchema + if (inputType === 'json' || inputType === 'array') { + try { + finalInputs[key] = JSON.parse(value.trim()) + } catch (error) { + logger.warn(`Failed to parse ${inputType} field "${key}":`, { + error: error instanceof Error ? error.message : String(error), + }) + } + } + } + } + } + } + + try { + const result = await executeTool( + block.config.tool, + { + ...finalInputs, + _context: { + workflowId: ctx.workflowId, + workspaceId: ctx.workspaceId, + executionId: ctx.executionId, + }, + }, + false, + false, + ctx + ) + + if (!result.success) { + const errorDetails = [] + if (result.error) { + // Enhance credential errors with Google Vault specific guidance + errorDetails.push(enhanceCredentialError(result.error)) + } + + const errorMessage = + errorDetails.length > 0 + ? errorDetails.join(' - ') + : `Block execution of ${tool?.name || block.config.tool} failed with no error message` + + const error = new Error(errorMessage) + + Object.assign(error, { + toolId: block.config.tool, + toolName: tool?.name || 'Unknown tool', + blockId: block.id, + blockName: block.metadata?.name || 'Unnamed Block', + output: result.output || {}, + timestamp: new Date().toISOString(), + }) + + throw error + } + + const output = result.output + let cost = null + + if (output?.cost) { + cost = output.cost + } + + if (cost) { + return { + ...output, + cost: { + input: cost.input, + output: cost.output, + total: cost.total, + }, + tokens: cost.tokens, + model: cost.model, + } + } + + return output + } catch (error: any) { + // Enhance credential errors thrown during tool execution + if (error instanceof Error) { + const enhancedMessage = enhanceCredentialError(error.message) + if (enhancedMessage !== error.message) { + error.message = enhancedMessage + } + } + + if (!error.message || error.message === 'undefined (undefined)') { + let errorMessage = `Block execution of ${tool?.name || block.config.tool} failed` + + if (block.metadata?.name) { + errorMessage += `: ${block.metadata.name}` + } + + if (error.status) { + errorMessage += ` (Status: ${error.status})` + } + + error.message = errorMessage + } + + if (typeof error === 'object' && error !== null) { + if (!error.toolId) error.toolId = block.config.tool + if (!error.blockName) error.blockName = block.metadata?.name || 'Unnamed Block' + } + + throw error + } + } +} diff --git a/apps/sim/executor/handlers/index.ts b/apps/sim/executor/handlers/index.ts index fb27421b2d..fbb2f661ce 100644 --- a/apps/sim/executor/handlers/index.ts +++ b/apps/sim/executor/handlers/index.ts @@ -4,6 +4,7 @@ import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-h import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' +import { GoogleVaultBlockHandler } from '@/executor/handlers/google-vault/google-vault-handler' import { HumanInTheLoopBlockHandler } from '@/executor/handlers/human-in-the-loop/human-in-the-loop-handler' import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' @@ -19,6 +20,7 @@ export { EvaluatorBlockHandler, FunctionBlockHandler, GenericBlockHandler, + GoogleVaultBlockHandler, ResponseBlockHandler, HumanInTheLoopBlockHandler, RouterBlockHandler, diff --git a/apps/sim/executor/handlers/registry.ts b/apps/sim/executor/handlers/registry.ts index 9e977668a2..31bcbe57f7 100644 --- a/apps/sim/executor/handlers/registry.ts +++ b/apps/sim/executor/handlers/registry.ts @@ -11,6 +11,7 @@ import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-h import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' +import { GoogleVaultBlockHandler } from '@/executor/handlers/google-vault/google-vault-handler' import { HumanInTheLoopBlockHandler } from '@/executor/handlers/human-in-the-loop/human-in-the-loop-handler' import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' @@ -40,6 +41,7 @@ export function createBlockHandlers(): BlockHandler[] { new WorkflowBlockHandler(), new WaitBlockHandler(), new EvaluatorBlockHandler(), + new GoogleVaultBlockHandler(), new GenericBlockHandler(), ] } From 53cfd70e113649719367cdfd7dd6301db0cbf195 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Thu, 8 Jan 2026 16:18:21 -0800 Subject: [PATCH 6/9] updated docs --- apps/docs/components/icons.tsx | 19 +++++++++++++++++++ .../content/docs/en/tools/google_vault.mdx | 7 +++++++ apps/docs/content/docs/en/tools/translate.mdx | 3 +++ 3 files changed, 29 insertions(+) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 192905bead..de0ab92021 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4575,3 +4575,22 @@ export function FirefliesIcon(props: SVGProps) { ) } + +export function BedrockIcon(props: SVGProps) { + return ( + + + + + + + + + + + ) +} diff --git a/apps/docs/content/docs/en/tools/google_vault.mdx b/apps/docs/content/docs/en/tools/google_vault.mdx index 39714d1122..7f5d3b34d5 100644 --- a/apps/docs/content/docs/en/tools/google_vault.mdx +++ b/apps/docs/content/docs/en/tools/google_vault.mdx @@ -31,6 +31,9 @@ Create an export in a matter | `corpus` | string | Yes | Data corpus to export \(MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE\) | | `accountEmails` | string | No | Comma-separated list of user emails to scope export | | `orgUnitId` | string | No | Organization unit ID to scope export \(alternative to emails\) | +| `startTime` | string | No | Start time for date filtering \(ISO 8601 format, e.g., 2024-01-01T00:00:00Z\) | +| `endTime` | string | No | End time for date filtering \(ISO 8601 format, e.g., 2024-12-31T23:59:59Z\) | +| `terms` | string | No | Search query terms to filter exported content | #### Output @@ -91,6 +94,10 @@ Create a hold in a matter | `corpus` | string | Yes | Data corpus to hold \(MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE\) | | `accountEmails` | string | No | Comma-separated list of user emails to put on hold | | `orgUnitId` | string | No | Organization unit ID to put on hold \(alternative to accounts\) | +| `terms` | string | No | Search terms to filter held content \(for MAIL and GROUPS corpus\) | +| `startTime` | string | No | Start time for date filtering \(ISO 8601 format, for MAIL and GROUPS corpus\) | +| `endTime` | string | No | End time for date filtering \(ISO 8601 format, for MAIL and GROUPS corpus\) | +| `includeSharedDrives` | boolean | No | Include files in shared drives \(for DRIVE corpus\) | #### Output diff --git a/apps/docs/content/docs/en/tools/translate.mdx b/apps/docs/content/docs/en/tools/translate.mdx index 790cc4d8bc..d28443a91b 100644 --- a/apps/docs/content/docs/en/tools/translate.mdx +++ b/apps/docs/content/docs/en/tools/translate.mdx @@ -53,6 +53,9 @@ Send a chat completion request to any supported LLM provider | `vertexProject` | string | No | Google Cloud project ID for Vertex AI | | `vertexLocation` | string | No | Google Cloud location for Vertex AI \(defaults to us-central1\) | | `vertexCredential` | string | No | Google Cloud OAuth credential ID for Vertex AI | +| `bedrockAccessKeyId` | string | No | AWS Access Key ID for Bedrock | +| `bedrockSecretKey` | string | No | AWS Secret Access Key for Bedrock | +| `bedrockRegion` | string | No | AWS region for Bedrock \(defaults to us-east-1\) | #### Output From 58417ebaa83b47dad2e08866bbd83846bdec5ecf Mon Sep 17 00:00:00 2001 From: aadamgough Date: Thu, 8 Jan 2026 16:19:40 -0800 Subject: [PATCH 7/9] restored --- apps/docs/content/docs/en/tools/translate.mdx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/docs/content/docs/en/tools/translate.mdx b/apps/docs/content/docs/en/tools/translate.mdx index d28443a91b..790cc4d8bc 100644 --- a/apps/docs/content/docs/en/tools/translate.mdx +++ b/apps/docs/content/docs/en/tools/translate.mdx @@ -53,9 +53,6 @@ Send a chat completion request to any supported LLM provider | `vertexProject` | string | No | Google Cloud project ID for Vertex AI | | `vertexLocation` | string | No | Google Cloud location for Vertex AI \(defaults to us-central1\) | | `vertexCredential` | string | No | Google Cloud OAuth credential ID for Vertex AI | -| `bedrockAccessKeyId` | string | No | AWS Access Key ID for Bedrock | -| `bedrockSecretKey` | string | No | AWS Secret Access Key for Bedrock | -| `bedrockRegion` | string | No | AWS region for Bedrock \(defaults to us-east-1\) | #### Output From 79a93ed6304634d35bf402ca28371bb5870f19a3 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Thu, 8 Jan 2026 16:20:54 -0800 Subject: [PATCH 8/9] Restored icon file --- apps/docs/components/icons.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index de0ab92021..192905bead 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4575,22 +4575,3 @@ export function FirefliesIcon(props: SVGProps) { ) } - -export function BedrockIcon(props: SVGProps) { - return ( - - - - - - - - - - - ) -} From 3979d09892e0edd80181d59eb0b0865990cc8d7b Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 8 Jan 2026 23:31:12 -0800 Subject: [PATCH 9/9] removed google vault from executor --- .../google-vault/google-vault-handler.ts | 198 ------------------ apps/sim/executor/handlers/index.ts | 2 - apps/sim/executor/handlers/registry.ts | 2 - apps/sim/tools/google_vault/create_matters.ts | 13 +- .../google_vault/create_matters_export.ts | 7 +- .../google_vault/create_matters_holds.ts | 12 +- .../google_vault/download_export_file.ts | 29 +-- apps/sim/tools/google_vault/list_matters.ts | 12 +- .../tools/google_vault/list_matters_export.ts | 4 +- .../tools/google_vault/list_matters_holds.ts | 4 +- apps/sim/tools/google_vault/types.ts | 30 ++- apps/sim/tools/google_vault/utils.ts | 41 ++++ 12 files changed, 94 insertions(+), 260 deletions(-) delete mode 100644 apps/sim/executor/handlers/google-vault/google-vault-handler.ts create mode 100644 apps/sim/tools/google_vault/utils.ts diff --git a/apps/sim/executor/handlers/google-vault/google-vault-handler.ts b/apps/sim/executor/handlers/google-vault/google-vault-handler.ts deleted file mode 100644 index abdd86eb09..0000000000 --- a/apps/sim/executor/handlers/google-vault/google-vault-handler.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * Google Vault Block Handler - * - * Specialized handler for Google Vault blocks that provides enhanced error - * messages for credential-related issues specific to Google Vault's - * administrative requirements. - */ - -import { createLogger } from '@sim/logger' -import { getBlock } from '@/blocks/index' -import type { BlockHandler, ExecutionContext } from '@/executor/types' -import type { SerializedBlock } from '@/serializer/types' -import { executeTool } from '@/tools' -import { getTool } from '@/tools/utils' - -const logger = createLogger('GoogleVaultBlockHandler') - -/** - * Detects Google Vault credential/reauthentication errors - * These can manifest as: - * - RAPT (reauthentication policy) errors when Google Workspace admin requires reauth - * - Generic "failed to refresh token" errors which often wrap RAPT errors - */ -function isCredentialRefreshError(errorMessage: string): boolean { - const lowerMessage = errorMessage.toLowerCase() - return ( - lowerMessage.includes('invalid_rapt') || - lowerMessage.includes('reauth related error') || - (lowerMessage.includes('invalid_grant') && lowerMessage.includes('rapt')) || - lowerMessage.includes('failed to refresh token') || - (lowerMessage.includes('failed to fetch access token') && lowerMessage.includes('401')) - ) -} - -/** - * Enhances error messages for Google Vault credential failures - * Provides actionable workaround instructions for administrators - */ -function enhanceCredentialError(originalError: string): string { - if (isCredentialRefreshError(originalError)) { - return ( - `Google Vault authentication failed (likely due to reauthentication policy). ` + - `To resolve this, try disconnecting and reconnecting your Google Vault credential ` + - `in the Credentials settings. If the issue persists, ask your Google Workspace ` + - `administrator to disable "Reauthentication policy" for Sim Studio in the Google ` + - `Admin Console (Security > Access and data control > Context-Aware Access > ` + - `Reauthentication policy), or exempt Sim Studio from reauthentication requirements. ` + - `Learn more: https://support.google.com/a/answer/9368756` - ) - } - return originalError -} - -export class GoogleVaultBlockHandler implements BlockHandler { - canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'google_vault' - } - - async execute( - ctx: ExecutionContext, - block: SerializedBlock, - inputs: Record - ): Promise { - const tool = getTool(block.config.tool) - if (!tool) { - throw new Error(`Tool not found: ${block.config.tool}`) - } - - let finalInputs = { ...inputs } - - const blockType = block.metadata?.id - if (blockType) { - const blockConfig = getBlock(blockType) - if (blockConfig?.tools?.config?.params) { - try { - const transformedParams = blockConfig.tools.config.params(inputs) - finalInputs = { ...inputs, ...transformedParams } - } catch (error) { - logger.warn(`Failed to apply parameter transformation for block type ${blockType}:`, { - error: error instanceof Error ? error.message : String(error), - }) - } - } - - if (blockConfig?.inputs) { - for (const [key, inputSchema] of Object.entries(blockConfig.inputs)) { - const value = finalInputs[key] - if (typeof value === 'string' && value.trim().length > 0) { - const inputType = typeof inputSchema === 'object' ? inputSchema.type : inputSchema - if (inputType === 'json' || inputType === 'array') { - try { - finalInputs[key] = JSON.parse(value.trim()) - } catch (error) { - logger.warn(`Failed to parse ${inputType} field "${key}":`, { - error: error instanceof Error ? error.message : String(error), - }) - } - } - } - } - } - } - - try { - const result = await executeTool( - block.config.tool, - { - ...finalInputs, - _context: { - workflowId: ctx.workflowId, - workspaceId: ctx.workspaceId, - executionId: ctx.executionId, - }, - }, - false, - false, - ctx - ) - - if (!result.success) { - const errorDetails = [] - if (result.error) { - // Enhance credential errors with Google Vault specific guidance - errorDetails.push(enhanceCredentialError(result.error)) - } - - const errorMessage = - errorDetails.length > 0 - ? errorDetails.join(' - ') - : `Block execution of ${tool?.name || block.config.tool} failed with no error message` - - const error = new Error(errorMessage) - - Object.assign(error, { - toolId: block.config.tool, - toolName: tool?.name || 'Unknown tool', - blockId: block.id, - blockName: block.metadata?.name || 'Unnamed Block', - output: result.output || {}, - timestamp: new Date().toISOString(), - }) - - throw error - } - - const output = result.output - let cost = null - - if (output?.cost) { - cost = output.cost - } - - if (cost) { - return { - ...output, - cost: { - input: cost.input, - output: cost.output, - total: cost.total, - }, - tokens: cost.tokens, - model: cost.model, - } - } - - return output - } catch (error: any) { - // Enhance credential errors thrown during tool execution - if (error instanceof Error) { - const enhancedMessage = enhanceCredentialError(error.message) - if (enhancedMessage !== error.message) { - error.message = enhancedMessage - } - } - - if (!error.message || error.message === 'undefined (undefined)') { - let errorMessage = `Block execution of ${tool?.name || block.config.tool} failed` - - if (block.metadata?.name) { - errorMessage += `: ${block.metadata.name}` - } - - if (error.status) { - errorMessage += ` (Status: ${error.status})` - } - - error.message = errorMessage - } - - if (typeof error === 'object' && error !== null) { - if (!error.toolId) error.toolId = block.config.tool - if (!error.blockName) error.blockName = block.metadata?.name || 'Unnamed Block' - } - - throw error - } - } -} diff --git a/apps/sim/executor/handlers/index.ts b/apps/sim/executor/handlers/index.ts index fbb2f661ce..fb27421b2d 100644 --- a/apps/sim/executor/handlers/index.ts +++ b/apps/sim/executor/handlers/index.ts @@ -4,7 +4,6 @@ import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-h import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' -import { GoogleVaultBlockHandler } from '@/executor/handlers/google-vault/google-vault-handler' import { HumanInTheLoopBlockHandler } from '@/executor/handlers/human-in-the-loop/human-in-the-loop-handler' import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' @@ -20,7 +19,6 @@ export { EvaluatorBlockHandler, FunctionBlockHandler, GenericBlockHandler, - GoogleVaultBlockHandler, ResponseBlockHandler, HumanInTheLoopBlockHandler, RouterBlockHandler, diff --git a/apps/sim/executor/handlers/registry.ts b/apps/sim/executor/handlers/registry.ts index 31bcbe57f7..9e977668a2 100644 --- a/apps/sim/executor/handlers/registry.ts +++ b/apps/sim/executor/handlers/registry.ts @@ -11,7 +11,6 @@ import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-h import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' -import { GoogleVaultBlockHandler } from '@/executor/handlers/google-vault/google-vault-handler' import { HumanInTheLoopBlockHandler } from '@/executor/handlers/human-in-the-loop/human-in-the-loop-handler' import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' @@ -41,7 +40,6 @@ export function createBlockHandlers(): BlockHandler[] { new WorkflowBlockHandler(), new WaitBlockHandler(), new EvaluatorBlockHandler(), - new GoogleVaultBlockHandler(), new GenericBlockHandler(), ] } diff --git a/apps/sim/tools/google_vault/create_matters.ts b/apps/sim/tools/google_vault/create_matters.ts index 9886606656..ccbfc587e7 100644 --- a/apps/sim/tools/google_vault/create_matters.ts +++ b/apps/sim/tools/google_vault/create_matters.ts @@ -1,13 +1,7 @@ +import type { GoogleVaultCreateMattersParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' import type { ToolConfig } from '@/tools/types' -export interface GoogleVaultCreateMattersParams { - accessToken: string - name: string - description?: string -} - -// matters.create -// POST https://vault.googleapis.com/v1/matters export const createMattersTool: ToolConfig = { id: 'create_matters', name: 'Vault Create Matter', @@ -38,7 +32,8 @@ export const createMattersTool: ToolConfig = { transformResponse: async (response: Response) => { const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to create matter') + const errorMessage = data.error?.message || 'Failed to create matter' + throw new Error(enhanceGoogleVaultError(errorMessage)) } return { success: true, output: { matter: data } } }, diff --git a/apps/sim/tools/google_vault/create_matters_export.ts b/apps/sim/tools/google_vault/create_matters_export.ts index 3f443ce6d6..d20432eb6e 100644 --- a/apps/sim/tools/google_vault/create_matters_export.ts +++ b/apps/sim/tools/google_vault/create_matters_export.ts @@ -1,8 +1,7 @@ import type { GoogleVaultCreateMattersExportParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' import type { ToolConfig } from '@/tools/types' -// matters.exports.create -// POST https://vault.googleapis.com/v1/matters/{matterId}/exports export const createMattersExportTool: ToolConfig = { id: 'create_matters_export', name: 'Vault Create Export (by Matter)', @@ -64,7 +63,6 @@ export const createMattersExportTool: ToolConfig { - // Handle accountEmails - can be string (comma-separated) or array let emails: string[] = [] if (params.accountEmails) { if (Array.isArray(params.accountEmails)) { @@ -106,7 +104,8 @@ export const createMattersExportTool: ToolConfig { const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to create export') + const errorMessage = data.error?.message || 'Failed to create export' + throw new Error(enhanceGoogleVaultError(errorMessage)) } return { success: true, output: { export: data } } }, diff --git a/apps/sim/tools/google_vault/create_matters_holds.ts b/apps/sim/tools/google_vault/create_matters_holds.ts index 8c464edb4f..021f50101a 100644 --- a/apps/sim/tools/google_vault/create_matters_holds.ts +++ b/apps/sim/tools/google_vault/create_matters_holds.ts @@ -1,8 +1,7 @@ import type { GoogleVaultCreateMattersHoldsParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' import type { ToolConfig } from '@/tools/types' -// matters.holds.create -// POST https://vault.googleapis.com/v1/matters/{matterId}/holds export const createMattersHoldsTool: ToolConfig = { id: 'create_matters_holds', name: 'Vault Create Hold (by Matter)', @@ -36,7 +35,6 @@ export const createMattersHoldsTool: ToolConfig { - // Build Hold body. One of accounts or orgUnit must be provided. const body: any = { name: params.holdName, corpus: params.corpus, } - // Handle accountEmails - can be string (comma-separated) or array let emails: string[] = [] if (params.accountEmails) { if (Array.isArray(params.accountEmails)) { @@ -92,13 +87,11 @@ export const createMattersHoldsTool: ToolConfig 0) { - // Google Vault expects HeldAccount objects with 'email' or 'accountId'. Use 'email' here. body.accounts = emails.map((email: string) => ({ email })) } else if (params.orgUnitId) { body.orgUnit = { orgUnitId: params.orgUnitId } } - // Build corpus-specific query for date filtering if (params.corpus === 'MAIL' || params.corpus === 'GROUPS') { const hasQueryParams = params.terms || params.startTime || params.endTime if (hasQueryParams) { @@ -124,7 +117,8 @@ export const createMattersHoldsTool: ToolConfig { const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to create hold') + const errorMessage = data.error?.message || 'Failed to create hold' + throw new Error(enhanceGoogleVaultError(errorMessage)) } return { success: true, output: { hold: data } } }, diff --git a/apps/sim/tools/google_vault/download_export_file.ts b/apps/sim/tools/google_vault/download_export_file.ts index e63bd71cd9..ce16dcdcc2 100644 --- a/apps/sim/tools/google_vault/download_export_file.ts +++ b/apps/sim/tools/google_vault/download_export_file.ts @@ -1,17 +1,8 @@ -import { createLogger } from '@sim/logger' +import type { GoogleVaultDownloadExportFileParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' import type { ToolConfig } from '@/tools/types' -const logger = createLogger('GoogleVaultDownloadExportFileTool') - -interface DownloadParams { - accessToken: string - matterId: string - bucketName: string - objectName: string - fileName?: string -} - -export const downloadExportFileTool: ToolConfig = { +export const downloadExportFileTool: ToolConfig = { id: 'google_vault_download_export_file', name: 'Vault Download Export File', description: 'Download a single file from a Google Vault export (GCS object)', @@ -34,17 +25,15 @@ export const downloadExportFileTool: ToolConfig = { url: (params) => { const bucket = encodeURIComponent(params.bucketName) const object = encodeURIComponent(params.objectName) - // Use GCS media endpoint directly; framework will prefetch token and inject accessToken return `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}?alt=media` }, method: 'GET', headers: (params) => ({ - // Access token is injected by the tools framework when 'credential' is present Authorization: `Bearer ${params.accessToken}`, }), }, - transformResponse: async (response: Response, params?: DownloadParams) => { + transformResponse: async (response: Response, params?: GoogleVaultDownloadExportFileParams) => { if (!response.ok) { let details: any try { @@ -57,10 +46,11 @@ export const downloadExportFileTool: ToolConfig = { details = undefined } } - throw new Error(details?.error || `Failed to download Vault export file (${response.status})`) + const errorMessage = + details?.error || `Failed to download Vault export file (${response.status})` + throw new Error(enhanceGoogleVaultError(errorMessage)) } - // Since we're just doing a HEAD request to verify access, we need to fetch the actual file if (!params?.accessToken || !params?.bucketName || !params?.objectName) { throw new Error('Missing required parameters for download') } @@ -69,7 +59,6 @@ export const downloadExportFileTool: ToolConfig = { const object = encodeURIComponent(params.objectName) const downloadUrl = `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}?alt=media` - // Fetch the actual file content const downloadResponse = await fetch(downloadUrl, { method: 'GET', headers: { @@ -79,7 +68,8 @@ export const downloadExportFileTool: ToolConfig = { if (!downloadResponse.ok) { const errorText = await downloadResponse.text().catch(() => '') - throw new Error(`Failed to download file: ${errorText || downloadResponse.statusText}`) + const errorMessage = `Failed to download file: ${errorText || downloadResponse.statusText}` + throw new Error(enhanceGoogleVaultError(errorMessage)) } const contentType = downloadResponse.headers.get('content-type') || 'application/octet-stream' @@ -104,7 +94,6 @@ export const downloadExportFileTool: ToolConfig = { } } - // Get the file as an array buffer and convert to Buffer const arrayBuffer = await downloadResponse.arrayBuffer() const buffer = Buffer.from(arrayBuffer) diff --git a/apps/sim/tools/google_vault/list_matters.ts b/apps/sim/tools/google_vault/list_matters.ts index d5182aacc2..9edbd80564 100644 --- a/apps/sim/tools/google_vault/list_matters.ts +++ b/apps/sim/tools/google_vault/list_matters.ts @@ -1,12 +1,7 @@ +import type { GoogleVaultListMattersParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' import type { ToolConfig } from '@/tools/types' -export interface GoogleVaultListMattersParams { - accessToken: string - pageSize?: number - pageToken?: string - matterId?: string // Optional get for a specific matter -} - export const listMattersTool: ToolConfig = { id: 'list_matters', name: 'Vault List Matters', @@ -47,7 +42,8 @@ export const listMattersTool: ToolConfig = { transformResponse: async (response: Response, params?: GoogleVaultListMattersParams) => { const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to list matters') + const errorMessage = data.error?.message || 'Failed to list matters' + throw new Error(enhanceGoogleVaultError(errorMessage)) } if (params?.matterId) { return { success: true, output: { matter: data } } diff --git a/apps/sim/tools/google_vault/list_matters_export.ts b/apps/sim/tools/google_vault/list_matters_export.ts index 539ccd11d2..fbfcfd1092 100644 --- a/apps/sim/tools/google_vault/list_matters_export.ts +++ b/apps/sim/tools/google_vault/list_matters_export.ts @@ -1,4 +1,5 @@ import type { GoogleVaultListMattersExportParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' import type { ToolConfig } from '@/tools/types' export const listMattersExportTool: ToolConfig = { @@ -42,7 +43,8 @@ export const listMattersExportTool: ToolConfig { const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to list exports') + const errorMessage = data.error?.message || 'Failed to list exports' + throw new Error(enhanceGoogleVaultError(errorMessage)) } if (params?.exportId) { return { success: true, output: { export: data } } diff --git a/apps/sim/tools/google_vault/list_matters_holds.ts b/apps/sim/tools/google_vault/list_matters_holds.ts index 6f7a90cdce..f0134a4afb 100644 --- a/apps/sim/tools/google_vault/list_matters_holds.ts +++ b/apps/sim/tools/google_vault/list_matters_holds.ts @@ -1,4 +1,5 @@ import type { GoogleVaultListMattersHoldsParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' import type { ToolConfig } from '@/tools/types' export const listMattersHoldsTool: ToolConfig = { @@ -42,7 +43,8 @@ export const listMattersHoldsTool: ToolConfig transformResponse: async (response: Response, params?: GoogleVaultListMattersHoldsParams) => { const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to list holds') + const errorMessage = data.error?.message || 'Failed to list holds' + throw new Error(enhanceGoogleVaultError(errorMessage)) } if (params?.holdId) { return { success: true, output: { hold: data } } diff --git a/apps/sim/tools/google_vault/types.ts b/apps/sim/tools/google_vault/types.ts index 344bb02eaf..a94ff39819 100644 --- a/apps/sim/tools/google_vault/types.ts +++ b/apps/sim/tools/google_vault/types.ts @@ -5,11 +5,31 @@ export interface GoogleVaultCommonParams { matterId: string } -// Exports +export interface GoogleVaultCreateMattersParams { + accessToken: string + name: string + description?: string +} + +export interface GoogleVaultListMattersParams { + accessToken: string + pageSize?: number + pageToken?: string + matterId?: string +} + +export interface GoogleVaultDownloadExportFileParams { + accessToken: string + matterId: string + bucketName: string + objectName: string + fileName?: string +} + export interface GoogleVaultCreateMattersExportParams extends GoogleVaultCommonParams { exportName: string corpus: GoogleVaultCorpus - accountEmails?: string // Comma-separated list or array handled in the tool + accountEmails?: string orgUnitId?: string terms?: string startTime?: string @@ -20,15 +40,13 @@ export interface GoogleVaultCreateMattersExportParams extends GoogleVaultCommonP export interface GoogleVaultListMattersExportParams extends GoogleVaultCommonParams { pageSize?: number pageToken?: string - exportId?: string // Short input to fetch a specific export + exportId?: string } export interface GoogleVaultListMattersExportResponse extends ToolResponse { output: any } -// Holds -// Simplified: default to BASIC_HOLD by omission in requests export type GoogleVaultHoldView = 'BASIC_HOLD' | 'FULL_HOLD' export type GoogleVaultCorpus = 'MAIL' | 'DRIVE' | 'GROUPS' | 'HANGOUTS_CHAT' | 'VOICE' @@ -47,7 +65,7 @@ export interface GoogleVaultCreateMattersHoldsParams extends GoogleVaultCommonPa export interface GoogleVaultListMattersHoldsParams extends GoogleVaultCommonParams { pageSize?: number pageToken?: string - holdId?: string // Short input to fetch a specific hold + holdId?: string } export interface GoogleVaultListMattersHoldsResponse extends ToolResponse { diff --git a/apps/sim/tools/google_vault/utils.ts b/apps/sim/tools/google_vault/utils.ts new file mode 100644 index 0000000000..6fdd40ebee --- /dev/null +++ b/apps/sim/tools/google_vault/utils.ts @@ -0,0 +1,41 @@ +/** + * Google Vault Error Enhancement Utilities + * + * Provides user-friendly error messages for common Google Vault authentication + * and credential issues, particularly RAPT (reauthentication policy) errors. + */ + +/** + * Detects if an error message indicates a credential/reauthentication issue + */ +function isCredentialRefreshError(errorMessage: string): boolean { + const lowerMessage = errorMessage.toLowerCase() + return ( + lowerMessage.includes('invalid_rapt') || + lowerMessage.includes('reauth related error') || + (lowerMessage.includes('invalid_grant') && lowerMessage.includes('rapt')) || + lowerMessage.includes('failed to refresh token') || + (lowerMessage.includes('failed to fetch access token') && lowerMessage.includes('401')) + ) +} + +/** + * Enhances Google Vault error messages with actionable guidance + * + * For credential/reauthentication errors (RAPT errors), provides specific + * instructions for resolving the issue through Google Admin Console settings. + */ +export function enhanceGoogleVaultError(errorMessage: string): string { + if (isCredentialRefreshError(errorMessage)) { + return ( + `Google Vault authentication failed (likely due to reauthentication policy). ` + + `To resolve this, try disconnecting and reconnecting your Google Vault credential ` + + `in the Credentials settings. If the issue persists, ask your Google Workspace ` + + `administrator to disable "Reauthentication policy" for Sim Studio in the Google ` + + `Admin Console (Security > Access and data control > Context-Aware Access > ` + + `Reauthentication policy), or exempt Sim Studio from reauthentication requirements. ` + + `Learn more: https://support.google.com/a/answer/9368756` + ) + } + return errorMessage +}