* Returns `null` and clears the column when decryption fails (e.g. key rotation) * so the next call triggers a fresh OAuth flow instead of a 500.
( rowId: string, column: 'tokens' | 'clientInformation' | 'codeVerifier', encrypted: string, decode: (decrypted: string) => T )
| 49 | * so the next call triggers a fresh OAuth flow instead of a 500. |
| 50 | */ |
| 51 | async function safeDecrypt<T>( |
| 52 | rowId: string, |
| 53 | column: 'tokens' | 'clientInformation' | 'codeVerifier', |
| 54 | encrypted: string, |
| 55 | decode: (decrypted: string) => T |
| 56 | ): Promise<T | null> { |
| 57 | try { |
| 58 | const { decrypted } = await decryptSecret(encrypted) |
| 59 | return decode(decrypted) |
| 60 | } catch (error) { |
| 61 | logger.warn(`Failed to decrypt ${column} for OAuth row ${rowId}; clearing column`, { |
| 62 | error: toError(error).message, |
| 63 | }) |
| 64 | await db |
| 65 | .update(mcpServerOauth) |
| 66 | .set({ [column]: null, updatedAt: new Date() }) |
| 67 | .where(eq(mcpServerOauth.id, rowId)) |
| 68 | return null |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | export async function getOrCreateOauthRow(params: { |
| 73 | mcpServerId: string |
no test coverage detected