Skip to content

Commit ff1af41

Browse files
author
aadamgough
committed
new error throw and improvement
1 parent fdac431 commit ff1af41

File tree

7 files changed

+222
-23
lines changed

7 files changed

+222
-23
lines changed

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,12 @@ export async function POST(request: NextRequest) {
129129
{ status: 200 }
130130
)
131131
} catch (error) {
132-
logger.error(`[${requestId}] Failed to refresh access token:`, error)
133-
return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 })
132+
const errorMessage = error instanceof Error ? error.message : 'Failed to refresh access token'
133+
logger.error(`[${requestId}] Failed to refresh access token:`, {
134+
error: errorMessage,
135+
stack: error instanceof Error ? error.stack : undefined,
136+
})
137+
return NextResponse.json({ error: errorMessage }, { status: 401 })
134138
}
135139
} catch (error) {
136140
logger.error(`[${requestId}] Error getting access token`, error)
@@ -207,8 +211,13 @@ export async function GET(request: NextRequest) {
207211
},
208212
{ status: 200 }
209213
)
210-
} catch (_error) {
211-
return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 })
214+
} catch (error) {
215+
const errorMessage = error instanceof Error ? error.message : 'Failed to refresh access token'
216+
logger.error(`[${requestId}] Failed to refresh access token:`, {
217+
error: errorMessage,
218+
stack: error instanceof Error ? error.stack : undefined,
219+
})
220+
return NextResponse.json({ error: errorMessage }, { status: 401 })
212221
}
213222
} catch (error) {
214223
logger.error(`[${requestId}] Error fetching access token`, error)

apps/sim/app/api/auth/oauth/utils.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,14 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
135135
const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!)
136136

137137
if (!refreshResult) {
138-
logger.error(`Failed to refresh token for user ${userId}, provider ${providerId}`, {
139-
providerId,
140-
userId,
141-
hasRefreshToken: !!credential.refreshToken,
142-
})
138+
logger.error(
139+
`Failed to refresh token for user ${userId}, provider ${providerId} - no result returned`,
140+
{
141+
providerId,
142+
userId,
143+
hasRefreshToken: !!credential.refreshToken,
144+
}
145+
)
143146
return null
144147
}
145148

@@ -170,7 +173,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
170173
providerId,
171174
userId,
172175
})
173-
return null
176+
throw error
174177
}
175178
}
176179

@@ -221,12 +224,15 @@ export async function refreshAccessTokenIfNeeded(
221224
)
222225

223226
if (!refreshedToken) {
224-
logger.error(`[${requestId}] Failed to refresh token for credential: ${credentialId}`, {
225-
credentialId,
226-
providerId: credential.providerId,
227-
userId: credential.userId,
228-
hasRefreshToken: !!credential.refreshToken,
229-
})
227+
logger.error(
228+
`[${requestId}] Failed to refresh token for credential: ${credentialId} - no result returned`,
229+
{
230+
credentialId,
231+
providerId: credential.providerId,
232+
userId: credential.userId,
233+
hasRefreshToken: !!credential.refreshToken,
234+
}
235+
)
230236
return null
231237
}
232238

