new error throw and improvement

This commit is contained in:
aadamgough
2026-01-08 15:11:50 -08:00
parent fdac4314d2
commit ff1af41b37
7 changed files with 222 additions and 23 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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' },

View File

@@ -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
}
}

View File

@@ -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,
}

View File

@@ -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
},
},

View File

@@ -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 {