diff --git a/apps/sim/app/api/auth/oauth2/shopify/store/route.ts b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts index dfa59fa77c6..c4a26459ecb 100644 --- a/apps/sim/app/api/auth/oauth2/shopify/store/route.ts +++ b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts @@ -3,7 +3,10 @@ import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { shopifyStoreCookieSchema } from '@/lib/api/contracts/oauth-connections' +import { + shopifyShopDomainSchema, + shopifyStoreCookieSchema, +} from '@/lib/api/contracts/oauth-connections' import { getSession } from '@/lib/auth' import { getBaseUrl } from '@/lib/core/utils/urls' import { isSameOrigin } from '@/lib/core/utils/validation' @@ -38,6 +41,11 @@ export const GET = withRouteHandler(async (request: NextRequest) => { } const { accessToken, shopDomain, scope, returnUrl } = parsedCookies.data + if (!shopifyShopDomainSchema.safeParse(shopDomain).success) { + logger.error('Invalid shop domain format in cookie', { shopDomain }) + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_invalid_domain`) + } + const shopResponse = await fetch(`https://${shopDomain}/admin/api/2024-10/shop.json`, { headers: { 'X-Shopify-Access-Token': accessToken, diff --git a/apps/sim/app/api/tools/tts/unified/route.ts b/apps/sim/app/api/tools/tts/unified/route.ts index 10c069b0607..ca7186e9b99 100644 --- a/apps/sim/app/api/tools/tts/unified/route.ts +++ b/apps/sim/app/api/tools/tts/unified/route.ts @@ -659,6 +659,13 @@ async function synthesizeWithAzure( throw new Error('text and apiKey are required for Azure TTS') } + const AZURE_REGION_RE = /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/ + if (!AZURE_REGION_RE.test(region)) { + throw new Error( + 'Invalid Azure region: must match /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/ (e.g. eastus, westeurope)' + ) + } + let ssml = `` if (style) { diff --git a/apps/sim/lib/api/contracts/tools/media/tts.ts b/apps/sim/lib/api/contracts/tools/media/tts.ts index 7fbc5643f99..3a5b68e50a8 100644 --- a/apps/sim/lib/api/contracts/tools/media/tts.ts +++ b/apps/sim/lib/api/contracts/tools/media/tts.ts @@ -54,7 +54,13 @@ export const ttsUnifiedToolBodySchema = z volumeGainDb: z.coerce.number().optional(), sampleRateHertz: z.coerce.number().optional(), effectsProfileId: z.array(z.string()).optional(), - region: z.string().optional(), + region: z + .string() + .regex( + /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/, + 'region must be a valid Azure region identifier (e.g. eastus, westeurope)' + ) + .optional(), rate: z.string().optional(), styleDegree: z.coerce.number().optional(), role: z.string().optional(), diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index b446fff9727..d0c1bc5422b 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1,3 +1,4 @@ +import { createHash } from 'crypto' import { cache } from 'react' import { sso } from '@better-auth/sso' import { stripe } from '@better-auth/stripe' @@ -1611,27 +1612,71 @@ export const auth = betterAuth({ clientSecret: env.WEALTHBOX_CLIENT_SECRET as string, authorizationUrl: 'https://app.crmworkspace.com/oauth/authorize', tokenUrl: 'https://app.crmworkspace.com/oauth/token', - userInfoUrl: 'https://dummy-not-used.wealthbox.com', // Dummy URL since no user info endpoint exists + userInfoUrl: 'https://api.crmworkspace.com/v1/me', scopes: getCanonicalScopesForProvider('wealthbox'), responseType: 'code', redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/wealthbox`, - getUserInfo: async (_tokens) => { + getUserInfo: async (tokens) => { try { - logger.info('Creating Wealthbox user profile from token data') + logger.info('Fetching Wealthbox user profile') + + const response = await fetch('https://api.crmworkspace.com/v1/me', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) - const uniqueId = 'wealthbox-user' const now = new Date() + if (response.ok) { + const data = await response.json() + const userId = data.id?.toString() + if (!userId) { + return null + } + const email = + data.email && typeof data.email === 'string' + ? data.email + : `wealthbox-${userId}@wealthbox.user` + const name = data.name || data.full_name || data.username || 'Wealthbox User' + + return { + id: `wealthbox-${userId}-${generateId()}`, + name, + email, + emailVerified: false, + createdAt: now, + updatedAt: now, + } + } + + // Fallback: derive a stable identifier from the refresh token (long-lived) + // rather than the access token (rotates every ~2 hours) to avoid creating + // duplicate accounts on token refresh. + logger.warn( + 'Wealthbox user info fetch failed, falling back to token-derived identity', + { + status: response.status, + } + ) + const stableToken = tokens.refreshToken ?? tokens.accessToken + if (!stableToken) { + logger.error('Wealthbox fallback identity: no refresh or access token available') + return null + } + const tokenHash = createHash('sha256').update(stableToken).digest('hex').slice(0, 24) return { - id: `${uniqueId}-${generateId()}`, + id: `wealthbox-${tokenHash}-${generateId()}`, name: 'Wealthbox User', - email: `${uniqueId}@wealthbox.user`, + email: `wealthbox-${tokenHash}@wealthbox.user`, emailVerified: false, createdAt: now, updatedAt: now, } } catch (error) { - logger.error('Error creating Wealthbox user profile:', { error }) + logger.error('Error creating Wealthbox user profile:', { + error: toError(error).message, + }) return null } }, @@ -1730,11 +1775,12 @@ export const auth = betterAuth({ } logger.info('HubSpot token metadata response:', { + hubId: data.hub_id, + hubDomain: data.hub_domain, + userId: data.user_id, hasScopes: !!data.scopes, scopesType: typeof data.scopes, scopesIsArray: Array.isArray(data.scopes), - scopesValue: data.scopes, - fullResponse: data, }) return {