@@ -249,14 +255,15 @@ export async function refreshAccessTokenIfNeeded(
249255
logger.info(`[${requestId}] Successfully refreshed access token for credential`)
250256
return refreshedToken.accessToken
251257
} catch (error) {
258+
// Re-throw the error to propagate detailed error messages (e.g., session expiry instructions)
252259
logger.error(`[${requestId}] Error refreshing token for credential`, {
253260
error: error instanceof Error ? error.message : String(error),
254261
stack: error instanceof Error ? error.stack : undefined,
255262
providerId: credential.providerId,
256263
credentialId,
257264
userId: credential.userId,
258265
})
259-
return null
266+
throw error
260267
}
261268
} else if (!accessToken) {
262269
// We have no access token and either no refresh token or not eligible to refresh
@@ -292,8 +299,8 @@ export async function refreshTokenIfNeeded(
292299
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!)
293300

294301
if (!refreshResult) {
295-
logger.error(`[${requestId}] Failed to refresh token for credential`)
296-
throw new Error('Failed to refresh token')
302+
logger.error(`[${requestId}] Failed to refresh token for credential - no result returned`)
303+
throw new Error('Failed to refresh token: no result returned from provider')
297304
}
298305

299306
const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult

apps/sim/blocks/blocks/google_vault.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,90 @@ Return ONLY the hold name - no explanations, no quotes, no extra text.`,
159159
placeholder: 'Org Unit ID (alternative to emails)',
160160
condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] },
161161
},
162+
// Date filtering for exports and holds (holds only support MAIL and GROUPS corpus)
163+
{
164+
id: 'startTime',
165+
title: 'Start Time',
166+
type: 'short-input',
167+
placeholder: 'YYYY-MM-DDTHH:mm:ssZ',
168+
condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] },
169+
wandConfig: {
170+
enabled: true,
171+
prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering.
172+
The timestamp should be in the format: YYYY-MM-DDTHH:mm:ssZ (UTC timezone).
173+
Note: Google Vault rounds times to 12 AM on the specified date.
174+
Examples:
175+
- "yesterday" -> Calculate yesterday's date at 00:00:00Z
176+
- "last week" -> Calculate 7 days ago at 00:00:00Z
177+
- "beginning of this month" -> Calculate the 1st of current month at 00:00:00Z
178+
- "January 1, 2024" -> 2024-01-01T00:00:00Z
179+
180+
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
181+
placeholder: 'Describe the start date (e.g., "last month", "January 1, 2024")...',
182+
generationType: 'timestamp',
183+
},
184+
},
185+
{
186+
id: 'endTime',
187+
title: 'End Time',
188+
type: 'short-input',
189+
placeholder: 'YYYY-MM-DDTHH:mm:ssZ',
190+
condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] },
191+
wandConfig: {
192+
enabled: true,
193+
prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering.
194+
The timestamp should be in the format: YYYY-MM-DDTHH:mm:ssZ (UTC timezone).
195+
Note: Google Vault rounds times to 12 AM on the specified date.
196+
Examples:
197+
- "now" -> Current timestamp
198+
- "today" -> Today's date at 23:59:59Z
199+
- "end of last month" -> Last day of previous month at 23:59:59Z
200+
- "December 31, 2024" -> 2024-12-31T23:59:59Z
201+
202+
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
203+
placeholder: 'Describe the end date (e.g., "today", "end of last quarter")...',
204+
generationType: 'timestamp',
205+
},
206+
},
207+
{
208+
id: 'terms',
209+
title: 'Search Terms',
210+
type: 'long-input',
211+
placeholder: 'Enter search query (e.g., from:user@example.com subject:confidential)',
212+
condition: { field: 'operation', value: ['create_matters_export', 'create_matters_holds'] },
213+
wandConfig: {
214+
enabled: true,
215+
prompt: `Generate a Google Vault search query based on the user's description.
216+
The query can use Gmail-style search operators for MAIL corpus:
217+
- from:user@example.com - emails from specific sender
218+
- to:user@example.com - emails to specific recipient
219+
- subject:keyword - emails with keyword in subject
220+
- has:attachment - emails with attachments
221+
- filename:pdf - emails with PDF attachments
222+
- before:YYYY/MM/DD - emails before date
223+
- after:YYYY/MM/DD - emails after date
224+
225+
For DRIVE corpus, use Drive search operators:
226+
- owner:user@example.com - files owned by user
227+
- type:document - specific file types
228+
229+
For holds, date filtering only works with MAIL and GROUPS corpus.
230+
231+
Return ONLY the search query - no explanations, no quotes, no extra text.`,
232+
placeholder: 'Describe what content to search for...',
233+
},
234+
},
235+
// Drive-specific option for holds
236+
{
237+
id: 'includeSharedDrives',
238+
title: 'Include Shared Drives',
239+
type: 'switch',
240+
condition: {
241+
field: 'operation',
242+
value: 'create_matters_holds',
243+
and: { field: 'corpus', value: 'DRIVE' },
244+
},
245+
},
162246
{
163247
id: 'exportId',
164248
title: 'Export ID',
@@ -296,9 +380,16 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
296380
corpus: { type: 'string', description: 'Data corpus (MAIL, DRIVE, GROUPS, etc.)' },
297381
accountEmails: { type: 'string', description: 'Comma-separated account emails' },
298382
orgUnitId: { type: 'string', description: 'Organization unit ID' },
383+
startTime: { type: 'string', description: 'Start time for date filtering (ISO 8601 format)' },
384+
endTime: { type: 'string', description: 'End time for date filtering (ISO 8601 format)' },
385+
terms: { type: 'string', description: 'Search query terms' },
299386

300387
// Create hold inputs
301388
holdName: { type: 'string', description: 'Name for the hold' },
389+
includeSharedDrives: {
390+
type: 'boolean',
391+
description: 'Include files in shared drives (for DRIVE corpus holds)',
392+
},
302393

303394
// Download export file inputs
304395
bucketName: { type: 'string', description: 'GCS bucket name from export' },

apps/sim/lib/oauth/oauth.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,7 @@ export async function refreshOAuthToken(
11711171

11721172
if (!response.ok) {
11731173
const errorText = await response.text()
1174-
let errorData = errorText
1174+
let errorData: any = errorText
11751175

11761176
try {
11771177
errorData = JSON.parse(errorText)
@@ -1191,6 +1191,29 @@ export async function refreshOAuthToken(
11911191
hasRefreshToken: !!refreshToken,
11921192
refreshTokenPrefix: refreshToken ? `${refreshToken.substring(0, 10)}...` : 'none',
11931193
})
1194+
1195+
// Check for Google Workspace session control errors (RAPT - Reauthentication Policy Token)
1196+
// This occurs when the organization enforces periodic re-authentication
1197+
if (
1198+
typeof errorData === 'object' &&
1199+
(errorData.error_subtype === 'invalid_rapt' ||
1200+
errorData.error_description?.includes('reauth related error'))
1201+
) {
1202+
throw new Error(
1203+
`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".`
1204+
)
1205+
}
1206+
1207+
if (
1208+
typeof errorData === 'object' &&
1209+
errorData.error === 'invalid_grant' &&
1210+
!errorData.error_subtype
1211+
) {
1212+
throw new Error(
1213+
`Access has been revoked or the refresh token is no longer valid. Please reconnect your ${providerId} account.`
1214+
)
1215+
}
1216+
11941217
throw new Error(`Failed to refresh token: ${response.status} ${errorText}`)
11951218
}
11961219

