mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
reformatted to PAT from oauth
This commit is contained in:
@@ -69,7 +69,6 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
|
||||
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
||||
accountId: account.accountId,
|
||||
providerId: account.providerId,
|
||||
password: account.password, // Include password field for Snowflake OAuth credentials
|
||||
})
|
||||
.from(account)
|
||||
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
|
||||
@@ -96,25 +95,8 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
|
||||
)
|
||||
|
||||
try {
|
||||
// Extract account URL and OAuth credentials for Snowflake
|
||||
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
|
||||
if (providerId === 'snowflake' && credential.accountId) {
|
||||
metadata = { accountUrl: credential.accountId }
|
||||
|
||||
// Extract clientId and clientSecret from the password field (stored as JSON)
|
||||
if (credential.password) {
|
||||
try {
|
||||
const oauthCredentials = JSON.parse(credential.password)
|
||||
metadata.clientId = oauthCredentials.clientId
|
||||
metadata.clientSecret = oauthCredentials.clientSecret
|
||||
} catch (e) {
|
||||
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the existing refreshOAuthToken function
|
||||
const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!, metadata)
|
||||
const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!)
|
||||
|
||||
if (!refreshResult) {
|
||||
logger.error(`Failed to refresh token for user ${userId}, provider ${providerId}`, {
|
||||
@@ -197,27 +179,9 @@ export async function refreshAccessTokenIfNeeded(
|
||||
if (shouldRefresh) {
|
||||
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
|
||||
try {
|
||||
// Extract account URL and OAuth credentials for Snowflake
|
||||
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
|
||||
if (credential.providerId === 'snowflake' && credential.accountId) {
|
||||
metadata = { accountUrl: credential.accountId }
|
||||
|
||||
// Extract clientId and clientSecret from the password field (stored as JSON)
|
||||
if (credential.password) {
|
||||
try {
|
||||
const oauthCredentials = JSON.parse(credential.password)
|
||||
metadata.clientId = oauthCredentials.clientId
|
||||
metadata.clientSecret = oauthCredentials.clientSecret
|
||||
} catch (e) {
|
||||
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refreshedToken = await refreshOAuthToken(
|
||||
credential.providerId,
|
||||
credential.refreshToken!,
|
||||
metadata
|
||||
credential.refreshToken!
|
||||
)
|
||||
|
||||
if (!refreshedToken) {
|
||||
@@ -289,28 +253,7 @@ export async function refreshTokenIfNeeded(
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract account URL and OAuth credentials for Snowflake
|
||||
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
|
||||
if (credential.providerId === 'snowflake' && credential.accountId) {
|
||||
metadata = { accountUrl: credential.accountId }
|
||||
|
||||
// Extract clientId and clientSecret from the password field (stored as JSON)
|
||||
if (credential.password) {
|
||||
try {
|
||||
const oauthCredentials = JSON.parse(credential.password)
|
||||
metadata.clientId = oauthCredentials.clientId
|
||||
metadata.clientSecret = oauthCredentials.clientSecret
|
||||
} catch (e) {
|
||||
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refreshResult = await refreshOAuthToken(
|
||||
credential.providerId,
|
||||
credential.refreshToken!,
|
||||
metadata
|
||||
)
|
||||
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!)
|
||||
|
||||
if (!refreshResult) {
|
||||
logger.error(`[${requestId}] Failed to refresh token for credential`)
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateCodeChallenge, generateCodeVerifier } from '@/lib/oauth/pkce'
|
||||
import { getBaseUrl } from '@/lib/urls/utils'
|
||||
|
||||
const logger = createLogger('SnowflakeAuthorize')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Initiates Snowflake OAuth flow
|
||||
* Expects credentials to be posted in the request body (accountUrl, clientId, clientSecret)
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn('Unauthorized Snowflake OAuth attempt')
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { accountUrl, clientId, clientSecret } = body
|
||||
|
||||
if (!accountUrl || !clientId || !clientSecret) {
|
||||
logger.error('Missing required Snowflake OAuth parameters', {
|
||||
hasAccountUrl: !!accountUrl,
|
||||
hasClientId: !!clientId,
|
||||
hasClientSecret: !!clientSecret,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'accountUrl, clientId, and clientSecret are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Parse and clean the account URL
|
||||
let cleanAccountUrl = accountUrl.replace(/^https?:\/\//, '')
|
||||
cleanAccountUrl = cleanAccountUrl.replace(/\/$/, '')
|
||||
if (!cleanAccountUrl.includes('snowflakecomputing.com')) {
|
||||
cleanAccountUrl = `${cleanAccountUrl}.snowflakecomputing.com`
|
||||
}
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const redirectUri = `${baseUrl}/api/auth/snowflake/callback`
|
||||
|
||||
// Generate PKCE values
|
||||
const codeVerifier = generateCodeVerifier()
|
||||
const codeChallenge = await generateCodeChallenge(codeVerifier)
|
||||
|
||||
// Store user-provided credentials in the state (will be used in callback)
|
||||
const state = Buffer.from(
|
||||
JSON.stringify({
|
||||
userId: session.user.id,
|
||||
accountUrl: cleanAccountUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
timestamp: Date.now(),
|
||||
codeVerifier,
|
||||
})
|
||||
).toString('base64url')
|
||||
|
||||
// Construct Snowflake-specific authorization URL
|
||||
const authUrl = new URL(`https://${cleanAccountUrl}/oauth/authorize`)
|
||||
authUrl.searchParams.set('client_id', clientId)
|
||||
authUrl.searchParams.set('response_type', 'code')
|
||||
authUrl.searchParams.set('redirect_uri', redirectUri)
|
||||
// Add scope parameter to specify a safe role (not ACCOUNTADMIN or SECURITYADMIN)
|
||||
authUrl.searchParams.set('scope', 'refresh_token session:role:PUBLIC')
|
||||
authUrl.searchParams.set('state', state)
|
||||
// Add PKCE parameters for security and compatibility with OAUTH_ENFORCE_PKCE
|
||||
authUrl.searchParams.set('code_challenge', codeChallenge)
|
||||
authUrl.searchParams.set('code_challenge_method', 'S256')
|
||||
|
||||
logger.info('Initiating Snowflake OAuth flow with user-provided credentials (PKCE)', {
|
||||
userId: session.user.id,
|
||||
accountUrl: cleanAccountUrl,
|
||||
hasClientId: !!clientId,
|
||||
hasClientSecret: !!clientSecret,
|
||||
redirectUri,
|
||||
hasPkce: true,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
authUrl: authUrl.toString(),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error initiating Snowflake authorization:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBaseUrl } from '@/lib/urls/utils'
|
||||
import { db } from '@/../../packages/db'
|
||||
import { account } from '@/../../packages/db/schema'
|
||||
|
||||
const logger = createLogger('SnowflakeCallback')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Handles Snowflake OAuth callback
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn('Unauthorized Snowflake OAuth callback')
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=unauthorized`)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const code = searchParams.get('code')
|
||||
const state = searchParams.get('state')
|
||||
const error = searchParams.get('error')
|
||||
const errorDescription = searchParams.get('error_description')
|
||||
|
||||
// Handle OAuth errors
|
||||
if (error) {
|
||||
logger.error('Snowflake OAuth error', { error, errorDescription })
|
||||
return NextResponse.redirect(
|
||||
`${getBaseUrl()}/workspace?error=snowflake_${error}&description=${encodeURIComponent(errorDescription || '')}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
logger.error('Missing code or state in callback')
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_invalid_callback`)
|
||||
}
|
||||
|
||||
// Decode state to get account URL, credentials, and code verifier
|
||||
let stateData: {
|
||||
userId: string
|
||||
accountUrl: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
timestamp: number
|
||||
codeVerifier: string
|
||||
}
|
||||
|
||||
try {
|
||||
stateData = JSON.parse(Buffer.from(state, 'base64url').toString())
|
||||
logger.info('Decoded state successfully', {
|
||||
userId: stateData.userId,
|
||||
accountUrl: stateData.accountUrl,
|
||||
hasClientId: !!stateData.clientId,
|
||||
hasClientSecret: !!stateData.clientSecret,
|
||||
age: Date.now() - stateData.timestamp,
|
||||
hasCodeVerifier: !!stateData.codeVerifier,
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('Invalid state parameter', { error: e, state })
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_invalid_state`)
|
||||
}
|
||||
|
||||
// Verify the user matches
|
||||
if (stateData.userId !== session.user.id) {
|
||||
logger.error('User ID mismatch in state', {
|
||||
stateUserId: stateData.userId,
|
||||
sessionUserId: session.user.id,
|
||||
})
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_user_mismatch`)
|
||||
}
|
||||
|
||||
// Verify state is not too old (15 minutes)
|
||||
if (Date.now() - stateData.timestamp > 15 * 60 * 1000) {
|
||||
logger.error('State expired', {
|
||||
age: Date.now() - stateData.timestamp,
|
||||
})
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_state_expired`)
|
||||
}
|
||||
|
||||
// Use user-provided credentials from state
|
||||
const clientId = stateData.clientId
|
||||
const clientSecret = stateData.clientSecret
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
logger.error('Missing client credentials in state')
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_missing_credentials`)
|
||||
}
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
const tokenUrl = `https://${stateData.accountUrl}/oauth/token-request`
|
||||
const redirectUri = `${getBaseUrl()}/api/auth/snowflake/callback`
|
||||
|
||||
const tokenParams = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code_verifier: stateData.codeVerifier,
|
||||
})
|
||||
|
||||
logger.info('Exchanging authorization code for tokens (with PKCE)', {
|
||||
tokenUrl,
|
||||
redirectUri,
|
||||
clientId,
|
||||
hasCode: !!code,
|
||||
hasClientSecret: !!clientSecret,
|
||||
hasCodeVerifier: !!stateData.codeVerifier,
|
||||
paramsLength: tokenParams.toString().length,
|
||||
})
|
||||
|
||||
const tokenResponse = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: tokenParams.toString(),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text()
|
||||
logger.error('Failed to exchange code for token', {
|
||||
status: tokenResponse.status,
|
||||
statusText: tokenResponse.statusText,
|
||||
error: errorText,
|
||||
tokenUrl,
|
||||
redirectUri,
|
||||
})
|
||||
|
||||
// Try to parse error as JSON for better diagnostics
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
logger.error('Snowflake error details:', errorJson)
|
||||
} catch (e) {
|
||||
logger.error('Error text (not JSON):', errorText)
|
||||
}
|
||||
|
||||
return NextResponse.redirect(
|
||||
`${getBaseUrl()}/workspace?error=snowflake_token_exchange_failed&details=${encodeURIComponent(errorText)}`
|
||||
)
|
||||
}
|
||||
|
||||
const tokens = await tokenResponse.json()
|
||||
|
||||
logger.info('Token exchange for Snowflake successful', {
|
||||
hasAccessToken: !!tokens.access_token,
|
||||
hasRefreshToken: !!tokens.refresh_token,
|
||||
expiresIn: tokens.expires_in,
|
||||
scope: tokens.scope,
|
||||
})
|
||||
|
||||
if (!tokens.access_token) {
|
||||
logger.error('No access token in response', { tokens })
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_no_access_token`)
|
||||
}
|
||||
|
||||
// Store the account and tokens in the database
|
||||
const existing = await db.query.account.findFirst({
|
||||
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'snowflake')),
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
const expiresAt = tokens.expires_in
|
||||
? new Date(now.getTime() + tokens.expires_in * 1000)
|
||||
: new Date(now.getTime() + 10 * 60 * 1000) // Default 10 minutes
|
||||
|
||||
// Store user-provided OAuth credentials securely
|
||||
// We use the password field to store a JSON object with clientId and clientSecret
|
||||
// and idToken to store the accountUrl for easier retrieval
|
||||
const oauthCredentials = JSON.stringify({
|
||||
clientId: stateData.clientId,
|
||||
clientSecret: stateData.clientSecret,
|
||||
})
|
||||
|
||||
const accountData = {
|
||||
userId: session.user.id,
|
||||
providerId: 'snowflake',
|
||||
accountId: stateData.accountUrl, // Store the Snowflake account URL here
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token || null,
|
||||
idToken: stateData.accountUrl, // Store accountUrl for easier access
|
||||
password: oauthCredentials, // Store clientId and clientSecret as JSON
|
||||
accessTokenExpiresAt: expiresAt,
|
||||
scope: tokens.scope || null,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
await db.update(account).set(accountData).where(eq(account.id, existing.id))
|
||||
|
||||
logger.info('Updated existing Snowflake account', {
|
||||
userId: session.user.id,
|
||||
accountUrl: stateData.accountUrl,
|
||||
})
|
||||
} else {
|
||||
await db.insert(account).values({
|
||||
...accountData,
|
||||
id: `snowflake_${session.user.id}_${Date.now()}`,
|
||||
createdAt: now,
|
||||
})
|
||||
|
||||
logger.info('Created new Snowflake account', {
|
||||
userId: session.user.id,
|
||||
accountUrl: stateData.accountUrl,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?snowflake_connected=true`)
|
||||
} catch (error) {
|
||||
logger.error('Error in Snowflake callback:', error)
|
||||
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_callback_failed`)
|
||||
}
|
||||
}
|
||||
@@ -226,9 +226,6 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'webhooks:read': 'Read your Pipedrive webhooks',
|
||||
'webhooks:full': 'Full access to manage your Pipedrive webhooks',
|
||||
w_member_social: 'Access your LinkedIn profile',
|
||||
// Box scopes
|
||||
root_readwrite: 'Read and write all files and folders in your Box account',
|
||||
root_readonly: 'Read all files and folders in your Box account',
|
||||
// Shopify scopes (write_* implicitly includes read access)
|
||||
write_products: 'Read and manage your Shopify products',
|
||||
write_orders: 'Read and manage your Shopify orders',
|
||||
@@ -273,13 +270,6 @@ export function OAuthRequiredModal({
|
||||
serviceId,
|
||||
newScopes = [],
|
||||
}: OAuthRequiredModalProps) {
|
||||
const [snowflakeAccountUrl, setSnowflakeAccountUrl] = useState('')
|
||||
const [snowflakeClientId, setSnowflakeClientId] = useState('')
|
||||
const [snowflakeClientSecret, setSnowflakeClientSecret] = useState('')
|
||||
const [accountUrlError, setAccountUrlError] = useState('')
|
||||
const [clientIdError, setClientIdError] = useState('')
|
||||
const [clientSecretError, setClientSecretError] = useState('')
|
||||
|
||||
const effectiveServiceId = serviceId || getServiceIdFromScopes(provider, requiredScopes)
|
||||
const { baseProvider } = parseProvider(provider)
|
||||
const baseProviderConfig = OAUTH_PROVIDERS[baseProvider]
|
||||
@@ -310,67 +300,6 @@ export function OAuthRequiredModal({
|
||||
try {
|
||||
const providerId = getProviderIdFromServiceId(effectiveServiceId)
|
||||
|
||||
// Special handling for Snowflake - requires account URL, client ID, and client secret
|
||||
if (providerId === 'snowflake') {
|
||||
let hasError = false
|
||||
|
||||
if (!snowflakeAccountUrl.trim()) {
|
||||
setAccountUrlError('Account URL is required')
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (!snowflakeClientId.trim()) {
|
||||
setClientIdError('Client ID is required')
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (!snowflakeClientSecret.trim()) {
|
||||
setClientSecretError('Client Secret is required')
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return
|
||||
}
|
||||
|
||||
onClose()
|
||||
|
||||
logger.info('Initiating Snowflake OAuth with user credentials:', {
|
||||
accountUrl: snowflakeAccountUrl,
|
||||
hasClientId: !!snowflakeClientId,
|
||||
hasClientSecret: !!snowflakeClientSecret,
|
||||
})
|
||||
|
||||
// Call the authorize endpoint with user credentials
|
||||
try {
|
||||
const response = await fetch('/api/auth/snowflake/authorize', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accountUrl: snowflakeAccountUrl,
|
||||
clientId: snowflakeClientId,
|
||||
clientSecret: snowflakeClientSecret,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to initiate Snowflake OAuth')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Redirect to Snowflake authorization page
|
||||
window.location.href = data.authUrl
|
||||
} catch (error) {
|
||||
logger.error('Error initiating Snowflake OAuth:', error)
|
||||
// TODO: Show error to user
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
onClose()
|
||||
|
||||
logger.info('Linking OAuth2:', {
|
||||
@@ -401,7 +330,7 @@ export function OAuthRequiredModal({
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<ModalContent className='w-[460px]'>
|
||||
<ModalContent className='w-[460px]'>
|
||||
<ModalHeader>Connect {providerName}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
|
||||
@@ -7,7 +7,7 @@ export const SnowflakeBlock: BlockConfig<SnowflakeResponse> = {
|
||||
type: 'snowflake',
|
||||
name: 'Snowflake',
|
||||
description: 'Execute queries on Snowflake data warehouse',
|
||||
authMode: AuthMode.OAuth,
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Snowflake into your workflow. Execute SQL queries, insert, update, and delete rows, list databases, schemas, and tables, and describe table structures in your Snowflake data warehouse.',
|
||||
docsLink: 'https://docs.sim.ai/tools/snowflake',
|
||||
@@ -36,6 +36,7 @@ export const SnowflakeBlock: BlockConfig<SnowflakeResponse> = {
|
||||
value: () => 'execute_query',
|
||||
},
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
id: 'credential',
|
||||
title: 'Snowflake Account',
|
||||
type: 'oauth-input',
|
||||
@@ -45,10 +46,22 @@ export const SnowflakeBlock: BlockConfig<SnowflakeResponse> = {
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
=======
|
||||
>>>>>>> 8de761181 (reformatted to PAT from oauth)
|
||||
id: 'accountUrl',
|
||||
title: 'Account URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'your-account.snowflakecomputing.com',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Personal Access Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Snowflake PAT',
|
||||
description: 'Generate a PAT in Snowflake Snowsight',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
@@ -376,11 +389,11 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, operation, ...rest } = params
|
||||
const { operation, ...rest } = params
|
||||
|
||||
// Build base params
|
||||
// Build base params - use PAT directly as accessToken
|
||||
const baseParams: Record<string, any> = {
|
||||
credential,
|
||||
accessToken: params.accessToken,
|
||||
accountUrl: params.accountUrl,
|
||||
}
|
||||
|
||||
@@ -559,11 +572,14 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Snowflake OAuth credential' },
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
description: 'Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
warehouse: { type: 'string', description: 'Warehouse name' },
|
||||
role: { type: 'string', description: 'Role name' },
|
||||
query: { type: 'string', description: 'SQL query to execute' },
|
||||
|
||||
@@ -221,8 +221,6 @@ export const env = createEnv({
|
||||
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
|
||||
WEBFLOW_CLIENT_ID: z.string().optional(), // Webflow OAuth client ID
|
||||
WEBFLOW_CLIENT_SECRET: z.string().optional(), // Webflow OAuth client secret
|
||||
SNOWFLAKE_CLIENT_ID: z.string().optional(), // Snowflake OAuth client ID
|
||||
SNOWFLAKE_CLIENT_SECRET: z.string().optional(), // Snowflake OAuth client secret
|
||||
TRELLO_API_KEY: z.string().optional(), // Trello API Key
|
||||
LINKEDIN_CLIENT_ID: z.string().optional(), // LinkedIn OAuth client ID
|
||||
LINKEDIN_CLIENT_SECRET: z.string().optional(), // LinkedIn OAuth client secret
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
SalesforceIcon,
|
||||
ShopifyIcon,
|
||||
SlackIcon,
|
||||
SnowflakeIcon,
|
||||
TrelloIcon,
|
||||
WealthboxIcon,
|
||||
WebflowIcon,
|
||||
@@ -111,6 +110,7 @@ export type OAuthService =
|
||||
| 'zoom'
|
||||
| 'wordpress'
|
||||
| 'snowflake'
|
||||
|
||||
export interface OAuthProviderConfig {
|
||||
id: OAuthProvider
|
||||
name: string
|
||||
@@ -1529,44 +1529,17 @@ function buildAuthRequest(
|
||||
* This is a server-side utility function to refresh OAuth tokens
|
||||
* @param providerId The provider ID (e.g., 'google-drive')
|
||||
* @param refreshToken The refresh token to use
|
||||
* @param metadata Optional metadata (e.g., accountUrl, clientId, clientSecret for Snowflake)
|
||||
* @returns Object containing the new access token and expiration time in seconds, or null if refresh failed
|
||||
*/
|
||||
export async function refreshOAuthToken(
|
||||
providerId: string,
|
||||
refreshToken: string,
|
||||
metadata?: { accountUrl?: string; clientId?: string; clientSecret?: string }
|
||||
refreshToken: string
|
||||
): Promise<{ accessToken: string; expiresIn: number; refreshToken: string } | null> {
|
||||
try {
|
||||
// Get the provider from the providerId (e.g., 'google-drive' -> 'google')
|
||||
const provider = providerId.split('-')[0]
|
||||
|
||||
let config: ProviderAuthConfig
|
||||
|
||||
if (provider === 'snowflake' && metadata?.clientId && metadata?.clientSecret) {
|
||||
config = {
|
||||
tokenEndpoint: `https://${metadata.accountUrl}/oauth/token-request`,
|
||||
clientId: metadata.clientId,
|
||||
clientSecret: metadata.clientSecret,
|
||||
useBasicAuth: false,
|
||||
supportsRefreshTokenRotation: true,
|
||||
}
|
||||
logger.info('Using user-provided Snowflake OAuth credentials for token refresh', {
|
||||
accountUrl: metadata.accountUrl,
|
||||
hasClientId: !!metadata.clientId,
|
||||
hasClientSecret: !!metadata.clientSecret,
|
||||
})
|
||||
} else {
|
||||
config = getProviderAuthConfig(provider)
|
||||
|
||||
// For Snowflake without user credentials, use the account-specific token endpoint
|
||||
if (provider === 'snowflake' && metadata?.accountUrl) {
|
||||
config.tokenEndpoint = `https://${metadata.accountUrl}/oauth/token-request`
|
||||
logger.info('Using Snowflake account-specific token endpoint', {
|
||||
accountUrl: metadata.accountUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
const config = getProviderAuthConfig(provider)
|
||||
|
||||
// Build authentication request
|
||||
const { headers, bodyParams } = buildAuthRequest(config, refreshToken)
|
||||
|
||||
@@ -41,17 +41,12 @@ export const snowflakeDeleteRowsTool: ToolConfig<
|
||||
description: 'Delete rows from a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -113,7 +108,7 @@ export const snowflakeDeleteRowsTool: ToolConfig<
|
||||
headers: (params: SnowflakeDeleteRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeDeleteRowsParams) => {
|
||||
// Build DELETE SQL
|
||||
|
||||
@@ -17,17 +17,12 @@ export const snowflakeDescribeTableTool: ToolConfig<
|
||||
description: 'Get the schema and structure of a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -76,7 +71,7 @@ export const snowflakeDescribeTableTool: ToolConfig<
|
||||
headers: (params: SnowflakeDescribeTableParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeDescribeTableParams) => {
|
||||
const sanitizedDatabase = sanitizeIdentifier(params.database)
|
||||
|
||||
@@ -21,17 +21,12 @@ export const snowflakeExecuteQueryTool: ToolConfig<
|
||||
description: 'Execute a SQL query on your Snowflake data warehouse',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -86,7 +81,7 @@ export const snowflakeExecuteQueryTool: ToolConfig<
|
||||
headers: (params: SnowflakeExecuteQueryParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeExecuteQueryParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
|
||||
@@ -56,17 +56,12 @@ export const snowflakeInsertRowsTool: ToolConfig<
|
||||
description: 'Insert rows into a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -134,7 +129,7 @@ export const snowflakeInsertRowsTool: ToolConfig<
|
||||
headers: (params: SnowflakeInsertRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeInsertRowsParams) => {
|
||||
// Validate inputs
|
||||
|
||||
@@ -17,17 +17,12 @@ export const snowflakeListDatabasesTool: ToolConfig<
|
||||
description: 'List all databases in your Snowflake account',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -58,7 +53,7 @@ export const snowflakeListDatabasesTool: ToolConfig<
|
||||
headers: (params: SnowflakeListDatabasesParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListDatabasesParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
|
||||
@@ -17,17 +17,12 @@ export const snowflakeListFileFormatsTool: ToolConfig<
|
||||
description: 'List all file formats in a Snowflake schema',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -70,7 +65,7 @@ export const snowflakeListFileFormatsTool: ToolConfig<
|
||||
headers: (params: SnowflakeListFileFormatsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListFileFormatsParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
|
||||
@@ -17,17 +17,12 @@ export const snowflakeListSchemasTool: ToolConfig<
|
||||
description: 'List all schemas in a Snowflake database',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -64,7 +59,7 @@ export const snowflakeListSchemasTool: ToolConfig<
|
||||
headers: (params: SnowflakeListSchemasParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListSchemasParams) => {
|
||||
const sanitizedDatabase = sanitizeIdentifier(params.database)
|
||||
|
||||
@@ -17,17 +17,12 @@ export const snowflakeListStagesTool: ToolConfig<
|
||||
description: 'List all stages in a Snowflake schema',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -70,7 +65,7 @@ export const snowflakeListStagesTool: ToolConfig<
|
||||
headers: (params: SnowflakeListStagesParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListStagesParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
|
||||
@@ -17,17 +17,12 @@ export const snowflakeListTablesTool: ToolConfig<
|
||||
description: 'List all tables in a Snowflake schema',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -70,7 +65,7 @@ export const snowflakeListTablesTool: ToolConfig<
|
||||
headers: (params: SnowflakeListTablesParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListTablesParams) => {
|
||||
const sanitizedDatabase = sanitizeIdentifier(params.database)
|
||||
|
||||
@@ -14,17 +14,12 @@ export const snowflakeListViewsTool: ToolConfig<
|
||||
description: 'List all views in a Snowflake schema',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -67,7 +62,7 @@ export const snowflakeListViewsTool: ToolConfig<
|
||||
headers: (params: SnowflakeListViewsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListViewsParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
|
||||
@@ -17,17 +17,12 @@ export const snowflakeListWarehousesTool: ToolConfig<
|
||||
description: 'List all warehouses in the Snowflake account',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -58,7 +53,7 @@ export const snowflakeListWarehousesTool: ToolConfig<
|
||||
headers: (params: SnowflakeListWarehousesParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListWarehousesParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
|
||||
@@ -63,17 +63,12 @@ export const snowflakeUpdateRowsTool: ToolConfig<
|
||||
description: 'Update rows in a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
@@ -142,7 +137,7 @@ export const snowflakeUpdateRowsTool: ToolConfig<
|
||||
headers: (params: SnowflakeUpdateRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeUpdateRowsParams) => {
|
||||
// Validate inputs
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function executeSnowflakeStatement(
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user