mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
new error throw and improvement
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,24 @@ export const createMattersExportTool: ToolConfig<GoogleVaultCreateMattersExportP
|
||||
visibility: 'user-only',
|
||||
description: 'Organization unit ID to scope export (alternative to emails)',
|
||||
},
|
||||
startTime: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Start time for date filtering (ISO 8601 format, e.g., 2024-01-01T00:00:00Z)',
|
||||
},
|
||||
endTime: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'End time for date filtering (ISO 8601 format, e.g., 2024-12-31T23:59:59Z)',
|
||||
},
|
||||
terms: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Search query terms to filter exported content',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -75,7 +93,6 @@ export const createMattersExportTool: ToolConfig<GoogleVaultCreateMattersExportP
|
||||
terms: params.terms || undefined,
|
||||
startTime: params.startTime || undefined,
|
||||
endTime: params.endTime || undefined,
|
||||
timeZone: params.timeZone || undefined,
|
||||
...scope,
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,32 @@ export const createMattersHoldsTool: ToolConfig<GoogleVaultCreateMattersHoldsPar
|
||||
visibility: 'user-only',
|
||||
description: 'Organization unit ID to put on hold (alternative to accounts)',
|
||||
},
|
||||
// Query parameters for MAIL and GROUPS corpus (date filtering)
|
||||
terms: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Search terms to filter held content (for MAIL and GROUPS corpus)',
|
||||
},
|
||||
startTime: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Start time for date filtering (ISO 8601 format, for MAIL and GROUPS corpus)',
|
||||
},
|
||||
endTime: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'End time for date filtering (ISO 8601 format, for MAIL and GROUPS corpus)',
|
||||
},
|
||||
// Drive-specific option
|
||||
includeSharedDrives: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Include files in shared drives (for DRIVE corpus)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -72,6 +98,25 @@ export const createMattersHoldsTool: ToolConfig<GoogleVaultCreateMattersHoldsPar
|
||||
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) {
|
||||
const queryObj: any = {}
|
||||
if (params.terms) queryObj.terms = params.terms
|
||||
if (params.startTime) queryObj.startTime = params.startTime
|
||||
if (params.endTime) queryObj.endTime = params.endTime
|
||||
|
||||
if (params.corpus === 'MAIL') {
|
||||
body.query = { mailQuery: queryObj }
|
||||
} else {
|
||||
body.query = { groupsQuery: queryObj }
|
||||
}
|
||||
}
|
||||
} else if (params.corpus === 'DRIVE' && params.includeSharedDrives) {
|
||||
body.query = { driveQuery: { includeSharedDriveFiles: params.includeSharedDrives } }
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ export interface GoogleVaultCreateMattersExportParams extends GoogleVaultCommonP
|
||||
terms?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
timeZone?: string
|
||||
includeSharedDrives?: boolean
|
||||
}
|
||||
|
||||
@@ -39,6 +38,12 @@ export interface GoogleVaultCreateMattersHoldsParams extends GoogleVaultCommonPa
|
||||
corpus: GoogleVaultCorpus
|
||||
accountEmails?: string // Comma-separated list or array handled in the tool
|
||||
orgUnitId?: string
|
||||
// Query parameters for MAIL and GROUPS corpus (date filtering)
|
||||
terms?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
// Drive-specific option
|
||||
includeSharedDrives?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleVaultListMattersHoldsParams extends GoogleVaultCommonParams {
|
||||
|
||||
Reference in New Issue
Block a user