@@ -1224,6 +1247,8 @@ export async function refreshOAuthToken(
12241247
}
12251248
} catch (error) {
12261249
logger.error('Error refreshing token:', { error })
1227-
return null
1250+
// Re-throw specific errors so they propagate with their detailed messages
1251+
// Only return null for truly unexpected errors without useful messages
1252+
throw error
12281253
}
12291254
}

apps/sim/tools/google_vault/create_matters_export.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ export const createMattersExportTool: ToolConfig<GoogleVaultCreateMattersExportP
3636
visibility: 'user-only',
3737
description: 'Organization unit ID to scope export (alternative to emails)',
3838
},
39+
startTime: {
40+
type: 'string',
41+
required: false,
42+
visibility: 'user-only',
43+
description: 'Start time for date filtering (ISO 8601 format, e.g., 2024-01-01T00:00:00Z)',
44+
},
45+
endTime: {
46+
type: 'string',
47+
required: false,
48+
visibility: 'user-only',
49+
description: 'End time for date filtering (ISO 8601 format, e.g., 2024-12-31T23:59:59Z)',
50+
},
51+
terms: {
52+
type: 'string',
53+
required: false,
54+
visibility: 'user-only',
55+
description: 'Search query terms to filter exported content',
56+
},
3957
},
4058

4159
request: {
@@ -75,7 +93,6 @@ export const createMattersExportTool: ToolConfig<GoogleVaultCreateMattersExportP
7593
terms: params.terms || undefined,
7694
startTime: params.startTime || undefined,
7795
endTime: params.endTime || undefined,
78-
timeZone: params.timeZone || undefined,
7996
...scope,
8097
}
8198

apps/sim/tools/google_vault/create_matters_holds.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ export const createMattersHoldsTool: ToolConfig<GoogleVaultCreateMattersHoldsPar
3636
visibility: 'user-only',
3737
description: 'Organization unit ID to put on hold (alternative to accounts)',
3838
},
39+
// Query parameters for MAIL and GROUPS corpus (date filtering)
40+
terms: {
41+
type: 'string',
42+
required: false,
43+
visibility: 'user-only',
44+
description: 'Search terms to filter held content (for MAIL and GROUPS corpus)',
45+
},
46+
startTime: {
47+
type: 'string',
48+
required: false,
49+
visibility: 'user-only',
50+
description: 'Start time for date filtering (ISO 8601 format, for MAIL and GROUPS corpus)',
51+
},
52+
endTime: {
53+
type: 'string',
54+
required: false,
55+
visibility: 'user-only',
56+
description: 'End time for date filtering (ISO 8601 format, for MAIL and GROUPS corpus)',
57+
},
58+
// Drive-specific option
59+
includeSharedDrives: {
60+
type: 'boolean',
61+
required: false,
62+
visibility: 'user-only',
63+
description: 'Include files in shared drives (for DRIVE corpus)',
64+
},
3965
},
4066

4167
request: {
@@ -72,6 +98,25 @@ export const createMattersHoldsTool: ToolConfig<GoogleVaultCreateMattersHoldsPar
7298
body.orgUnit = { orgUnitId: params.orgUnitId }
7399
}
74100

101+
// Build corpus-specific query for date filtering
102+
if (params.corpus === 'MAIL' || params.corpus === 'GROUPS') {
103+
const hasQueryParams = params.terms || params.startTime || params.endTime
104+
if (hasQueryParams) {
105+
const queryObj: any = {}
106+
if (params.terms) queryObj.terms = params.terms
107+
if (params.startTime) queryObj.startTime = params.startTime
108+
if (params.endTime) queryObj.endTime = params.endTime
109+
110+
if (params.corpus === 'MAIL') {
111+
body.query = { mailQuery: queryObj }
112+
} else {
113+
body.query = { groupsQuery: queryObj }
114+
}
115+
}
116+
} else if (params.corpus === 'DRIVE' && params.includeSharedDrives) {
117+
body.query = { driveQuery: { includeSharedDriveFiles: params.includeSharedDrives } }
118+
}
119+
75120
return body
76121
},
77122
},

apps/sim/tools/google_vault/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export interface GoogleVaultCreateMattersExportParams extends GoogleVaultCommonP
1414
terms?: string
1515
startTime?: string
1616
endTime?: string
17-
timeZone?: string
1817
includeSharedDrives?: boolean
1918
}
2019

@@ -39,6 +38,12 @@ export interface GoogleVaultCreateMattersHoldsParams extends GoogleVaultCommonPa
3938
corpus: GoogleVaultCorpus
4039
accountEmails?: string // Comma-separated list or array handled in the tool
4140
orgUnitId?: string
41+
// Query parameters for MAIL and GROUPS corpus (date filtering)
42+
terms?: string
43+
startTime?: string
44+
endTime?: string
45+
// Drive-specific option
46+
includeSharedDrives?: boolean
4247
}
4348

4449
export interface GoogleVaultListMattersHoldsParams extends GoogleVaultCommonParams {

0 commit comments

Comments
 (0)